DIL全局解释锁
一、介绍
-
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) # 99
- 优点
二、使用多线程提高效率
"""
分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
"""
计算密集型:多进程效率高
# 计算密集型:多进程效率高
from multiprocessing import Process
from threading import Thread
import os, time
def work():
res = 0
for i in range(10000000):
res*=i
if __name__ == '__main__':
l = []
print(os.cpu_count())
start_time = time.time()
for i in range(4):
p = Process(target=work) # 消耗时间:1.021416187286377
# p = Thread(target=work) # 消耗时间:2.3506696224212646
l.append(p)
p.start()
for p in l:
p.join()
stop_time = time.time()
print(f'消耗时间:{stop_time - start_time}')
I/O密集型:多线程效率高
# I/O密集型:多线程效率高
from multiprocessing import Process
from threading import Thread
import threading, os, time
def work():
time.sleep(2)
print('--->')
if __name__ == '__main__':
l = []
print(os.cpu_count())
start_time = time.time()
for i in range(400):
# p = Process(target=work) # 消耗时间:15.45025897026062
p = Thread(target=work) # 消耗时间:2.0674052238464355
l.append(p)
p.start()
for p in l:
p.join()
stop_time = time.time()
print(f'消耗时间:{stop_time - start_time}')
- 应用场景
- 多线程用于I/O密集型,如socket、爬虫、web
- 多进程用于计算密集型,如金融分析
三、协程
- 进程:资源单位
- 线程:执行单位
- 协程:单线程下实现并发
在IO密集型的情况下,使用协程能提高最高效率
- 优点
- 应用程序级别速度要远远高于操作系统的切换
- 缺点
- 多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地该线程内的其他任务都不能执行了
一旦引入协程,就需要检测单线程下所有的IO行为,实现遇到IO就切换,少一个都不行,因为一旦一个任务阻塞了,整个线程就阻塞了,其他的任务即便是可以计算,但是也无法运行
-
协程的目的
- 想要在单线程下实现并发
- 并发指的是多个任务看起来是同时运行的
- 并发= 切换+保持状态
- PS:手动实现"遇到IO切换+保存状态"去欺骗操作系统,让操作系统误以为没有IO操作,将CPU的执行权限给你
-
总结协程特点
- 必须在只有一个单线程里实现并发
- 修改共享数据不需要加锁
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其他协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块)
Greenlet模块介绍
如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦(需要先得到初始化一次的生成器,然后再调用send,很麻烦),而使用greenlet模块可以非常简单的实现这20个任务直接的切换
Genvent模块介绍
Genvent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程。Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式额调度
from gevent import monkey # 猴子补丁
monkey.patch_all() # 监听所有的任务是否有IO操作
from gevent import spawn # spawn任务
from gevent import joinall
import time
def task1():
print('start from task1...')
time.sleep(1)
print('end from task1...')
def task2():
print('start from task2...')
time.sleep(3)
print('end from task2...')
def task3():
print('start from task3...')
time.sleep(5)
print('end from task3...')
if __name__ == '__main__':
start_time = time.time()
sp1 = spawn(task1)
sp2 = spawn(task2)
sp3 = spawn(task3)
joinall([sp1, sp2, sp3])
end_time = time.time()
print(f'消耗时间为:{end_time - start_time}')