GIL全局解释锁、协程
一、GIL全局解释锁
1、GIL
GIL本质就是一把互斥锁,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
保证不同的数据,需要加上不同的锁。
GIL保护的是解释器级别的数据,保护用户自己的数据则需要自己加锁处理。
import time from threading import Thread n = 100 def task(): global n m = n time.sleep(3) n = m - 1 if __name__ == '__main__': list1 = [] for line in range(10): t = Thread(target=task) t.start() list1.append(t) for t in list1: t.join() print(n)
2、GIL与多线程
#分析: 我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是: 方案一:开启四个进程 方案二:一个进程下,开启四个线程 #单核情况下,分析结果: 如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜 如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜 #多核情况下,分析结果: 如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜 如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜 #结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
3、多线程性能测试
计算密集型:多进程效率高
from multiprocessing import Process from threading import Thread import os,time def work(): res=0 for i in range(100000000): res*=i if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(4): p=Process(target=work) #耗时5s多 p=Thread(target=work) #耗时18s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
I/O密集型:多线程效率高
from multiprocessing import Process from threading import Thread import threading import os,time def work(): time.sleep(2) print('===>') if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(400): # p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上 p=Thread(target=work) #耗时2s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
应用:
多线程: 用于I/O密集型,如socket,爬虫,web
多进程: 用于计算密集型,如金融分析
二、协程
1、协程
是单线程下的并发,又称微线程,纤程,英文名Coroutine。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
强调:
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行) #2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换:
#优点: #1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级 #2. 单线程内就可以实现并发的效果,最大限度地利用cpu #缺点: #1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程 #2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
''' 1、协程: 单线程实现并发 在应用程序里控制多个任务的切换+保存状态 优点: 应用程序级别速度要远远高于操作系统的切换 缺点: 多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地 该线程内的其他的任务都不能执行了 一旦引入协程,就需要检测单线程下所有的IO行为, 实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了, 其他的任务即便是可以计算,但是也无法运行了 2、协程序的目的: 想要在单线程下实现并发 并发指的是多个任务看起来是同时运行的 并发=切换+保存状态 ''' #串行执行 import time def func1(): for i in range(10000000): i+1 def func2(): for i in range(10000000): i+1 start = time.time() func1() func2() stop = time.time() print(stop - start) #基于yield并发执行 import time def func1(): while True: yield def func2(): g=func1() for i in range(10000000): i+1 next(g) start=time.time() func2() stop=time.time() print(stop-start)
2、协程特点
1、必须在只有一个单线程里实现并发
2、修改共享数据不许加锁
3、用户程序里自己保存多个控制流的上下文化栈
4、附加:一个协程遇到IO操作自动切换到其他协程(通过gevent模块实现检测IO)
from threading import Thread import time from gevent import joinall, spawn, monkey;monkey.patch_all() # 猴子补丁 spawn(任务) #monkey.patch_all() # 监听所有的任务是否有IO操作 # monkey.patch_all() #这一步是为了让串行变成并发 def task1(): print('start1...') time.sleep(1) print('end1...') def task2(): print('start2...') time.sleep(3) print('end2...') def task3(): print('start3...') time.sleep(5) print('end3...') if __name__ == '__main__': start_time = time.time() sp1 = spawn(task1) sp2 = spawn(task2) sp3 = spawn(task3) sp1.start() sp2.start() sp3.start() sp1.join() sp2.join() sp3.join() #sp1.start()...sp3.join() == joinall([sp1, sp2, sp3]) joinall([sp1, sp2, sp3]) end_time = time.time() print(f'消耗时间为:{end_time - start_time}')
3、练习
TCP并发协程服务端
import socket from gevent import joinall, spawn, monkey;monkey.patch_all() # sv = socket.socket() # sv.bind(('127.0.0.1', 9010)) # sv.listen(5) def server(): while True: sv = socket.socket() sv.bind(('127.0.0.1', 9010)) sv.listen(5) conn, addr = sv.accept() spawn(run, conn) def run(conn): while True: try: data = conn.recv(1024) print(data.decode('utf-8')) except Exception as e: print(e) break conn.close() if __name__ == '__main__': obj = spawn(server) # obj.join() joinall([obj])
import socket cl = socket.socket() cl.connect(('127.0.0.1', 9010)) while True: for i in range(10): data = input('>>>:').strip() cl.send(data.encode('utf-8'))