多线程并发

一, 开启多线程的两种方式

# 方式一
from threading import Thread
import time

def sayhi(name):
    time.sleep(2)
    print(f'{name} say hi')

if __name__ == '__main__':
    t = Thread(target=sayhi, args=('麻花腾',))
    t.start()
    print('in 主线程')
-----------------------------------------------------------
# 方式二
from threading import Thread
import time
class Sayhi(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
    def run(self):
        time.sleep(2)
        print(f'{self.name} say hello')

if __name__ == '__main__':
    t = Sayhi('孙悟空')
    t.start()
    print('in 主线程')

二, 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别

# 开启速度对比
from threading import Thread
from multiprocessing import Process

def sayhi(name):
    print(f'{name} say hi')

if __name__ == '__main__':
    t = Thread(target=sayhi, args=('孙悟空',))
    p = Process(target=sayhi, args=('猪八戒',))
    t.start()
    p.start()
    print('in 主进程/主线程')
# 孙悟空 say hi
# in 主进程/主线程
# 猪八戒 say hi
# 线程的开启速度比进程的开启速度快
# pid对比
from threading import Thread
from multiprocessing import Process
import os

def sayhi(name):
    print(f'{name}{os.getpid()} say hi')

if __name__ == '__main__':
    t = Thread(target=sayhi, args=('子线程',))
    p = Process(target=sayhi, args=('子进程',))
    t.start()
    p.start()
    print('in 主进程/主线程', os.getpid())
# 子线程12812 say hi
# in 主进程/主线程 12812
# 子进程12648 say hi
# 在主进程下开启多个线程,每个线程都跟主进程的pid一样
# 开多个进程,每个进程都有不同的pid
# 同一进程内的线程共享数据
from threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n = 0

if __name__ == '__main__':
    # n = 100
    # p = Process(target=work)
    # p.start()
    # p.join()
    # print('主进程', n)
# 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
    n = 1
    t = Thread(target=work)
    t.start()
    t.join()
    print('主线程', n)
# 查看结果为0,因为同一进程内的线程之间共享进程内的数据
# socket小练习
# 多线程实现并发: server端
from threading import Thread
import socket

server = socket.socket()
server.bind(('127.0.0.1', 2019))
server.listen(5)

def action(conn):
    while 1:
        data = conn.recv(1024).decode('utf-8')
        print(data)
        conn.send(data.upper().encode('utf-8'))

if __name__ == '__main__':
    while 1:
        conn, addr = server.accept()
        t = Thread(target=action, args=(conn,))
        t.start()
----------------------------------------------------------
# client端
import socket

client = socket.socket()
client.connect(('127.0.0.1', 2019))

while 1:
    t_s = input('>>>').strip()
    if not t_s:
        continue
    client.send(t_s.encode('utf-8'))
    f_s = client.recv(1024).decode('utf-8')
    print(f_s)

三, 线程相关的其他方法

from threading import Thread
import threading
import time
def sayhi(name):
    time.sleep(2)
    print(f'{name} say hi')

if __name__ == '__main__':
    t = Thread(target=sayhi, args=('麻花腾',))
    t.start()
    # 线程对象的方法
    print(t.is_alive())  # 判断子线程是否存活
    print(t.getName())   # 返回线程名
    t.setName('线程11')  # 设置线程名
    print(t.getName())

    # threading模块的方法:
    print(threading.current_thread().name)  # 获取此线程对象,可以使用此对象
    print(threading.enumerate())  # 返回一个列表,放置的是所有线程对象
    print(threading.active_count())   # 获取活跃的线程个数,包括主线程

四, 守护线程

# 回顾守护进程
from multiprocessing import Process
import time
def foo():
    print(123)  # 开启进程太慢了,还没来得及打印,主进程就结束了
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':

    p1 = Process(target=foo)
    p2 = Process(target=bar)

    p1.daemon = True
    p1.start()
    p2.start()
    print("main-------")
# main-------
# 456
# end456
# 守护线程
from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    # 比非守护线程短,主线程等到所有非守护子线程结束才结束,此守护线程已经运行完毕
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':

    t1 = Thread(target=foo)
    t2 = Thread(target=bar)

    t1.daemon = True
    t1.start()
    t2.start()
    print("main-------")
# 123
# 456
# main-------
# end123
# end456
-----------------------------------------------------------
from threading import Thread
import time
def foo():
    print(123)
    time.sleep(4)  
    # 比非守护线程长,非守护线程结束时主线程也结束,此守护线程也同时结束,后面不会打印
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':

    t1 = Thread(target=foo)
    t2 = Thread(target=bar)

    t1.daemon = True
    t1.start()
    t2.start()
    print("main-------")
# 123
# 456
# main-------
# end456
# 多线程是同一个空间,同一个进程
# 主线程是进程空间存活在内存的必要条件
# 主线程会等所有非守护子线程结束才能结束
  • 无论是进程还是线程,都遵循: 守护xx会等待主xx运行完毕后被销毁

    • 需要强调的是: 运行完毕并非终止运行

    • 对主进程来说,运行完毕指的是主进程代码运行完毕

      主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束

    • 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

      主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收).因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束

