Python多线程

Python多线程随手记

内容来自于Python并发编程实战

有哪些程序提速的方法?

单线程串行

普通脚本就是单线程串行。

多线程并发

CPU和IO同时工作,对应Threading。CPU不用等带IO。

多CPU并行(多进程)

由于当前PC中处理器都包含多核心,利用多个核心,多进程并行执行任务,对应MultiProcessing。利用多核CPU的能力,真正的并行执行任务。

多机器并行

大数据时间通常使用多个PC进行任务,对应Hadoop/Hive/Spark等。

Python对并发编程的支持

  • 多线程、多进程;
  • asycio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行;
  • Lock可以对资源加锁,防止冲突访问;
  • 使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式;
  • 使用线程池Pool、进程池Pool,简化线程、进程的任务提交、等待结束、获取结果;
  • 使用subprocess启动外部程序的进程,并进行输入输出交互;

Python并发编程的三种方式

线程、进程、协程之间有层级关系。一个进程中可以包含和启动很多个线程,一个线程可以启动很多个协程。

预备知识

CPU密集型计算(CPU-Bound)

CPU-Bound指在执行任务过程中会受到CPU的限制,也叫做计算密集型,I/O在很短的时间就可以完成,CPU需要大量的计算和处理,特点就是CPU占用率相当高。
比如:压缩解压、加密解密、正则表达式搜索。

IO密集型计算(I/O-Bound)

I/O-Bound指在执行任务过程中会受到I/O的限制,系统运作大部分的状况时CPU在等I/O(硬盘/内存)的读/写操作,CPU占用率较低。
比如:文件处理程序、网络爬虫程序、读写数据库程序。

多线程Thread

多进程通常通过threading库实现,使用于I/O密集型计算、同时运行的任务数目要求不多。

  • 优点:
    相比进程,更轻量、占用资源更少。
  • 缺点:
    相比进程:多线程只能并发执行,不能利用多CPU(GIL);
    相比协程:启动数目有限制,占用内存资源,有线程切换开销。

多进程Process

多进程通常通过multiprocessing库实现,适用于CPU密集型计算。

  • 优点:
    可以利用多核CPU并行计算
  • 缺点:
    占用资源最多、可启动数目受限于处理器核心数量,比线程要少。

多协程Coroutine

多协程通常通过asyncio库实现,使用I/O密集型计算、需要超多任务运行、有现成库支持的场景。

  • 优点:
    内存开销最少、启动数量最多。
  • 缺点:
    支持库有限制,很多库都不支持协程技术(requests VS aiohttp)、代码实现复杂。
    requests库就不支持协程,如果需要使用到多协程,只能使用aiohttp库。

全局解释器锁GIL

Python速度慢的原因

导致Python速度慢的原因有两个:

  1. 执行过程边解释边执行;
    比如C++程序在编写完成后会先编译成机器码,机器执行机器码的速度非常快,但Python执行的就是源码,需要边翻译成机器码,边执行。
  2. Python是动态类型语言
    Python中的变量可以是任意类型,可以随意的从“数字”切换到“字符串”,这导致了在执行过程中Python需要随时检查变量的类型,从而影响程序执行速度。
  3. GIL
    由于GIL的存在导致Python无法利用多核CPU并发执行程序。

GIL是什么

全局解释器锁(Global Interpreter Lock,GIL),是计算机程序涉及语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用GIL的解释器也只允许同一时间执行一个线程。

GIL存在理由

Python设计初期,为了规避并发问题引入GIL,后来想去除去不掉了。
GIL存在的目的是为了解决多线程之间数据完整性和状态同步的问题。Python中对象的管理,是使用引用计数器进行的,引用数量为0则释放对象。
举个栗子:
假设存在线程A和线程B都引用对象obj,obj.ref_num=2,线程A和线程B都想撤销对obj的引用。
线程A先撤销引用,obj.ref_num=1,此时发生多线程调度切换,切换到线程B。线程B撤销obj的引用,obj.ref_num=0,又由于Python通过引用数管理对象,obj.ref_num=0后内存中就删除了变量obj的相关内容,此时发生多线程调度切换,切换回A时可能就会对其他应用程序造成影响。

如何避免GIL带来的限制

  1. threading机制依然可用
    因为在I/O期间,线程会释放GIL,实现CPU和IO的并行,因此多线程用于IO密集型计算依然可以大幅度提升速度,但多线程用于CPU密集型计算时,只会拖慢速度。
  2. 使用multiprocessing实现并行计算
    为应对GIL的问题,Python提供了multiprocessing模块,用于多计算机制实现真正的并行计算。