Python标准库-threading模块
threading是Python标准库中用于多线程编程的模块
1. 简单理解线程
当我们运行一个Python脚本时,它会创建一个新的进程:
1 | python main.py |
这样操作系统就会启动一个Python进程
我们刚启动时就已经有了一个线程,它就是我们的主线程
而一个进程中可以创建多个线程,这就是多线程编程的核心
假设有一个程序存在三个函数:
1 | receive_data() |
这三个函数在一个线程中只能依次执行:
1 | receive_data() |
但如果我们希望这三个函数:
1 | receive_data() # 一直接收数据 |
那么就需要创建三个线程,并在每个线程中执行一个函数
1 | Python进程 |
同进程中的线程会共享同一个内存空间
Python的多线程模块threading提供了一些函数和类,可以帮助我们创建多线程
使用时需要导入:
1 | import threading |
因为threading是Python标准库,所以不需要安装
2. threading.Thread:创建线程
threading.Thread可以创建线程
常用构造形式:
1 | threading.Thread( |
例子,创建一个简单的线程:
1 | import threading |
这里注意,创建线程后,并不会直接执行,需要调用start()方法
target不能写worker(),因为worker()表示立即调用函数
给线程函数传参数:
例如函数:
1 | def worker(name, count): |
则可以:
1 | t = threading.Thread( |
也可以:
1 | t = threading.Thread( |
daemon=True表示这个线程是守护线程,不阻止整个Python进程退出
也就是说,当:
1 | daemon=False |
那么主线程结束,Python通常还要等它
如果:
1 | daemon=True |
那么主线程和其他非守护线程结束后,Python进程可以直接退出
3. threading.start():启动线程
threading有两个容易混淆的方法:
1 | start() |
start()表示创建新线程,并让新线程执行run():
1 | 主线程 |
如果直接:
1 | t.run() |
不会创建新线程,而是直接执行target(...),也就是相当于普通的函数调用
4. threading.join():等线程结束
join()方法等待线程结束,并返回线程的返回值
例子:
1 | import threading |
输出:
1 | 开始 |
也可以:
1 | t.join(timeout=2) |
表示主线程最多阻塞2秒
注意它不会杀死线程,只是主线程不会继续阻塞
5. threading.is_alive():检查线程是否存活
is_alive()方法返回线程是否存活
1 | threading.is_alive() |
返回:
1 | True |
表示线程是否仍然运行
6. threading.current_thread():获取当前线程
current_thread()方法返回当前正在执行代码的线程
例子:
1 | import threading |
输出:
1 | <_MainThread(MainThread, started ...)> |
7. threading.main_thread():获取主线程
main_thread()方法返回主线程对象
判断当前线程是否为主线程:
1 | threading.main_thread() == threading.current_thread() |
如果当前在主线程:
1 | True |
8. threading.enumerate():获取所有线程
enumerate()方法返回一个包含所有线程的列表
例子:
1 | import threading |
输出:
1 | <_MainThread(MainThread, started ...)> |
9. threading.active_count():获取活跃线程数量
active_count()方法返回活跃线程数量
例子:
1 | import threading |
输出:
1 | 3 |
10. 竞态条件
多个线程同时操作共享数据时,程序最终结果取决于线程执行的先后顺序,而线程的执行顺序又是不确定的
当两个线程共同操作一个变量:
1 | count = 0 |
线程1:
1 | count += 1 |
线程2:
1 | count += 1 |
有人可能认为:
1 | 初始 count = 0 |
但是 count+=1 从底层逻辑上看并不是一个不可分割的操作,因此可能会出现下面这种情况:
1 | 初始: |
程序员很难预测线程执行顺序,因此这种Bug往往会导致不可预料的结果,并且非常难排查
11. threading.Lock:互斥锁
threading.Lock是threading模块中的一个类,用于实现互斥锁
互斥锁可以防止多个线程同时访问共享资源,从而避免并发问题
创建互斥锁:
1 | import threading |
核心方法:
1 | acquire() |
例子:
1 | import threading |
输出:
1 | 线程1 开始 |
这样:
1 | Thread 1 |
如果这个时候Thread 2:
1 | lock.acquire() |
则会被阻塞,直到Thread 1释放锁
保证同一时刻只有一个线程能获得锁
Python更推荐使用上下文管理器操作锁:
1 | with lock: |
等同于:
1 | lock.acquire() |
这里的count += 1被称为临界区(Critical Section),也就是访问共享资源,需要保证线程安全的代码区域
12. threading.RLock:可读写锁
创建:
1 | lock = threading.RLock() |
普通的Lock同一个线程第二次获得锁会出现阻塞,把自己卡住:
1 | lock.acquire() |
但是RLock可以多次获得锁
RLock常用于嵌套函数:
1 | import threading |
如果是普通的锁,则这里会出现死锁
13. threading.Event:事件
事件是一个布尔值,只有在事件被设置为True时,才会触发线程
主要方法:
1 | event.set() # Event状态设置为True |
刚创建Event时:
1 | event = threading.Event() |
内部:
1 | False |
执行:
1 | event.set() |
内部:
1 | True |
执行:
1 | event.clear() |
内部:
1 | False |
例子:
1 | import threading |
输出:
1 | worker:等待信号(等待2秒) |
14. threading.Condition:条件变量
条件变量是一个布尔值,只有在条件变量为True时,才会触发线程
作用场景是:一个线程必须等到某个“条件成立”才能继续执行
Condition是一个类,主要方法:
1 | condition.wait() # 等待条件 |
Condition.wait():让当前线程进入等待状态,直到其他线程调用notify()或notify_all()
Condition.notify():唤醒一个正在wait的线程,但唤醒的线程不依赖具体顺序,也无法指定
Condition.notify(n):唤醒n个正在wait的线程
Condition.notify_all():唤醒所有正在wait的线程
Condition内部关联了一个锁,所以也需要上下文管理器with来获取锁
例子:
1 | import threading |
输出:
1 | worker:开始等待(等待2秒) |