一.GIL锁
什么是GIL? 全局解释器锁,是加在解释器上的互斥锁
GC是python自带的内存管理机制,GC的工作原理:python中的内存管理使用的是应用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成为了垃圾数据,当内存占用达到某个阈值,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行.
为什么需要GIL?
由于CPython的内存管理机制是非线程安全,于是CPython就给解释器加了一个锁,解决了安全问题,但是降低了效率,另外,虽然有解决方案,但是由于牵涉太多,一旦修改则很多基于GIL的程序都需要修改,所以变成了历史遗留问题.
GIL加锁,解锁的时机?
加锁时机:在调用解释器时立即加锁
解锁时机:①当前线程遇到IO时释放 ②当前线程执行时间超过设定值时释放,解释器会检测线程的执行时间,一旦到达某个阈值,通知线程保存状态切换线程.
GIL带来的问题:即使是多核处理器下也无法真正的并行.
总结:
①在单核情况下,无论是IO密集型还是计算密集型,GIL都不会产生影响,而多线程开销小,并且节约资源,所以使用多线程.
②在多核情况下,IO密集型会受到GIL的影响,但是很明显IO速度远比计算速度慢,所以两者执行的时间差不多,基本可以忽略不计,而在这个情况下我们考虑到多线程开销小,并且节约资源,所以多核情况下,IO密集型我们使用多线程.
③对于计算密集型,在多核情况下,CPython中多线程是无法并行的,为了解决这一弊端,Python推出了多进程技术,可以良好的利用多核处理器来完成计算的任务.
多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析
多进程与多线程效率对比:
1现在的电脑都是多核系统 2#多进程解决计算密集型 3from multiprocessing import Process 4import time 5a = 10 6def task(): 7 for i in range(10000000): 8 global a 9 a +=1 10 a*10/2-3 11if __name__ == '__main__': 12 start = time.time() 13 ps = [] 14 for i in range(3): 15 p = Process(target=task) 16 p.start() 17 ps.append(p) 18 for p in ps: 19 p.join() 20 print(time.time()-start) 21结果:5.455920934677124 22 23#多线程解决计算密集型 24from threading import Thread 25import time 26a = 10 27def task(): 28 for i in range(10000000): 29 global a 30 a +=1 31 a*10/2-3 32if __name__ == '__main__': 33 start = time.time() 34 ts = [] 35 for i in range(3): 36 t = Thread(target=task) 37 t.start() 38 ts.append(t) 39 for t in ts: 40 t.join() 41 print(time.time()-start) 42结果:8.375339031219482 43 44#多进程解决IO密集型 45from multiprocessing import Process 46import time 47def task(): 48 path =r'E:\python试学视频\day27、28选课系统\11 测试程序2.mp4' 49 with open(path,mode='rb') as f: 50 while True: 51 data = f.read(1024) 52 if not data: 53 break 54if __name__ == '__main__': 55 start = time.time() 56 ps = [] 57 for i in range(3): 58 p = Process(target=task) 59 p.start() 60 ps.append(p) 61 for p in ps: 62 p.join() 63 print(time.time()-start) 64结果:0.3124856948852539 65#多线程解决IO密集型 66from threading import Thread 67import time 68a = 10 69def task(): 70 path =r'E:\python试学视频\day27、28选课系统\11 测试程序2.mp4' 71 with open(path,mode='rb') as f: 72 while True: 73 data = f.read(1024) 74 if not data: 75 break 76if __name__ == '__main__': 77 start = time.time() 78 ts = [] 79 for i in range(3): 80 t = Thread(target=task) 81 t.start() 82 ts.append(t) 83 for t in ts: 84 t.join() 85 print(time.time()-start) 86结果:0.1250016689300537
二.GIL锁与自定义锁的区别
GIL是用于保护解释器相关的数据,解释器也是一段程序,肯定有其定义的各种数据
GIL并不能保证自己定义的数据的安全,所以当程序中出现多线程共享数据的时候就需要自定义加锁.
三.线程池与进程池
什么是进程池/线程池?
池表示是一个容器,本质就是一个存储进程或线程的列表
IO密集型使用线程池,计算密集型使用进程池
为什么需要线程池/进程池?
很多情况下需要控制进程或者线程在一个合理的范围内,线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配
线程池的使用:
1from concurrent.futures import ThreadPoolExecutor 2from threading import current_thread,active_count 3import time 4#创建线程池,指定最大线程数为3 如果不指定 默认为cpu核心数*5 5pool = ThreadPoolExecutor(3) #不会立即开启子线程 6def task(): 7 print('%s running..'%current_thread().name) 8 print(active_count()) 9 time.sleep(2) 10#提交任务到线程池 11for i in range(10): 12 pool.submit(task)
进程池的使用:
1from concurrent.futures import ProcessPoolExecutor 2import time,os 3#创建进程池,最大进程数为3,默认为cpu个数 4pool = ProcessPoolExecutor(3)#不会立即开启子进程 5def task(): 6 print('%s running..'%os.getpid()) 7 time.sleep(2) 8if __name__ == '__main__': 9 #提交任务到进程池 10 for i in range(10): 11 pool.submit(task) 12 #第一次提交任务时会创建进程后续提交任务直接交给已经存在的进程来完成,如果没有空闲进程就等待 13结果: 141464 running.. 1511732 running.. 168236 running.. 17 181464 running.. 1911732 running.. 208236 running.. 21 221464 running.. 2311732 running.. 248236 running.. 25 261464 running..
案例:TCP中的应用
首先要明确,TCP是IO密集型,应该使用线程池
1#多线程TCP服务器 2from concurrent.futures import ThreadPoolExecutor 3import socket 4server = socket.socket() 5server.bind(('192.168.12.207',4396)) 6server.listen() 7pool = ThreadPoolExecutor(3) #线程池,控制可以连接到服务器的客户端的个数 8def task(client): 9 while True: 10 try: 11 data = client.recv(1024) 12 if not data: 13 client.close() 14 break 15 client.send(data.upper()) 16 except ConnectionResetError: 17 client.close() 18 break 19while True: 20 client,addr = server.accept() 21 t = pool.submit(task,client) 22 23#多线程TCP客户端 24#使用多线程是为了可以一直输入,不用等输出了才可以输入 25from threading import Thread 26import socket 27client = socket.socket() 28client.connect(('192.168.12.207',4396)) 29def send_msg(): 30 while True: 31 msg = input('>>:').strip() 32 if not msg: 33 continue 34 client.send(msg.encode('utf-8')) 35send_t = Thread(target=send_msg) 36send_t.start() 37while True: 38 try: #这个也要自定义抛出异常,如果服务器终止,客户端也会报错 39 data = client.recv(1024) 40 print(data.decode('utf-8')) 41 except: 42 client.close() 43 break
与信号量的区别:
信号量也是一种锁,适用于保证同一时间能有多少个进程或线程访问
而线程池和进程池,没有对数据访问进行限制仅仅是控制数量
四.同步与异步
同步(调用/执行/任务/提交),发起任务后必须等待任务结束,拿到一个结果才能继续运行
异步 发起任务后不需要关系任务的执行过程,可以继续往下运行,但还是需要结果
异步效率高于同步但是并不是所有任务都可以异步执行,判断一个任务是否可以异步的条件是,任务发起方是否立即需要执行结果
同步不等于阻塞 异步不等于非阻塞当使用异步方式发起任务时 任务中可能包含io操作 异步也可能阻塞同步提交任务 也会卡主程序 但是不等同阻塞,因为任务中可能在做一些计算任务,CPU没有切换到其他程序
1from concurrent.futures import ThreadPoolExecutor 2import time 3pool = ThreadPoolExecutor() 4def task(): 5 time.sleep(1) 6 print('sub thread run...') 7for i in range(10): 8 pool.submit(task) #submit是以异步的方式提交任务 9print('over') 10 11from concurrent.futures import ThreadPoolExecutor 12import time 13pool = ThreadPoolExecutor() 14def task(i): 15 time.sleep(1) 16 print('sub thread run ...') 17 i += 1 18 return i 19for i in range(10): 20 f = pool.submit(task,i) 21 print(f) 22 print(f.result()) #result是阻塞的,会等到这个任务执行完毕才能继续执行,会将异步变为同步 23print('over') 24 25#同步又变为了异步 26from concurrent.futures import ThreadPoolExecutor 27import time 28pool = ThreadPoolExecutor() 29def task(i): 30 time.sleep(1) 31 print('sub thread run ...') 32 i += 1 33 return i 34fs = [] 35for i in range(10): 36 f = pool.submit(task,i) 37 fs.append(f) 38#是一个阻塞函数,会等到池子中的所有任务完成后继续执行 39pool.shutdown() #里面有一个wait参数:默认值是True 40#注意:shutdown之后就不能提交新任务了 41for i in fs: 42 print(i.result()) 43print('over')