五, 互斥锁,同步锁,锁

  • 多线程的同步锁与多进程的同步锁是一个道理,就是多个线程抢占同一个数据(资源)时,我们要保证数据的安全,合理的顺序

  • 不加锁抢占同一个资源的问题

    from threading import Thread
    import time
    x = 100
    
    def task():
        global x
        temp = x
        time.sleep(1)
        temp -= 1
        x = temp
    
    if __name__ == '__main__':
        t1 = Thread(target=task)
        t1.start()
        t1.join()
        print('主线程', x)  # 99
    ------------------------------------------------------
    from threading import Thread
    import time
    x = 100
    
    def task():
        global x
        temp = x
        time.sleep(1)
        temp -= 1
        x = temp
    
    if __name__ == '__main__':
        lst = []
        for i in range(100):
            t = Thread(target=task)
            t.start()
            lst.append(t)
        for el in lst:
            el.join()
        print('主线程', x)  # 99
        # 因为100个子线程近乎同时运行,同时取得temp=100,又分别对x赋值99
    
  • 加锁:

    from threading import Thread
    from threading import Lock
    import time
    
    x = 100
    def task(lock):
        lock.acquire()
        global x
        temp = x
        time.sleep(0.1)
        temp -= 1
        x = temp
        lock.release()
    
    if __name__ == '__main__':
        lock = Lock()
        lst = []
        for i in range(100):
            t = Thread(target=task, args=(lock,))
            t.start()
            lst.append(t)
        for el in lst:
            el.join()
        print('主线程', x)  # 0
    
    # 互斥锁与join的区别
    # 互斥锁随机抢锁,公平  join提前排好顺序,不公平   都是串行
    

