当前位置: 首页 > 科技观察

使用多线程和多处理器的Python并发编程

时间:2023-03-14 20:27:04 科技观察

我们在Python编码中经常讨论的一个方面是如何优化模拟执行的性能。虽然在考虑量化代码时NumPy、SciPy和pandas在这方面已经非常有用,但在构建事件驱动系统时我们无法有效地使用这些工具。还有其他方法可以加速我们的代码吗?答案是肯定的,但需要注意!在本文中,我们将研究一种不同的模型——并发,我们可以将其引入我们的Python程序中。该模型在不需要共享状态的模拟中效果特别好。蒙特卡洛模拟器可用于模拟各种参数,例如期权定价和测试算法交易。我们将特别考虑Threading库和Multiprocessing库。Python并发当Python初学者探索多线程代码以进行计算密集型优化时,最常问的问题之一是:“为什么我的程序在使用多线程时会变慢?”在多核机器上,我们希望多线程代码使用额外的内核,从而提高整体性能。不幸的是,主要Python解释器(CPython)的内部并不是真正的多线程,而是通过全局解释器锁(GIL)处理的。GIL是必需的,因为Python解释器不是线程安全的。这意味着当试图从一个线程内安全地访问Python对象时,将会有一个全局强制锁。在任何时候,只有一个线程可以访问Python对象或CAPI。每执行100个字节的Python指令,解释器就会重新获取锁,这(可能)会阻止I/0操作。由于锁,CPU密集型代码在使用线程库时不会获得性能提升,但在使用多处理库时会获得性能提升。并行库的实现现在,我们将使用上面提到的两个库来实现一个“小”问题的并发优化。上面我们提到了线程库:运行CPython解释器的Python不会通过多线程来支持多核处理。不过,Python确实有一个线程库。那么如果我们不能(很可能)使用多核进行处理,那么使用这个库我们能获得什么好处呢?许多程序,尤其是与网络通信或数据输入/输出(I/O)相关的程序,通常会受到网络性能或输入/输出(I/O)性能的限制。这样,Python解释器将等待函数调用的返回,以从“远程”数据源(如网络地址或硬盘)读取和写入数据。因此,这种数据访问比从本地内存或CPU缓冲区读取数据要慢得多。因此,如果以这种方式访问??许多数据源,提高此数据访问性能的一种方法是为需要访问的每个数据项生成一个线程。例如,假设有一段Python代码可以抓取许多站点的URL。进一步假设下载每个URL所花费的时间远远超过计算机的CPU可以处理它的时间,仅使用一个线程的实现将受到输入/输出(I/O)性能的极大限制。通过为每个下载的资源生成一个新线程,此代码将并行下载多个数据源,并在所有下载完成后合并结果。这意味着每个后续下载都不会等待前一个页面完成下载。此时,此代码受到从客户端/服务器接收到的带宽的限制。然而,许多与金融相关的应用程序由于高度集中的数字运算而受到CPU限制。此类应用程序将执行大规模线性代数计算或数值随机统计,例如蒙特卡洛模拟统计。因此,只要您对此类应用程序使用Python和全局解释器锁(GIL),此时使用Python线程库就不会提高性能。Python实现了以下“玩具”代码,将数字依次添加到列表中,说明了多线程的实现。每个线程创建一个新列表并随机添加一些数字到列表中。这个选定的“玩具”示例非常占用CPU。下面的代码概述了线程库的接口,但它不会比我们的单线程实现更快。当我们为以下代码使用多处理库时,我们会看到它显着减少了整体运行时间。让我们检查一下代码是如何工作的。首先我们导入线程库。然后我们创建一个带有三个参数的函数list_append。第一个参数count定义了创建列表的大小。第二个参数id是“作业”的ID(用于我们向控制台输出调试信息)。第三个参数out_list是附加随机数的列表。__main__函数创建大小为107并使用两个线程来执行工作。然后创建一个作业列表来存储分离的线程。threading.Thread对象将list_append函数作为参数并将其附加到作业列表。最后,作业分别启动并分别“加入”。join()方法阻塞调用线程(例如主Python解释器线程)直到线程终止。在将完整消息打印到控制台之前确认所有线程执行已完成。#thread_test.pyimportrandomimportthreadingdeflist_append(count,id,out_list):"""Createsananemptylistandthenappendarandomnumbertothelist'count'numberoftimes.ACPU-heavyoperation!"""foriinrange(count):out_list.append(random.random())if__name__==”__main__":size=10000000#Numberofrandomnumberstoaddthreads=2#Numberofthreadstocreate#Createalistofjobsandtheniteratethrough#thenumberofthreadsappendingeachthreadto#thejoblistjobs=[]foriinrange(0,threads):out_list=list()thread=threading.Thread(tentarget_app,list_list))jobs.append(线程)#Startthethreads(i.e.calculatetherandomnumberlists)forjinjobs:j.start()#Ensureallofthethreadshavefinishedforjinjobs:j.join()打印“列表处理完成。”我们可以在控制台调用如下命令时间这段代码timepythonthread_test.py会产生如下输出:Listprocessingcomplete.real0m2.003suser0m1.838ssys0m0.161s注意user时间和sys时间相加大致等于realtime。这表明我们没有通过使用线程库获得性能提升。我们预计实时会显着减少。并发编程中的这些概念分别称为CPU时间和挂钟时间。Multiprocessinglibrary为了充分利用所有现代处理器都能提供的多核,我们需要使用multiprocessinglibrary。.它的工作方式与线程库完全不同,但两个库的语法非常相似。多处理库实际上为每个并行任务生成多个操作系统进程。通过为每个进程分配一个单独的Python解释器和一个单独的全局解释锁(GIL),非常巧妙地避免了全局解释锁带来的问题。而且,每个进程还可以独立占用一个处理器核,当所有进程都处理完后,再对结果进行重组。但是,也有一些缺点。产生许多进程会产生很多I/O管理问题,因为由多个处理器处理数据会导致数据混乱。这将增加整体运行时间。但是,假设数据被限制在每个进程中,则可以大大提高性能。当然,无论怎么增加,都不会超过阿姆达尔定律规定的极限值。使用Multiprocessing实现的Python实现只需要修改import行和multiprocessing.Process行。这里,参数分别传递给目标函数。除了这些,代码几乎与使用线程的实现相同:#multiproc_test.pyimportrandomimportmultiprocessingdeflist_append(count,id,out_list):"""Createsanemptylistandthenappendarandomnumbertothelist'count'numberoftimes.ACPU-heavyoperation!"""foriinrange(count):out_list.append(random.random())if__name__=="__main__":size=10000000#Numberofrandomnumberstoaddprocs=2#Numberofprocessestocreate#Createalistofjobsandtheniteratethrough#thenumberofprocessesappendingeachprocessto#thejoblistjobs=[]foriinrangelti.procsing():outproclist=essProcess(目标=list_append,args=(size,i,out_list))jobs.append(process)#Starttheprocesses(i.e.calculatetherandomnumberlists)forjinjobs:j.start()#Ensurealloftheprocesseshavefinishedforjinjobs:j.join()print"Listprocessingcomplete."控制台测试运行time:timepythonmultiproc_test.py得到如下输出:Listprocessingcomplete.real0m1.045suser0m1.824ssys0m0.231s在这个例子中可以看到user和sys时间基本一致,而real下降了将近两倍发生这种情况的原因是因为我们使用了两个进程。扩展到四个进程或将列表的长度减半会导致以下结果(假设您的计算机至少有一个四核):Listprocessingcomplete。但是,将此规则推广到更大、更复杂的程序时要小心。数据转换、硬件缓存层次结构和其他问题会降低加速比。在下一篇文章中,我们将并行化Event-DribenBasketer以提高其运行多维参数优化的能力。相关阅读:Python和NumPy中的Cholesky分解EuropeanVanillaCall-PutOptionPricingwithPythonJacobiMethodinPython和NumPyLU分解和NumPyOptionsPricinginPythonQRDecompositionwithPythonandNumPyQuick-StartPythonQuantitativeResearchEnvironmentonUbuntu14.04英文原文:ParallelisingPythonwithThreadingandMultiprocessing翻译链接:http://www.oschina.net/translate/parallelising-python-with-threading-and-multiprocessing