不过最近有读者问:Python的多线程到底是真是假?一下子就来到了Python这个被人混了很久的特性——GIL。究竟发生了什么?让我们今天谈谈。完美我们知道Python灵活而强大,因为它是一种边解释边执行的解释型语言。实现此功能的标准实现称为CPython。它分两步运行Python程序:首先解析源代码文本,将其编译成字节码(bytecode)[1],然后使用基于堆栈的解释器运行字节码,并不断循环这个过程,直到程序结束or被终止的灵活性是有的,但是为了保证程序执行的稳定性,也付出了巨大的代价:引入全局解释器锁GIL(globalinterpreterlock)[2]来保证只有一个字节码被执行同时运行,这样就不会因为没有提前编译而导致资源争用和状态混乱的问题。看似“完美”,但这样做意味着在多线程执行时,会被GIL变成单线程,硬件资源无法得到充分利用。看代码:importtimedefgcd(pair):'''求解最大公约数'''a,b=pairlow=min(a,b)foriinrange(low,0,-1):ifa%I==0andb%i==0:ReturnIassertfalse,"notreachable"#tobesolveddatanumbers=[(1963309,2265973),(5948475,2734765),(1876435,4765849),(7654637、7654637、3458496)。(1823712,1924928),(2387454,5873948),(1239876,2987473),(3487248,2098437),(3498747,4563758),(1298737,2129874)]list(map(gcd,NUMBERStime))end=time.()delta=end-startprint(f'sequentialexecutiontime:{delta:.3f}seconds')函数gcd用于求解最大公约数,模拟一个数据操作NUMBERS使用map方法求解数据为solved,传入处理函数gcd,和待求解的数据,会返回一个result数组,最后转换成list计算耗时执行过程,在笔者电脑上打印出来(4核,16G)执行时间为2.043秒。如何切换到多线程?...fromconcurrent.futuresimportThreadPoolExecutor...##多线程解决方案start=time.time()pool=ThreadPoolExecutor(max_workers=4)results=list(pool.map(gcd,NUMBERS))end=time.time()delta=end-startprint(f'Executiontime:{delta:.3f}seconds')这里引入了concurrent.futures模块中的线程池。使用线程池更方便,将线程池设置为4,主要是为了匹配CPU的核心数,线程池pool提供了多线程版本的map,所以参数不变看运行效果:顺序执行时间:2.045秒并发执行时间:2.070秒什么?并行执行时间更长!连续执行多次,结果是一样的,也就是说在GIL的限制下,多线程是无效的,更多的时间因为线程调度而浪费了。在枷锁中跳舞Python中的多线程真的没用吗?事实上,它不是。虽然因为GIL的原因,无法实现真正??意义上的多线程,但是多线程机制还是为我们提供了两个重要的特性。一:如何理解多线程编写可以让某些程序更容易编写?如果要解决一个需要同时保持多个状态的程序,用单线程实现是非常困难的。例如,如果要检索文本文件中的数据,为了提高检索效率,可以将文件分成小段进行处理,如果先找到该段中的数据,则处理过程结束。单线程很难同时兼顾多个段,只能顺序或二分法执行检索任务。使用多线程,可以将每个段交给每个线程,依次执行,相当于同时推荐检索任务。在处理的时候,相比顺序查找,效率会大大提高。二:更高效的处理阻塞式I/O任务。阻塞I/O是指当系统需要与文件系统进行交互时(包括网络和终端显示),文件系统的处理速度远低于CPU。更多,所以程序会被设置为阻塞状态,即不再分配计算资源。直到文件系统的结果返回后才会被激活,才有机会重新分配计算资源。也就是说,处于阻塞状态的程序将永远等待下去。所以如果一个程序需要不断的从文件系统中读取数据,处理完再写入,如果是单线程的话,需要先等读再处理,再等处理再写,这样处理过程就变成了一个一个.等待。有了多线程,当一个处理进程被阻塞时,会立即被GIL切断,将计算资源分配给其他可执行进程,从而提高执行效率。有了这两个特性,就说明Python的多线程并不是一无是处。如果能根据情况写,效率会大大提高。但是,对于计算密集型任务,多线程功能就无能为力了。曲线救国那么有没有一种方法可以真正的使用计算资源而不受GIL的束缚呢?当然有,而且不止一个。先介绍一个简单易用的方法。回头看之前计算最大公约数的程序,我们用了线程池来处理,但是没用,比不用还不如。这是因为这个程序是计算密集型的,对CPU的依赖很大,显然会受到GIL的束缚。现在我们稍微修改一下程序:...fromconcurrent.futuresimportProcessPoolExecutor...##并行旅行解决方案start=time.time()pool=ProcessPoolExecutor(max_workers=4)results=list(pool.map(gcd,NUMBERS))end=time.time()delta=end-startprint(f'并行执行时间:{delta:.3f}秒')看效果:顺序执行时间:2.018秒并发执行时间:2.032秒并行执行时间:0.789秒并行执行提高了近3倍!什么情况?仔细看,主要是将多线程中的ThreadPoolExecutor替换为ProcessPoolExecutor,也就是进程池执行器。同一个进程中的Python程序会受到GIL的限制,但不同进程之间不会,因为每个进程中的GIL都是独立的。是不是很神奇?在这里,得益于concurrent.futures模块,封装了实现进程池的复杂性,留给我们一个简洁优雅的接口。这里需要注意的是ProcessPoolExecutor并不是万能的。比较适用于数据相关性不高的场景和计算密集的场景。如果数据相关性强,就会出现进程间的“通信”,可能会把来之不易的性能提升付诸东流。进程池还有什么用?那就是:用C语言重写需要提高性能的部分不要感到奇怪。Python已经为C扩展保留了API。但这样做需要付出更多的代价。为此,可以使用SWIG[3]、CLIF[4]等工具将python代码转为C语言,有兴趣的读者可以研究一下。自我提升了解Python多线程的问题及解决方法。对于我们这些热爱Python的人来说,我们应该去哪里呢?有一句话用在这里很贴切:与其求己不如求人。再强大的工具、武器也不能解决所有问题,而问题之所以能够解决,主要还是靠我们的主观能动性。分析判断情况,选择合适的解决方案,不正是我们需要做的吗?对于Python中对多线程的批评,我们看到的更多的是它阳光美好的一面,对于需要提高速度的地方采取适当的方法。这里简单总结一下:对于I/O密集型任务,使用Python的多线程是完全没有用的,而且可以大大提高执行效率。对于计算密集型任务,取决于数据依赖性是否低。如果低,使用ProcessPoolExecutor代替多线程处理可以充分利用硬件资源。如果数据依赖度高,可以考虑用C来实现关键部分。一方面,C本身比Python更快。另一方面,C可以使用更底层的多线程机制,而且你不用担心会受到GIL的影响。在大多数情况下,对于只能由多线程处理的任务,你不需要考虑太多。就用Python的多线程机制吧。不要想太多。总结是没有用的。完美的解决方案,如果有的话,也只能是在某些特定的条件下,就像在软件工程中一样,没有灵丹妙药。面对现实世界,我们只能靠自己。我们在学习中学到更多,在实践中感受更多,在总结回顾中收获更多,在思考反思中解决更多。这是人类不断发展的原动力。为了我们更美好的明天,为了人类更美好的明天,加油!以上就是本次分享的全部内容。觉得文章还不错的话,请关注公众号:Python编程学习圈,每日干货分享,发送“J”还能领取大量学习资料。或者去编程学习网了解更多编程技术知识。