六, 死锁现象与递归锁

  • 进程也有死锁与递归锁,进程的死锁和递归锁与线程的死锁递归锁同理

  • 死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

    # 死锁现象
    from threading import Thread
    from threading import Lock
    import time
    lock_A = Lock()
    lock_B = Lock()
    class Sayhi(Thread):
        def run(self):
            self.f1()
            self.f2()
    
        def f1(self):
            lock_A.acquire()
            print(f'{self.name}拿到A锁')
            lock_B.acquire()
            print(f'{self.name}拿到B锁')
            lock_B.release()
            lock_A.release()
    
        def f2(self):
            lock_B.acquire()
            print(f'{self.name}拿到B锁')
            time.sleep(1)
            lock_A.acquire()
            print(f'{self.name}拿到A锁')
            lock_A.release()
            lock_B.release()
    if __name__ == '__main__':
        t1 = Sayhi()
        t1.start()
        t2 = Sayhi()
        t2.start()
        t3 = Sayhi()
        t3.start()
        print('in 主线程')
    # Thread-1拿到A锁
    # Thread-1拿到B锁
    # Thread-1拿到B锁
    # Thread-2拿到A锁
    # in 主线程
    # 程序夯住了
    
  • 解决方法: 递归锁R Lock

    # 锁上有counter变量,只要acquire一次,counter就加一,release一次,counter减一,只要counter不为0,其他线程不能抢
    from threading import Thread
    from threading import RLock
    import time
    
    lock_B = lock_A = RLock()
    class Sayhi(Thread):
        def run(self):
            self.f1()
            self.f2()
    
        def f1(self):
            lock_A.acquire()
            print(f'{self.name}拿到A锁')
            lock_B.acquire()
            print(f'{self.name}拿到B锁')
            lock_B.release()
            lock_A.release()
    
        def f2(self):
            lock_B.acquire()
            print(f'{self.name}拿到B锁')
            time.sleep(1)
            lock_A.acquire()
            print(f'{self.name}拿到A锁')
            lock_A.release()
            lock_B.release()
    if __name__ == '__main__':
        t1 = Sayhi()
        t1.start()
        t2 = Sayhi()
        t2.start()
        t3 = Sayhi()
        t3.start()
        print('in 主线程')
    

七, 信号量Semaphore

  • 同进程一样

  • Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()

    # 实例: 同时只有5个线程可以获得semaphore,即可以限制最大连接数为5
    from threading import Thread
    from threading import Semaphore
    from threading import current_thread
    import time
    import random
    
    sem = Semaphore(5)
    
    def go_public_wc():
        sem.acquire()
        print(f'{current_thread().getName()} 上厕所ing')
        time.sleep(random.randint(1, 3))
        sem.release()
    
    if __name__ == '__main__':
        for i in range(1, 21):
            t = Thread(target=go_public_wc, name=f'{i}号')
            t.start()
    

八, GIL全局解释锁

  • GIL的全称是: Global Interpreter Lock,意思就是全局解释器锁,这个GIL并不是python的特性,他是只在Cpython解释器里引入的一个概念,而在其他的语言编写的解释器里就没有这个GIL例如: Jython,Pypy

  • 为什么会有gil: 随着电脑多核cpu的出现和cpu频率的提升,为了充分利用多核处理器,进行多线程的编程方式更为普及,随之而来的困难是线程之间数据的一致性和状态同步,而python也利用了多核,所以也逃不开这个困难,为了解决这个数据不能同步的问题,设计了gil全局解释器锁

  • GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全.

  • 在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内.

    • 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(est.py的所有代码以及Cpython解释器的所有代码).例如: test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行.

    • 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码.

    • 所以执行流程是:

      多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行.解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题: 对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

    • GIL与Lock

      GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理

    • GIL与多线程

      我们有四个任务需要处理,处理方式肯定是要并发的效果,解决方案可以是:
      方案一: 开启四个进程
      方案二: 一个进程下,开启四个线程
      
      # 单核情况下,分析结果: 
        如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
        如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
      
      # 多核情况下,分析结果:
        如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
        如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
      

      结论:

      • 现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的
      • 多核的前提下: 如果任务是IO密集型,使用多线程并发; 如果任务是计算密集型,使用多进程并发
    • 多线程与多进程性能测试

      # 计算密集型
      from multiprocessing import Process
      from threading import Thread
      import time
      
      def task():
          res = 1
          for i in range(1, 10000000):
              res += i
      
      if __name__ == '__main__':
          start_time = time.time()
          lst1 = []
          for i in range(4):
              p = Process(target=task)
              lst1.append(p)
              p.start()
          for i in lst1:
              i.join()
          print(f'总共用了:{time.time() - start_time}')
          # 四个进程并行总共用了:1.272585868835449
      
          start_time = time.time()
          lst1 = []
          for i in range(4):
              t = Thread(target=task)
              lst1.append(t)
              t.start()
          for i in lst1:
              i.join()
          print(f'总共用了:{time.time() - start_time}')
          # 一个进程四个线程并发总共用了:3.891002655029297
      
      # 计算密集型: 多进程的并行比单进程的多线程并发效率高很多
      
      # 讨论IO密集型:  通过大量的任务去验证
      from multiprocessing import Process
      from threading import Thread
      import time
      
      def task():
          time.sleep(3)
      
      if __name__ == '__main__':
          start_time = time.time()
          lst1 = []
          for i in range(150):
              p = Process(target=task)
              lst1.append(p)
              p.start()
          for i in lst1:
              i.join()
          print(f'总共用了:{time.time() - start_time}')
          # 开启150个进程,开销大,速度慢,执行IO任务,总共用了:8.147555351257324
      
          start_time = time.time()
          lst1 = []
          for i in range(150):
              t = Thread(target=task)
              lst1.append(t)
              t.start()
          for i in lst1:
              i.join()
          print(f'总共用了:{time.time() - start_time}')
          # 开启150个线程,开销小,速度快,执行IO任务,总共用了:3.0526199340820312
      
      # 任务是IO密集型,并且任务数量很大,用单进程下的多线程效率高
      

