写点什么

python 小知识 - 并发编程(2)

作者:AIWeker
  • 2022-11-07
    福建
  • 本文字数:1833 字

    阅读完需:约 6 分钟

python小知识-并发编程(2)

python 的并发有什么特性呢?

我们先来看另一个例子


from concurrent import futuresfrom time import sleep, strftimeimport 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 futuresfrom time import sleep, strftimeimport 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 达到真正的并行,就需要多进程来实现。

发布于: 刚刚阅读数: 2
用户头像

AIWeker

关注

InfoQ签约作者 / 公众号:人工智能微客 2019-11-21 加入

人工智能微客(aiweker)长期跟踪和分享人工智能前沿技术、应用、领域知识,不定期的发布相关产品和应用,欢迎关注和转发

评论

发布
暂无评论
python小知识-并发编程(2)_Python_AIWeker_InfoQ写作社区