并发编程之多线程
目录:
一 什么是线程
二 开启线程的两种方式
三 主进程下开启线程与主进程下开启子进程,谁的开启速度快?
四 Thread对象的其他属性或方法
五 守护线程
六 GIL全局解释器锁
七 死锁现象与递归锁
八 信号量、Event、定时器
九 线程queue
十 进程池与线程池
练习题
一 什么是线程
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
线程顾名思义,就是一条流水线工作的过程(流水线的工作需要电源,电源就相当于cpu),而一条流水线必须属于一个车间,一个车间的工作过程是一个进程,车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一条流水线。
所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
多线程(即多个控制线程)的概念是,在一个进程中存在多个线程,多个线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
1)、线程与进程的区别
1、线程共享创建它的进程的地址空间;进程有自己的地址空间。
2、线程可以直接访问进程的数据段;进程拥有父进程的数据段的自己的副本。
3、线程可以直接与进程的其他线程通信;进程必须使用进程间通信来与兄弟进程通信。
4、新线程很容易创建;新进程需要复制父进程。
5、线程可以对同一进程的线程进行相当大的控制;流程只能对子流程进行控制。
6、对主线程的更改(取消、优先级更改等)可能会影响进程中其他线程的行为;对父进程的更改不会影响子进程。
2)、多线程的应用场景
1、 常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)
2、 servlet多线程
3、 FTP下载,多线程操作文件
4、 数据库用到的多线程
5、 分布式计算
二 开启线程的两种方式
multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性。
方式一:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread 2 import time 3 4 def func(name): 5 time.sleep(2) 6 print('%s say hello' % name) 7 8 9 if __name__ == '__main__': 10 t = Thread(target=func, args=('eye',)) 11 t.start() 12 print('主线程')
方式二:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread 2 import time 3 class MyThread(Thread): 4 def __init__(self, name): 5 super().__init__() 6 self.name = name 7 8 def run(self): 9 time.sleep(2) 10 print("%s say hello" % self.name) 11 12 13 if __name__ == '__main__': 14 t = MyThread('eye') 15 t.start() 16 print('主线程')
三 主进程下开启线程与主进程下开启子进程,谁的开启速度快?
1、开进程的开销园大于开线程。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread 2 from multiprocessing import Process 3 import time 4 5 def func(name): 6 print("%s say hello" % name) 7 time.sleep(2) 8 print('%s say hello to' % name) 9 10 11 12 if __name__ == '__main__': 13 start_time = time.time() 14 p = Process(target=func, args=("eye",)) 15 p.start() 16 17 # t = Thread(target=func, args=('eye',)) 18 # t.start() 19 20 print('主') 21 stop_time = time.time() 22 print(stop_time-start_time)
2、同一进程内的多个线程共享该进程的地址空间。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread 2 from multiprocessing import Process 3 n = 100 4 def func(name): 5 global n 6 n = 0 7 8 9 if __name__ == '__main__': 10 11 # p = Process(target=func, args=("eye",)) 12 # p.start() 13 14 t = Thread(target=func, args=('eye',)) 15 t.start() 16 17 print('主',n) 18 # 进程运行结果是100, 线程是0
3、看看pid
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread 2 import os 3 4 def func(name): 5 print("线程1 <%s>" % os.getpid()) 6 7 8 if __name__ == '__main__': 9 t = Thread(target=func, args=('eye',)) 10 t.start() 11 t.join() 12 print( '主', os.getpid()) 13 14 """ 15 输出:线程1 <17228> 16 主 17228 17 """
四 Thread对象的其他属性或方法
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。是返回True # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
代码实列
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,currentThread 2 import time 3 import threading 4 5 def work(): 6 print("%s is running" % currentThread().getName()) 7 time.sleep(2) 8 print("%s is done" % currentThread().getName()) 9 10 11 if __name__ == '__main__': 12 t = Thread(target=work) 13 t.start() 14 print(threading.enumerate()) # 连同主线程在内有两个运行的线程 15 print(threading.active_count()) 16 t.join() 17 t.setName("线程1") 18 print(t.isAlive()) 19 print(threading.currentThread().getName()) 20 print(threading.current_thread()) #主线程 21 print(threading.active_count()) 22 print('主线程') 23 24 """ 25 输出: 26 Thread-1 is running 27 [<_MainThread(MainThread, started 24160)>, <Thread(Thread-1, started 24368)>] 28 2 29 Thread-1 is done 30 False 31 MainThread 32 <_MainThread(MainThread, started 24160)> 33 1 34 主线程 35 """
五 守护线程
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
需要强调的是:运行完毕并非终止运行
1、对主进程来说,运行完毕指的是主进程代码运行完毕
2、对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1、主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
2、主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的
代码实列:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread 2 import time 3 def sayhi(name): 4 time.sleep(2) 5 print('%s say hello' %name) 6 7 8 if __name__ == '__main__': 9 t = Thread(target=sayhi, args=('egon',)) 10 # t.setDaemon(True) 必须在t.start()之前设置 11 t.daemon = True 12 t.start() 13 14 print('主线程') 15 print(t.is_alive()) 16 17 """ 18 输出:主线程 19 True 20 """
六 GIL全局解释器锁
定义:在CPython中,全局解释器锁(或GIL)是一个互斥锁,它阻止多个本机线程同时执行Python字节码。之所以需要这个锁,主要是因为CPython的内存管理不是线程安全的。(然而,由于GIL的存在,其他特性已经变得依赖于它所执行的保证。)
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。>有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL
1)、GIL介绍
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。
例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程
验证python test.py只会产生一个进程
#test.py内容 import os,time print(os.getpid()) time.sleep(1000) #打开终端执行 python3 test.py #在windows下查看 tasklist |findstr python #在linux下下查看 ps aux |grep python
在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问
1、所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。
2、所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到
综上:
如果多个线程的target=work,那么执行流程是
多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行
解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码
三 GIL与Lock
机智的同学可能会问到这个问题:Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?
首先,我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据
然后,我们可以得出结论:保护不同的数据就应该加不同的锁。
最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock,如下图分析:
1、100个线程去抢GIL锁,即抢执行权限
2、肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
3、极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
4、直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
代码实列:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,Lock 2 import os,time 3 def work(): 4 global n 5 lock.acquire() 6 temp=n 7 time.sleep(0.1) 8 n=temp-1 9 lock.release() 10 if __name__ == '__main__': 11 lock=Lock() 12 n=100 13 l=[] 14 for i in range(100): 15 p=Thread(target=work) 16 l.append(p) 17 p.start() 18 for p in l: 19 p.join() 20 21 print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全,不加锁则结果可能为99
GIL与多线程
有了GIL的存在,同一时刻同一进程中只有一个线程被执行
听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php才是最牛逼的语言?
别着急啊,老娘还没讲完呢。
要解决这个问题,我们需要在几个点上达成一致:
1、cpu到底是用来做计算的,还是用来做I/O的?
2、多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
3、每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。
如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,
反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高
结论:
1、对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
2、当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
假设我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论:
现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
多线程性能测试
如果并发的多个任务是计算密集型:多进程效率高
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from multiprocessing import Process 2 from threading import Thread 3 import os,time 4 def work(): 5 res=0 6 for i in range(100000000): 7 res*=i 8 9 10 if __name__ == '__main__': 11 l=[] 12 print(os.cpu_count()) #本机为4核 13 start=time.time() 14 for i in range(4): 15 p=Process(target=work) #耗时5s多 16 p=Thread(target=work) #耗时18s多 17 l.append(p) 18 p.start() 19 for p in l: 20 p.join() 21 stop=time.time() 22 print('run time is %s' %(stop-start))
如果并发的多个任务是I/O密集型:多线程效率高
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from multiprocessing import Process 2 from threading import Thread 3 import threading 4 import os,time 5 def work(): 6 time.sleep(2) 7 print('===>') 8 9 if __name__ == '__main__': 10 l=[] 11 print(os.cpu_count()) #本机为4核 12 start=time.time() 13 for i in range(400): 14 # p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上 15 p=Thread(target=work) #耗时2s多 16 l.append(p) 17 p.start() 18 for p in l: 19 p.join() 20 stop=time.time() 21 print('run time is %s' %(stop-start))
应用:
多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析
七 死锁现象与递归锁
死锁现象
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,Lock 2 import time 3 mutexA=Lock() 4 mutexB=Lock() 5 6 class MyThread(Thread): 7 def run(self): 8 self.f1() 9 self.f2() 10 11 def f1(self): 12 mutexA.acquire() 13 print('%s 拿到A锁' % self.name) 14 mutexB.acquire() 15 print('%s 拿到B锁' % self.name) 16 mutexB.release() 17 mutexA.release() 18 19 def f2(self): 20 mutexB.acquire() 21 print('%s 拿到B锁' % self.name) 22 time.sleep(2) 23 mutexA.acquire() 24 print('拿到A锁' % self.name) 25 mutexA.release() 26 mutexB.release() 27 28 29 if __name__ == '__main__': 30 for i in range(10): 31 t=MyThread() 32 t.start() 33 34 """ 35 输出: 36 Thread-1 拿到A锁 37 Thread-1 拿到B锁 38 Thread-1 拿到B锁 39 Thread-2 拿到A锁 40 出现死锁,整个程序阻塞住 41 """
递归锁
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,RLock 2 import time 3 mutexA = mutexB = RLock() 4 5 # 递归锁:可以连续acquire多次,每acquire一次计算器+1,只要计数不为0,就不能被其他线程抢到 6 class MyThread(Thread): 7 def run(self): 8 self.f1() 9 self.f2() 10 11 def f1(self): 12 mutexA.acquire() 13 print('%s 拿到A锁' % self.name) 14 mutexB.acquire() 15 print('%s 拿到B锁' % self.name) 16 mutexB.release() 17 mutexA.release() 18 19 def f2(self): 20 mutexB.acquire() 21 print('%s 拿到B锁' % self.name) 22 time.sleep(2) 23 mutexA.acquire() 24 print('%s 拿到A锁' % self.name) 25 mutexA.release() 26 mutexB.release() 27 28 29 if __name__ == '__main__': 30 for i in range(10): 31 t = MyThread() 32 t.start()
八 信号量、Event、定时器
信号量:也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小
代码实列:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,Semaphore,currentThread 2 import time,random 3 sm = Semaphore(3) 4 def func(): 5 with sm: 6 print("%s in" % currentThread().getName()) 7 time.sleep(random.randint(1, 3)) 8 9 10 if __name__ == '__main__': 11 for i in range(10): 12 t=Thread(target=func) 13 t.start()
Event
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,Event 2 import time 3 event = Event() 4 5 def student(name): 6 print("学生%s 正在听课" % name) 7 event.wait(2) # 可以设置超时时间,即使没有发set信息也会往下走 8 print("学生%s 课间活动" % name) 9 10 def teacher(name): 11 print("老师%s 在上课" % name) 12 time.sleep(4) 13 event.set() 14 15 16 if __name__ == '__main__': 17 stu1 = Thread(target=student, args=("eye1",)) 18 stu2 = Thread(target=student, args=("eye2",)) 19 stu3 = Thread(target=student, args=("eye3",)) 20 tea1 = Thread(target=teacher, args=("Yu hou",)) 21 22 stu1.start() 23 stu2.start() 24 stu3.start() 25 tea1.start()
有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Thread,Event 2 import threading 3 import time,random 4 def conn_mysql(): 5 count=1 6 while not event.is_set(): 7 if count > 3: 8 raise TimeoutError('链接超时') 9 print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count)) 10 event.wait(0.5) 11 count+=1 12 print('<%s>链接成功' %threading.current_thread().getName()) 13 14 15 def check_mysql(): 16 print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName()) 17 time.sleep(random.randint(2,4)) 18 event.set() 19 if __name__ == '__main__': 20 event=Event() 21 conn1=Thread(target=conn_mysql) 22 conn2=Thread(target=conn_mysql) 23 check=Thread(target=check_mysql) 24 25 conn1.start() 26 conn2.start() 27 check.start()
定时器
定时器,指定n秒后执行某操作
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from threading import Timer 2 3 def hello(): 4 print("hello, world") 5 6 t = Timer(1, hello) 7 t.start() # after 1 seconds, "hello, world" will be printed
九 线程queue
当必须在多个线程之间安全地交换信息时,队列在线程编程中特别有用。
class queue.Queue(maxsize=0) #队列:先进先出
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import queue 2 3 q=queue.Queue() 4 q.put('first') 5 q.put('second') 6 q.put('third') 7 8 print(q.get()) 9 print(q.get()) 10 print(q.get()) 11 12 13 14 ''' 15 结果(先进先出): 16 first 17 second 18 third 19 '''
class queue.LifoQueue(maxsize=0) #堆栈:last in fisrt out
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import queue 2 3 q=queue.LifoQueue() 4 q.put('first') 5 q.put('second') 6 q.put('third') 7 8 print(q.get()) 9 print(q.get()) 10 print(q.get()) 11 12 13 14 ''' 15 结果(后进先出): 16 third 17 second 18 first 19 '''
class queue.PriorityQueue(maxsize=0) #优先级队列:存储数据时可设置优先级的队列
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import queue 2 3 q=queue.PriorityQueue() 4 #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 5 q.put((20,'a')) 6 q.put((10,'b')) 7 q.put((30,'c')) 8 9 print(q.get()) 10 print(q.get()) 11 print(q.get()) 12 13 14 15 ''' 16 结果(数字越小优先级越高,优先级高的优先出队): 17 (10, 'b') 18 (20, 'a') 19 (30, 'c') 20 '''
十 进程池与线程池
在刚开始学多进程或多线程时,我们迫不及待地基于多进程或多线程实现并发的套接字通信,然而这种实现方式的致命缺陷是:服务的开启的进程数或线程数都会随着并发的客户端数目地增多而增多,这会对服务端主机带来巨大的压力,甚至于不堪重负而瘫痪,于是我们必须对服务端开启的进程数或线程数加以控制,让机器在一个自己可以承受的范围内运行,这就是进程池或线程池的用途,例如进程池,就是用来存放进程的池子,本质还是基于多进程,只不过是对开启进程的数目加上了限制
介绍
官网:https://docs.python.org/dev/library/concurrent.futures.html
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.
基本方法
1、submit(fn, *args, **kwargs)
异步提交任务
2、map(func, *iterables, timeout=None, chunksize=1)
取代for循环submit的操作
3、shutdown(wait=True)
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前
4、result(timeout=None)
取得结果
5、add_done_callback(fn)
回调函数
进程池
介绍
ProcessPoolExecutor类是一个Executor子类,它使用一个进程池来异步执行调用。ProcessPoolExecutor使用了多处理模块,该模块允许它避开全局解释器锁,但也意味着只能执行和返回可pickle的对象。
类concurrent.futures。ProcessPoolExecutor(max_workers = None,mp_context =没有)
一个Executor子类,使用大多数max_workers进程池异步执行调用。如果max_workers为None或not given,它将默认为机器上的处理器数量。如果max_workers我
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from concurrent.futures import ProcessPoolExecutor 2 import os,time,random 3 def task(name): 4 print('name:%s pid:%s' % (name, os.getpid())) 5 time.sleep(random.randint(1,3)) 6 7 8 if __name__ == '__main__': 9 pool = ProcessPoolExecutor(4) 10 for i in range(10): 11 pool.submit(task,"eye %s" %i) 12 13 pool.shutdown(wait=True) 14 print("主")
线程池
介绍
ThreadPoolExecutor是一个Executor子类,它使用一个线程池来异步执行调用。
类concurrent.futures。ThreadPoolExecutor(max_workers = None,thread_name_prefix = ")
一个Executor子类,它使用最多max_workers线程池异步执行调用。
3.5版本的变化:如果max_workers没有或没有,它将默认为处理器的机器上,乘以5,假设ThreadPoolExecutor通常用于重叠I / O而不是CPU工作和工人的数量应该为ProcessPoo高于工人的数量
map方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor 2 3 import os,time,random 4 def task(n): 5 print('%s is runing' %os.getpid()) 6 time.sleep(random.randint(1,3)) 7 return n**2 8 9 if __name__ == '__main__': 10 11 executor=ThreadPoolExecutor(max_workers=3) 12 13 # for i in range(11): 14 # future=executor.submit(task,i) 15 16 executor.map(task,range(1,12)) #map取代了for+submit
回调函数
可以为进程池或线程池内的每个进程或线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收任务的返回值当作参数,该函数称为回调函数
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #1、同步调用:提交完任务后,就在原地等待任务执行完毕,拿到结果,再执行下一行代码,导致程序是串行执行 2 # 3 from concurrent.futures import ThreadPoolExecutor 4 import time 5 import random 6 7 def la(name): 8 print('%s is laing' %name) 9 time.sleep(random.randint(3,5)) 10 res=random.randint(7,13)*'#' 11 return {'name':name,'res':res} 12 13 def weigh(shit): 14 name=shit['name'] 15 size=len(shit['res']) 16 print('%s 拉了 《%s》kg' %(name,size)) 17 18 19 if __name__ == '__main__': 20 pool=ThreadPoolExecutor(13) 21 22 shit1=pool.submit(la,'alex').result() 23 weigh(shit1) 24 25 shit2=pool.submit(la,'wupeiqi').result() 26 weigh(shit2) 27 28 shit3=pool.submit(la,'yuanhao').result() 29 weigh(shit3)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #2、异步调用:提交完任务后,不地等待任务执行完毕, 2 3 from concurrent.futures import ThreadPoolExecutor 4 import time 5 import random 6 7 def la(name): 8 print('%s is laing' %name) 9 time.sleep(random.randint(3,5)) 10 res=random.randint(7,13)*'#' 11 return {'name':name,'res':res} 12 13 14 def weigh(shit): 15 shit=shit.result() 16 name=shit['name'] 17 size=len(shit['res']) 18 print('%s 拉了 《%s》kg' %(name,size)) 19 20 21 if __name__ == '__main__': 22 pool=ThreadPoolExecutor(13) 23 24 pool.submit(la,'alex').add_done_callback(weigh) 25 26 pool.submit(la,'wupeiqi').add_done_callback(weigh) 27 28 pool.submit(la,'yuanhao').add_done_callback(weigh)
练习题
1、基于多线程实现并发的套接字通信
服务端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import socket 2 from concurrent.futures import ProcessPoolExecutor 3 def talk(conn): 4 while True: 5 try: 6 res = conn.recv(1024) 7 if not res: 8 continue 9 conn.send(res.upper()) 10 except Exception as e: 11 print('\033[1;31m客户端出现错误,关闭连接!---->\033[0m', e) 12 break 13 conn.close() 14 15 def serve(ip, port): 16 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 18 server.bind((ip, port)) 19 server.listen(5) 20 21 while True: 22 conn, client_addr = server.accept() 23 pool.submit(talk, conn) 24 25 26 server.close() 27 28 29 if __name__ == "__main__": 30 pool = ProcessPoolExecutor(2) 31 serve("127.0.0.1", 8080)
客户端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import socket 2 3 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 5 client.connect(("127.0.0.1", 8080)) 6 while True: 7 cmd = input(">>>:").strip() 8 if not cmd:continue 9 client.send(cmd.encode("utf-8")) 10 res = client.recv(1024) 11 print(res.decode("utf-8"))
2、思考下述代码的执行结果有可能是哪些情况?为什么?
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