九, concurrent.futures -- 进程池/线程池模块

  • 系统启动一个新线程/新进程的成本是比较高的,因为它涉及与操作系统的交互.在这种情形下,使用线程池/进程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程/进程时,更应该考虑使用线程池/进程池.

  • 线程池/进程池在系统启动时即创建大量空闲的线程/进程,程序只要将一个函数提交给线程池/进程池,线程池/进程池就会启动一个空闲的线程/进程来执行它.当该函数执行结束后,该线程/进程并不会死亡,而是再次返回到线程池/进程池中变成空闲状态,等待执行下一个函数

  • 提升效率,资源复用

  • 此外,使用线程池/进程池可以有效地控制系统中并发线程/进程的数量

  • 从Python 3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,实现了对threading和multiprocessing的更高级的抽象,对编写线程池/进程池提供了直接的支持

  • 常用方法

    concurrent.futures模块提供了高度封装的异步调用接口
    ThreadPoolExecutor:线程池,提供异步调用
    ProcessPoolExecutor: 进程池,提供异步调用
    
    1.submit(fn, *args, **kwargs): 将fn函数提交给线程池.*args代表传给fn函数的参数,*kwargs代表以关键字参数的形式为fn函数传入参数
    2.map(func, *iterables, timeout=None, chunksize=1): 该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对iterables执行 map处理
    3.shutdown(wait=True): 关闭线程池
    相当于进程池的pool.close() + pool.join()操作
    wait=True,等待池内所有任务执行完毕回收完资源后才继续
    wait=False,立即返回,并不会等待池内的任务执行完毕
    但不管wait参数为何值,整个程序都会等到所有任务执行完毕
    submit和map必须在shutdown之前
    
    程序将task函数提交(submit)给线程池后,submit方法会返回一个Future对象,Future类主要用于获取线程任务函数的返回值.由于线程任务会在新线程中以异步方式执行,因此,线程执行的函数相当于一个“将来完成”的任务,所以Python使用Future来代表.
    Future 提供了如下方法: 
    1. cancel(): 取消该 Future 代表的线程任务.如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True.
    2. cancelled(): 返回 Future 代表的线程任务是否被成功取消.
    3. running(): 如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True.
    4. done(): 如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True.
    5. result(timeout=None): 获取该 Future 代表的线程任务最后返回的结果.如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒.
    6. exception(timeout=None): 获取该 Future 代表的线程任务所引发的异常.如果该任务成功完成,没有异常,则该方法返回 None.
    7. add_done_callback(fn): 为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数.
    
  • 基本使用

    from concurrent.futures import ThreadPoolExecutor
    from concurrent.futures import ProcessPoolExecutor
    import threading
    import time
    
    def func(n):
        time.sleep(2)
        print(f'{threading.get_ident()}打印的:{n}')
        return n**2
    
    tpool = ThreadPoolExecutor(max_workers=5) # 默认线程池的线程数为CPU个数*5
    # tpool = ProcessPoolExecutor(max_workers=5) # 进程池,默认进程池的进程数为CPU个数
    
    # 异步执行
    if __name__ == '__main__':
        t_lst = []
        for i in range(5):
            t = tpool.submit(func, i)
            # 提交执行函数,返回一个结果对象,i作为任务函数的参数
            t_lst.append(t)
            # print(t.result())
            # 这个返回的结果对象t,不能直接去拿结果,不然又变成串行了
            # 可以理解为拿到一个号码,等所有线程的结果都出来之后
            # 我们再去通过结果对象t获取结果
        tpool.shutdown() # 起到原来的close阻止新任务进来 + join的作用,等待所有的线程执行完毕
        print('in 主进程/主线程')
    
        for el in t_lst:
            print('结果:', el.result())
    
    # 12860打印的:1
    # 19164打印的:0
    # 4540打印的:3
    # 5284打印的:2
    # 19404打印的:4
    # in 主进程/主线程
    # 结果: 0
    # 结果: 1
    # 结果: 4
    # 结果: 9
    # 结果: 16
    # 结果分析: 打印的结果是没有顺序的,因为到了func函数中的sleep的时候线程会切换,谁先打印就没准儿了,但是最后通过结果对象取结果的时候拿到的是有序的,因为主线程进行for循环的时候,是按顺序将结果对象添加到列表中的
    
    
  • map的使用

    map(func, *iterables, timeout=None, chunksize=1)方法,该方法的功能类似于全局函数 map(),区别在于线程池的 map() 方法会为 iterables 的每个元素启动一个线程,以并发方式来执行 func 函数.这种方式相当于启动 len(iterables) 个线程,井收集每个线程的执行结果

    from concurrent.futures import ThreadPoolExecutor
    import threading
    import time
    def task(n):
        print(f'{threading.get_ident()}正在运行')
        time.sleep(2)
        return n*n
    
    if __name__ == '__main__':
        tpool = ThreadPoolExecutor(max_workers=5)
        # lst = []
        # for i in range(1, 5):
        #     future = tpool.submit(task, i)
        #     lst.append(future)
        # for el in lst:
        #     print(el.result())
    
        s = tpool.map(task, range(1, 5))  # 取代了for循环+submit
        # 启动4个线程,并收集每个线程的执行结果
        print([i for i in s])
    
  • 回调函数的使用

    from concurrent.futures import ThreadPoolExecutor
    import threading
    import time
    def task(n):
        print(f'{threading.get_ident()}正在运行')
        time.sleep(2)
        return n*n
    
    def call_back(m):
        print('结果为:', m.result())
    
    if __name__ == '__main__':
        tpool = ThreadPoolExecutor(5)
        t_lst = []
        for i in range(5):
            t = tpool.submit(task, i).add_done_callback(call_back)
    
    from concurrent.futures import ProcessPoolExecutor
    from multiprocessing import Pool
    import requests
    import os
    
    def get_page(url):
        print(f'进程{os.getpid()}拿到:{url}')
        res = requests.get(url)
        if res.status_code == 200:
            return {'url': url, 'text': res.text}
    
    def parse_page(res):
        res = res.result()
        print(f'进程{os.getpid()}分析{res["url"]}')
        parse_res = f'url:{res["url"]}  size:{len(res["text"])}\n'
        with open('db.txt', mode='a') as f:
            f.write(parse_res)
    
    if __name__ == '__main__':
        urls = [
            'https://www.baidu.com',
            'https://www.python.org',
            'https://www.openstack.org',
            'https://help.github.com/',
            'http://www.sina.com.cn/'
        ]
        # p = Pool(3)
        # for url in urls:
        #     p.apply_async(get_page, args=(url,), callback=parse_page)
        # p.close()
        # p.join()
        p = ProcessPoolExecutor(3)
        for url in urls:
            p.submit(get_page, url).add_done_callback(parse_page)
        # parse_page拿到的是一个future对象obj,需要用obj.result()拿到结果
    
  • 异步,同步;阻塞,非阻塞

    站在任务发布的角度:

    • 同步: 任务发出去之后,等待结果,直到这个任务最终结束之后,返回结果,再发布下一个任务.

    • 异步: 所有的任务同时发出, 不会在原地等待结果返回.

    程序运行中表现的状态: 阻塞, 运行,就绪

    • 阻塞: 程序遇到IO阻塞. 程序遇到IO立马会停止(挂起), cpu马上切换,等到IO结束之后,再执行.

    • 非阻塞: 程序没有IO或者遇到IO通过某种手段让cpu去执行其他的任务,尽可能的占用cpu.

    # 异步回收任务的方式一: 将所有任务的结果统一收回
    from concurrent.futures import ProcessPoolExecutor
    import os
    import time
    import random
    def task():
        print(f'{os.getpid()} is running')
        time.sleep(random.randint(1, 3))
        return f'{os.getpid()} is finish'
    
    if __name__ == '__main__':
        p = ProcessPoolExecutor(4)
        lst = []
        for i in range(10):
            res = p.submit(task)   # 异步发出
            lst.append(res)
            # print(res.result())  # 在这里result()就会变成同步
        p.shutdown(wait=True)
        # 1.阻止再向进程池投放新的任务
        # 2.wait=True 一个任务完成了就减一,直至为0才执行下一行
        for res in lst:
            print(res.result())
    
  • 异步加回调机制

    # 浏览器做的事情很简单,封装一些头部,发一个请求到服务器,服务器拿到请求信息,分析信息,分析正确之后,给浏览器返回一个文件,浏览器将这个文件的代码渲染就成了网页
    # 爬虫: 利用requests模块,模拟浏览器,封装头给服务器发送请求,骗过服务器,服务器也给你返回一个文件,
    # 爬虫拿到文件进行数据清洗,获取想要的信息
    # 爬虫: 分两步
    #   第一步: 爬取服务端的文件(IO阻塞)
    #   第二步: 拿到文件,进行数据清洗(非IO,极少IO)
    
    # 版本一
    from concurrent.futures import ProcessPoolExecutor
    import requests
    import time
    import os
    import random
    def get(url):  # 爬取文件
        response = requests.get(url)
        print(os.getpid(), '正在爬取:', url)
        time.sleep(random.randint(1, 3))
        if response.status_code == 200:
            return response.text
    
    def parse(text): # 对爬取回来的字符串的分析,用len模拟一下
        print('分析结果:', len(text))
    
    if __name__ == '__main__':
        url_list = [
            'https://www.baidu.com',
            'https://www.python.org',
            'https://www.openstack.org',
            'https://help.github.com/',
            'http://www.sina.com.cn/',
            'https://www.cnblogs.com/'
        ]
        pool = ProcessPoolExecutor(4)
        obj_list = []
        for url in url_list:
            obj = pool.submit(get, url)
            obj_list.append(obj)
        pool.shutdown(wait=True)
        for obj in obj_list:
            text = obj.result()
            parse(text)
    # 问题出在哪里?
    # 1.分析结果的过程是串行,效率低
    # 2.将所有的结果全部爬取成功之后,放在一个列表中
    -------------------------------------------------------
    # 版本二:异步处理,获取结果的第二种方式
    # 完成一个任务,返回一个结果,并发的获取结果
    from concurrent.futures import ProcessPoolExecutor
    import requests
    import time
    import os
    import random
    def get(url):  # 爬取文件
        response = requests.get(url)
        print(os.getpid(), '正在爬取:', url)
        time.sleep(random.randint(1, 3))
        if response.status_code == 200:
            parse(response.text)
            # return response.text
    
    def parse(text): # 对爬取回来的字符串的分析,用len模拟一下
        print('分析结果:', len(text))
    
    if __name__ == '__main__':
        url_list = [
            'https://www.baidu.com',
            'https://www.python.org',
            'https://www.openstack.org',
            'https://help.github.com/',
            'http://www.sina.com.cn/',
            'https://www.cnblogs.com/'
        ]
        pool = ProcessPoolExecutor(4)
        for url in url_list:
            obj = pool.submit(get, url)
        pool.shutdown(wait=True)
    # 问题,增强了耦合性
    ------------------------------------------------------
    # 版本三: 版本二,两个任务有耦合性.在上一个基础上,对其进行解耦
    from concurrent.futures import ProcessPoolExecutor
    import requests
    import time
    import os
    import random
    def get(url):  # 爬取文件
        response = requests.get(url)
        print(os.getpid(), '正在爬取:', url)
        time.sleep(random.randint(1, 3))
        if response.status_code == 200:
            return response.text
    
    def parse(obj): # 对爬取回来的字符串的分析,用len模拟一下
        print(f'{os.getpid()}分析结果:', len(obj.result()))
    
    if __name__ == '__main__':
        url_list = [
            'https://www.baidu.com',
            'https://www.python.org',
            'https://www.openstack.org',
            'https://help.github.com/',
            'http://www.sina.com.cn/',
            'https://www.cnblogs.com/'
        ]
        pool = ProcessPoolExecutor(4)
        for url in url_list:
            obj = pool.submit(get, url)
            obj.add_done_callback(parse)  # 增加一个回调函数
            # 现在的进程完成的还是网络爬取的任务,拿到返回值之后,丢给回调函数,
            # 进程继续完成下一个任务,回调函数进行分析结果
        pool.shutdown(wait=True)
    # 回调函数是主进程实现的,回调函数帮我们进行分析任务
    # 明确了进程的任务就是网络爬取,分析任务交给回调函数执行,对函数之间解耦
    
    # 极值情况: 如果回调函数是IO任务,那么由于回调函数是主进程做的,所以有可能影响效率
    # 回调不是万能的,如果回调的任务是IO,那么异步+回调机制不好,此时如果需要效率,只能再开一个线程或进程池
    
    # 异步就是回调?
    # 这个论点是错的,异步,回调是两个概念
    
    # 如果多个任务,多进程多线程处理的IO任务
    # 1. 剩下的任务 非IO阻塞   异步+回调机制
    # 2. 剩下的任务有 IO  远小于  多个任务的IO   异步+回调机制
    # 3. 剩下的任务 IO 大于等于 多个任务的IO  第二种解决方式,或者开启两个进程/线程池
    

