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

使用开源工具可视化多线程Python程序

时间:2023-03-15 20:56:11 科技观察

VizTracer可以跟踪并发Python程序以帮助记录、调试和分析。并发性是现代编程的重要组成部分,因为我们有多个核心,许多任务需要合作。但是,当并发程序乱序运行时,很难理解。工程师在这些程序中发现错误和性能问题并不像在单线程、单任务程序中那样容易。在Python中,您有多种并发选项。可能最常见的是使用threading模块的多线程,使用subprocess和multiprocessing模块的多处理,以及最近由asyncio模块提供的异步语法。在VizTracer之前,缺乏使用这些技术分析程序的工具。VizTracer是一种用于跟踪和可视化Python程序的工具,可用于记录、调试和分析。虽然它适用于单线程、单任务程序,但它在并发程序中的实用性才是它的独特之处。尝试一个简单的任务从一个简单的练习任务开始:找出数组中的整数是否为质数并返回一个布尔数组。这是一个简单的解决方案:defis_prime(n):foriinrange(2,n):ifn%i==0:returnFalsereturnTruedefget_prime_arr(arr):return[is_prime(elem)foreleminarr]尝试在单线程中正常运行VizTracer:if__name__=="__main__":num_arr=[random.randint(100,10000)for_inrange(6000)]get_prime_arr(num_arr)viztracermy_program.pyRunningcodeinAsinglethreadcallstackreport显示耗时140ms左右,大部分时间花在了get_prime_arr上。调用堆栈报告这只是对数组中的元素反复执行is_prime函数。这是您所期望的,但并不有趣(如果您了解VizTracer)。用多线程程序试试:=线程(目标=get_prime_arr,args=(num_arr,))thread3=线程(target=get_prime_arr,args=(num_arr,))thread1.start()thread2.start()thread3.start()thread1.join()thread2.join()thread3.join()为了匹配单线程程序的工作量,这里对三个线程使用了一个2000个元素的数组,模拟了三个线程分担任务的情况。多线程程序如果你熟悉Python的全局解释器锁(GIL),你会认为它再快不过了。由于开销,它花费了140毫秒多一点。但是,可以观察多线程的并发情况:后来,他们交换了。这是由于GIL,这就是Python没有真正的多线程的原因。它可以实现并发,但不能实现并行。尝试使用多进程来实现并行,方式就是多进程库。这是另一个使用多处理的版本:if__name__=="__main__":num_arr=[random.randint(100,10000)for_inrange(2000)]p1=Process(target=get_prime_arr,args=(num_arr,))p2=过程(target=get_prime_arr,args=(num_arr,))p3=过程(target=get_prime_arr,args=(num_arr,))p1.start()p2.start()p3.start()p1.join()p2.该程序大约快三倍。为了和多线程版本进行对比,这里给出多进程版本:多进程版本在没有GIL的情况下,可以并行实现多个进程,即可以并行执行多个is_prime函数。不过,Python的多线程也不是一无是处。例如,对于计算密集型和I/O密集型程序,您可以使用sleep伪造I/O密集型任务:defio_task():time.sleep(0.01)在单线程、单线程中尝试taskprogram:if__name__=="__main__":for_inrange(3):io_task()I/O-bound单线程,单任务程序整个程序耗时30ms左右,没什么特别的。现在使用多线程:if__name__=="__main__":thread1=Thread(target=io_task)thread2=Thread(target=io_task)thread3=Thread(target=io_task)thread1.start()thread2.start()thread3.start()thread1.join()thread2.join()thread3.join()I/O-bound多线程程序程序耗时10ms。很明显三个线程并发工作,提高了整体性能。试试asyncioPython正在尝试引入另一个有趣的特性,称为异步编程。你可以制作一个异步版本的任务:t3=asyncio.create_task(io_task())awaitt1awaitt2awaitt3if__name__=="__main__":asyncio.run(main())由于asyncio字面意思是一个带任务的单线程调度器,你可以直接在上面使用VizTracer:VizTracerwithasyncio还是用了10ms,但是显示的大部分函数都是底层结构,用户可能不感兴趣。要解决这个问题,请使用--log_async来分隔真正的任务:viztracer--log_asyncmy_program.py使用--log_async来分隔任务现在用户任务更加清晰。大多数时候,没有任务在运行(因为它唯一做的就是睡眠)。有趣的部分在这里:任务创建和执行图这显示了任务创建和执行时间。Task-1是main()协程,它创建其他任务。Task-2、Task-3、Task-4执行io_task并休眠等待唤醒。如图所示,因为是单线程程序,任务之间没有重叠。VizTracer以这种方式可视化,以便于理解。为了让它更有趣,向任务添加一个time.sleep调用以防止异步循环:asyncdefio_task():time.sleep(0.01)awaitasyncio.sleep(0.01)time.sleep调用程序需要更长的时间(40ms),任务填补了异步调度器的空白。此功能对于诊断异步程序的行为和性能问题非常有帮助。看看VizTracer发生了什么?使用VizTracer,您可以在时间线上查看程序的进度,而不是从复杂的日志中想象。这有助于您更好地理解您的并发程序。VizTracer是开源的,在Apache2.0许可下发布,支持所有常见的操作系统(Linux、macOS和Windows)。您可以在VizTracer的GitHub存储库中了解有关其功能的更多信息并访问其源代码。