简意介绍
线程 (Thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。同一进程中的多个线程共享进程中的全部系统资源。
Python 中的多线程因为全局解释器锁 GIL 的原因限制同一时刻只能由一个线程运行,无法发挥多核 CPU 的优势【当然这是在默认解释器 CPython 中的缺陷,在 JPython 中没有 GIL 的问题,以下基于 CPython 解释器】。所以:GIL 并不是 python 的特性,python 也不依赖于 GIL。说了那么多,那么 python 中的多线程是不是没用了?
答案当然不是。这取决于线程执行的场景是什么?是做计算(计算密集型)还是输入输出(I/O 密集型), 针对不同的场景使用不同的方法。多核心 CPU 可以有多个核心并行完成计算,所以多核提升的是计算性能,但每个 CPU 一旦遇到 I/O 阻塞,仍然需要等待,所以多核对 I/O 密集型任务没有太大的提升,多线程适合处理 I/0 密集型程序,如文件读写,web 请求,数据库请求等。
线程特点
- 线程执行的顺序是不确定的
- 主线程【进程】会等待所有子线程结束后才会退出,主线程【进程】结束么子线程必然结束
- 线程间共享资源
- 修改资源必要时需要加锁,同时避免死锁
- 占用的资源比进程少
- 线程并不是越多越快
- 由于 GIL 的原因,多线程并不是真正的并发,只是交替执行
线程使用
以下演示使用多线程对一个变量值进行修改,在循环的次数不多时修改后变量的值是符合预期的,当增加循环次数后,变量最终的值并不符合预期。由此可见:线程之间资源是存在竞争的,修改同一份资源必须加互斥锁,同时需要避免死锁。
# coding=utf-8import threading# 定义一个字段。多线程执行+1操作balance = 0def worker1(): global balance for i in range(1000): balance += 1 print('线程1执行完成,balance='+str(balance))def worker2(): global balance for i in range(1000): balance += 1 print('线程2执行完成,balance='+str(balance))def main(): # 构造线程对象 t1 = threading.Thread(target=worker1) t2 = threading.Thread(target=worker2) # 开始执行 t1.start() t2.start() """ 循环次数为1000时,程序输出: 线程1执行完成,balance=1000 线程2执行完成,balance=2000 循环次数为1000000时,程序输出: 线程1执行完成,balance=1180919 线程2执行完成,balance=1179703 """ if __name__ == '__main__': main()
要想解决以下的问题,需要使用线程的锁对象,只需要对 worker1 和 woker2 方法进行修改。
# 创建一个互斥锁,默认是未锁定状态mutex = threading.Lock()def worker1(): global balance for i in range(1000000): mutex.acquire() balance += 1 mutex.release() print('线程1执行完成,balance=' + str(balance))def worker2(): global balance for i in range(1000000): mutex.acquire() balance += 1 mutex.release() print('线程2执行完成,balance=' + str(balance))"""加了互斥锁之后的输出: 线程1执行完成,balance=1941343 线程2执行完成,balance=2000000"""