十, 线程队列(进程相同)

  • FIFO: 先进先出

    import queue
    #不需要通过threading模块里面导入,直接import queue就可以了,这是python自带的
    #用法基本和我们进程multiprocess中的queue是一样的
    q = queue.Queue()
    q.put('first')
    q.put('second')
    q.put('third')
    print(q.get())
    print(q.get())
    print(q.get())
    '''
    结果(先进先出):
    first
    second
    third
    '''
    
  • LIFO: 后进先出(栈)

    import queue
    q = queue.LifoQueue() # 队列,类似于栈
    q.put('first')
    q.put('second')
    q.put('third')
    
    print(q.get())
    print(q.get())
    print(q.get())
    '''
    结果(后进先出):
    third
    second
    first
    '''
    
  • Priority: 优先级队列

    import queue
    
    q = queue.PriorityQueue()
    # put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
    q.put((-10, 'a'))
    q.put((-5, 'a'))  #负数也可以
    # q.put((20, 'ws'))  #如果两个值的优先级一样,那么按照后面的值的acsii码顺序来排序,如果字符串第一个数元素相同,比较第二个元素的acsii码顺序
    # q.put((20, 'wd'))
    # q.put((20, {'a': 11})) #TypeError: unorderable types: dict() < dict() 不能是字典
    # q.put((20, ('w', 1)))  #优先级相同的两个数据,他们后面的值必须是相同的数据类型才能比较,可以是元祖,也是通过元素的ascii码顺序来排序
    
    q.put((20, 'b'))
    q.put((20, 'a'))
    q.put((0, 'b'))
    q.put((30, 'c'))
    
    print(q.get())
    print(q.get())
    print(q.get())
    print(q.get())
    print(q.get())
    print(q.get())
    '''
    结果(数字越小优先级越高,优先级高的优先出队):
    (-10, 'a')
    (-5, 'a')
    (0, 'b')
    (20, 'a')
    (20, 'b')
    (30, 'c')
    '''
    

