十八、并发编程之多线程
十八、并发编程之多线程
一、什么是线程
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程就是一个进程
车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线
流水线的工作需要电源,电源就相当于CPU
所以进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位
多线程(即多个控制线程)的概念,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间内的资源
二、为何要用多线程
多线程指的是在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程,详细的讲分为4点:
1.多线程共享一个进程的地址空间
2.线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量需要动态和快速修改时,这一特性很有用
3.若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度
4.在多CPU系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
三、线程与进程的区别
1.创建进程的开销要远大于线程
2.进程之间是竞争的关系,线程之间是协作关系
3.进程之间的内存空间物理隔离,线程之间的内存空间共享
4.进程只能对子进程进行控制,线程可以对同一进程的线程进行相当大的控制
5.对父进程的更改不会影响子进程,对主线程的更改(取消,优先级更改等)可能会影响进程中其他的线程
四、开启线程的两种方式
1 #方式一 2 from threading import Thread 3 4 def task(): 5 for i in range(1,5): 6 print('子线程干了第%s次活'%i) 7 8 9 for i in range(10): 10 t=Thread(target=task,name='线程%s'%i) 11 print(t.name) 12 t.start()
1 #方式二 2 from threading import Thread 3 4 class MYthread(Thread): 5 def __init__(self,name): 6 super().__init__() 7 self.name=name 8 9 def run(self): 10 for i in range(1, 5): 11 print('子线程干了第%s次活' % i) 12 13 for i in range(10): 14 t=MYthread('线程%s'%i) 15 print(t.name) 16 t.start()
五、死锁问题
1、所谓死锁:是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程或线程称之为死锁进程或线程
死锁发生的条件:有多个线程多个锁,如果只有一个锁,无论是Lock,还是Rlock都不会死锁(前提是程序逻辑正确)
2、递归锁:Rlock就算你的代码逻辑不对,同一个线程多次对一个锁执行acquire也不会死锁
1 #例如抢盘子,叉子吃饭 2 3 from threading import Thread,Lock,current_thread,RLock 4 import time 5 # 叉子 6 locka = RLock() 7 # 盘子 8 lockb = RLock() 9 10 11 12 def task1(): 13 print(current_thread()) 14 locka.acquire() 15 print("抢到叉子 需要盘子") 16 time.sleep(0.1) 17 lockb.acquire() 18 print("吃饭") 19 20 lockb.release() 21 locka.release() 22 23 def task2(): 24 print(current_thread()) 25 lockb.acquire() 26 print("抢到盘子 需要叉子") 27 time.sleep(0.1) 28 locka.acquire() 29 30 print("吃饭") 31 locka.release() 32 lockb.release() 33 34 35 t1 = Thread(target=task1) 36 t1.start() 37 t2 = Thread(target=task2) 38 t2.start()
六、信号量
1、semaphore管理一个内置的计数器,每当调用acquire时内置计数器+1,调用release时内置计数器-1
计数器不能小于0,当计数器为0时,acquire将阻塞线程直到其他线程调用release
1 from threading import Thread,Semaphore,current_thread,active_count 2 3 import time 4 # 用于控制 同时执行被锁定代码的线程数量 也就是线程的并发数量 5 # 也是一种锁,控制同时进入锁内代码的线程数量 6 sm = Semaphore(1) 7 8 def task(): 9 sm.acquire() 10 for i in range(10): 11 print(current_thread()) 12 time.sleep(0.5) 13 sm.release() 14 15 def task2(): 16 for i in range(10): 17 print(current_thread()) 18 time.sleep(0.5) 19 20 21 for i in range(5): 22 Thread(target=task).start() 23 Thread(target=task2).start() 24 print(active_count())
七、多进程与多线程之间的开启时间对比
1 from threading import Thread 2 from multiprocessing import Process 3 import time 4 import os 5 6 def work(): 7 time.sleep(2) 8 print('====>') 9 10 if __name__ == '__main__': 11 lis=[] 12 print(os.cpu_count())#此笔记本电脑为8核 13 start_time=time.time() 14 for i in range(500): 15 # p=Process(target=work) #耗时57秒 16 p=Thread(target=work)#耗时2秒 17 lis.append(p) 18 p.start() 19 for p in lis: 20 p.join() 21 print('run time is %s'%(time.time()-start_time))
八、GIL
1、什么是GIL:
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据的安全
可以肯定的一点是:保护不同的数据安全,就因该加不同的锁
要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程,例如python test.py , python aaa.py , python bbb.py会产生3个不同的python进程
1 ''' 2 #验证python test.py只会产生一个进程 3 #test.py内容 4 import os,time 5 print(os.getpid()) 6 time.sleep(1000) 7 ''' 8 python3 test.py 9 #在windows下 10 tasklist |findstr python 11 12 验证python test.py只会产生一个进程
在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内
1 #1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码) 2 例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。 3 4 #2 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
九、同步、异步和阻塞、非阻塞
进程或线程的三种状态:
1.就绪
2.运行
3.阻塞
阻塞:遇到了IO操作,代码卡主无法执行下一行,CPU会切换到其他任务
非阻塞:与阻塞相反,代码正在执行(运行状态)或处于就绪状态
阻塞和非阻塞描述的是运行的状态
同步:提交任务必须等任务完成,才能执行下一行
异步:提交任务不需要等待任务完成,立即执行下一行
指的是一种提交任务的方式
1 #所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。 2 #举例: 3 #1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束 4 #2. concurrent.futures.ProcessPoolExecutor().submit(func,).result() 5 #3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()
1 #异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。 2 #举例: 3 #1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。 4 #2. concurrent.futures.ProcessPoolExecutor(3).submit(func,) 5 #3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)
1 #阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 2 #举例: 3 #1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态); 4 #2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。
1 #非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
总结:
1 #1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。 2 3 #2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程
十、协程
1、什么是协程:
协程:是单线程下的并发,又称微线程,纤程。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的
2、为什么要用协程:
python的线程属于内核级别的,即由操作系统控制调度,如单线程遇到IO或执行时间过长就会被迫交出CPU的执行权限,切换其他线程运行
单线程内开启协程,一旦遇到IO阻塞,就会从应用程序级别控制切换,以此来提升效率
3、对比操作系统控制线程的切换,用户在单线程内控制协程的切换:
优点:
1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2.单线程内就可以实现并发的效果,最大限度地利用CPU
缺点;
1.协程的本质是单线程下的,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
4、总结特点;
1.必须在只有一个单线程里实现并发
2.修改共享数据不需加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到IO操作自动切换到其他协程,需要用到gevent模块
1 from gevent import monkey 2 #用来检测整个代码中的IO操作,如有就切换 3 monkey.patch_all() 4 5 import gevent 6 import time 7 def eat(): 8 print('eat food 1') 9 time.sleep(2) 10 print('eat food 2') 11 12 def play(): 13 print('play 1') 14 time.sleep(1) 15 print('play 2') 16 17 g1=gevent.spawn(eat) 18 g2=gevent.spawn(play) 19 gevent.joinall([g1,g2]) 20 print('主')
十一、socketserver
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环
socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
基于tcp的socketserver我们自己定义的类中
1.self.server即套接字对象
2.self.request即一个链接
3.self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中
1.self.request是一个元组(第一个元素是客户端发来的数据,第二个元素是服务端的udp套接字对象)
2.self.client_address即客户端的地址
1 #tcp服务端 2 from threading import current_thread 3 import socketserver 4 5 class MyServer(socketserver.BaseRequestHandler): 6 def handle(self): 7 print(self.request) 8 print(self.client_address) 9 print(self.server) 10 11 while True: 12 try: 13 data=self.request.recv(1024) 14 print(data.decode('utf-8')) 15 self.request.sendall(data.upper()) 16 print(current_thread()) 17 except ConnectionResetError: 18 print('客户端异常关闭') 19 self.request.close() 20 break 21 22 if __name__ == '__main__': 23 server=socketserver.ThreadingTCPServer(('127.0.0.1',10000),MyServer) 24 server.serve_forever()
1 #udp服务端 2 import socketserver 3 from threading import current_thread 4 5 class MyserverUDP(socketserver.BaseRequestHandler): 6 def handle(self): 7 print(self.request) 8 print(self.server) 9 print(self.client_address) 10 data,server=self.request 11 print(data.decode('utf-8')) 12 server.sendto(data.upper(),self.client_address) 13 print(current_thread()) 14 15 if __name__ == '__main__': 16 server=socketserver.ThreadingUDPServer(('127.0.0.1',10001),MyserverUDP) 17 server.serve_forever()