python 的并发有什么特性呢?
我们先来看另一个例子
from concurrent import futures
from time import sleep, strftime
import time
def download_resource(file):
st = time.time()
t = 0
for i in range(1000000):
t += i**2
print('one task time is ',time.time()-st)
return file
def run(files):
workers = min(5, len(files))
with futures.ThreadPoolExecutor(workers) as executor:
res = executor.map(download_resource, sorted(files))
return res
ts = time.time()
files = ['{}.png'.format(i) for i in range(5)]
res = run(files)
print('all cost is ', time.time()-ts)
# one task time is 3.5929086208343506
# one task time is 4.961361646652222
# one task time is 5.242505311965942
# one task time is 5.411738872528076
# one task time is 5.5458598136901855
# all cost is 5.8427581787109375
复制代码
这个例子中,并发执行的任务时 CPU 计算型的。执行的结果和我们预期的并不一样,经测试单纯的执行download_resource
需要 1.2s 的时间,预期的 5 个并发执行 5 个任务耗时应该小于 3s 吧。实际的结果却是串行执行的结果(6s)差别不大。
这是为什么?
这就要说道,python 线程的伪并发性;python 的线程在处理 cpu 计算型的任务时其实是单线程执行的。这个原因是 python 的 C 解释器是单线程执行的,在执行多线程任务时,C 解释器都会给线程上一个全局解释锁 GIL(global interpreter lock),使得多个线程轮流使用 cpu 时间。
为什么需要 GIL?单线程的好处是线程安全的,共享的数据不会出现异常的情况。python 中的一切都是对象,python 解释器负责管理这些对象,包括对象的销毁并自动回收内存;python 解释器确切的说 Cpython 解释器会给每一个对象记录一个引用计数,每多一次引用,计数就会+1,反之则-1,如果引用计数为 0,Cpython 解释器会回收这些资源。
在这种机制下,多线程执行时会出现什么情况?两个线程 A 和 B 同时引用一个对象 obj,这个时候 obj 的引用计数为 2;A 打算撤销对 obj 的引用,完成第一步时引用计数减去 1 时,这时发生了线程切换,A 挂起等待,还没判断是否需要执行销毁对象操作。B 进入运行状态,这个时候 B 也对 obj 撤销引用,并完成引用计数减 1,销毁对象,这个时候 obj 的引用数为 0,释放内存。如果此时 A 重新唤醒,单判断 obj 引用计数为 0,开始销毁对象,可是这个时候已经没有对象了。 所以为了保证不出现数据污染,才引入 GIL。
也就是多线程情况下,Cpython 解释器让每一个线程不断的获得 GIL 锁和释放锁,保证每一次只有一个线程在执行。
from concurrent import futures
from time import sleep, strftime
import time
def download_resource(file):
sleep(1)
print(strftime('[%H:%M:%S]'), ' ', 'download done file = {} '.format(file))
return file
def run(files):
workers = min(10, len(files))
with futures.ThreadPoolExecutor(workers) as executor:
res = executor.map(download_resource, sorted(files))
return res
ts = time.time()
files = ['{}.png'.format(i) for i in range(10)]
res = run(files)
print('='*10, 'return information')
for i in res:
print(i)
print(time.time()-ts)
# [23:58:06] download done file = 5.png
# [23:58:06] download done file = 2.png
# [23:58:06] download done file = 4.png
# [23:58:06] download done file = 9.png
# [23:58:06] download done file = 1.png
# [23:58:06] download done file = 3.png
# [23:58:06] download done file = 8.png
# [23:58:06] download done file = 6.png
# [23:58:06] download done file = 7.png
# [23:58:06] download done file = 0.png
# ========== return information
# 0.png
# 1.png
# 2.png
# 3.png
# 4.png
# 5.png
# 6.png
# 7.png
# 8.png
# 9.png
# all cost is 1.0396032333374023
复制代码
看上面例子,确实是并发执行了。这个任务并不是 cpu 计算型的。为什么?
当线程获得 GIL 锁时,Cpython 解释器为线程设置一个 check_interavl,满足条件时线程就会释放锁。条件时 check_interavl 是当前线程遇见 IO 操作或者 ticks 计数达到 100。上面 time.sleep 也可以是 IO 操作,所以 python 比较适合于 IO 密集型的任务(比如爬虫下载网络数据)。
需要真正利用多核 cpu 达到真正的并行,就需要多进程来实现。
评论