十一, Event事件

  • 线程的一个关键特性是每个线程都是独立运行且状态不可预测.如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手.为了解决这些问题,我们需要使用threading库中的Event对象. 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生.在初始情况下,Event对象中的信号标志被设置为假.如果有线程等待一个Event对象,而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真.一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程.如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件,继续执行(进程也一样)

  • 方法

    event.isSet(): 返回event的状态值
    
    event.wait(): 如果 event.isSet() == False将阻塞线程
    
    event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度
    
    event.clear(): 恢复event的状态值为False
    
  • 示例

    from threading import Thread
    from threading import current_thread
    from threading import Event
    import time
    event = Event()  # 默认False
    def task():
        print(f'{current_thread().name}检测服务器是否正常开启....')
        time.sleep(3)
        event.set()  # 改成True
    
    def task1():
        print(f'{current_thread().name}正在尝试连接服务器')
        event.wait()  # 阻塞,轮询检测event是否为True,当其为True,继续下一行代码
        # event.wait(1) # 超时时间,超过时间无论是否为True都继续下一行代码
        print(f'{current_thread().name}连接成功')
    
    if __name__ == '__main__':
        t1 = Thread(target=task1)
        t2 = Thread(target=task1)
        t3 = Thread(target=task1)
        t = Thread(target=task)
        t1.start()
        t2.start()
        t3.start()
        t.start()
    

十二, Condition条件

  • 使得线程等待,只有满足某条件时,才(依次)释放n个线程(串行)

    import time
    from threading import Thread
    from threading import Condition
    from threading import current_thread
    
    def func1(c):
        c.acquire(False)  # 固定格式
    
        c.wait()  # 等待通知,
        time.sleep(3)  # 通知完成后大家是串行执行的,这也看出了锁的机制了
        print(f'{current_thread().name}执行了')
    
        c.release()
    
    if __name__ == '__main__':
        c = Condition()
        for i in range(5):
            t = Thread(target=func1, args=(c,))
            t.start()
    
        num = int(input('请输入你要通知的线程个数:'))
        c.acquire()  # 固定格式
        c.notify(num)  # 通知num个线程别等待了,去执行吧
        c.release()
    

十三, 定时器

  • 定时器,指定n秒后执行某个操作

    from threading import Thread
    from threading import Event
    import threading
    import time
    import random
    def conn_mysql():
        count = 1
        while not event.is_set():
            if count > 3:
                raise TimeoutError('链接超时')
            print(f'{threading.current_thread().getName()}第{count}次尝试链接')
            event.wait(0.5)
            count += 1
        print(f'{threading.current_thread().getName()}链接成功')
    
    def check_mysql():
        print(f'{threading.current_thread().getName()}正在检查mysql')
        time.sleep(random.randint(1, 3))
        event.set()
    if __name__ == '__main__':
        event = Event()
        conn1 = Thread(target=conn_mysql)
        conn2 = Thread(target=conn_mysql)
        check = Thread(target=check_mysql)
    
        conn1.start()
        conn2.start()
        check.start()
    
posted @ 2019-07-27 16:14  怀心抱素  阅读(144)  评论(0编辑  收藏  举报