1.引言
多线程即并发编程,目的是提升计算机处理应用的能力(提效)。python的并发编程稍显特别,因为GIL(全局解释器锁的存在)。GIL本质上是一个互斥锁,同一时间只能有一个线程执行python字节码。这样的设计简化了内存管理,当然也影响了并发的性能。
正因为这样,python还支持了另外一个特性:多进程。多进程不受GIL的影响。那么问题来了?在实际应用中,我们如何衡量选择多线程,还是多进程呢?毕竟都存在,存在即合理,自然有它的原由。
- I/O密集型任务,线程在等待I/O时会释放锁,GIL影响小,适合多线程
- CPU密集型任务,适合多进程
如何区分I/O密集型任务,和CPU密集型任务?
- I/O密集型任务:文件读写任务,网络请求任务
- CPU密集型任务:涉及大量计算的任务
2.多线程案例
2.1.线程入门
python模块threading,方便我们实现多线程编程模型。像这样
python
import threading
import time
# 定义线程任务
def task(num):
print(f"线程{num}启动")
time.sleep(1)
print(f"线程{num}完成")
# 循环创建线程任务,并启动执行
for i in range(3):
t = threading.Thread(target=task, args=(i,))
t.start()
2.2.线程属性
线程thread本身是对象,在实际应用中,我们需要关心相关的一些属性。
python
import threading
import time
# 定义线程任务
def task(num):
print(f"线程{num}启动")
time.sleep(1)
print(f"线程{num}完成")
# 循环创建线程任务,并启动执行
t = threading.Thread(target=task, args=(1,))
# 设置线程名称
t.name = "MyThread"
# 设置线程为守护线程
t.daemon = True
t.start()
# 输出线程相关属性
print(f"线程名称: {t.name}")
print(f"线程状态: {t.is_alive()}")
print(f"线程ID: {t.ident}")
print(f"线程守护状态: {t.daemon}")
2.3.线程方法
我们还需要关心的一些常用线程方法
2.3.1.线程生命周期控制
正确启动线程方式:
python
t = threading.Thread(target=lambda: print("线程已启动"))
t.start() # 正确启动方式
# t.run() # 错误用法!
等待线程结束:
python
import threading
import time
def long_task():
time.sleep(2)
print("任务完成")
t = threading.Thread(target=long_task)
t.daemon = True # 设置为守护线程
t.start()
print("主线程继续执行")
t.join(timeout=1) # 等待1秒
print("等待1秒后继续")
t.join() # 继续等待直到结束
示例效果关键代码行,设置为守护线程,只有守护线程主线程不用等待它的执行完成。可以通过注释最后一行代码观察执行结果。
python
t.join()
2.3.2.守护线程配置
守护线程,等于是后台执行线程,它的特点是主线程不用等待它的执行完成,即可结束。
python
import threading
import time
def daemon_worker():
while True:
print("后台运行中...")
time.sleep(1)
t = threading.Thread(target=daemon_worker)
t.daemon = True # 必须设置在start之前
t.start()
time.sleep(3)
print("主程序退出,守护线程自动终止")
2.3.3.线程中断
线程中断是个很讲究的概念,和处理问题的方式。在并发编程中,我们不建议强制终止线程任务,那样会出现不受控的意外结果。但有时候业务上确实需要停止线程任务继续执行,该如何办?
答案是协商一致,和正在执行任务的线程商量:你可以停下来了吗?由线程自主决定,中断即是与线程协商的方法方式。
python
import threading
import time
# 自定义线程类
class StoppableThread(threading.Thread):
def __init__(self):
super().__init__()
self._stop_event = threading.Event()
def stop(self):
self._stop_event.set()
def stopped(self):
return self._stop_event.is_set()
def run(self):
while not self.stopped():
print("线程运行中...")
time.sleep(0.5)
# 创建并启动线程
t = StoppableThread()
t.start()
time.sleep(2)
# 发送线程中断信号
print("发送线程中断信号...")
t.stop()
# 等待线程结束
t.join()
print("线程已安全停止")
2.4.锁
并发编程中存在共享资源,多线程会争夺共享资源占用,需要有一种机制来保障资源的有序使用,不至于出现错乱现象。这种机制即是锁。
2.4.1.无锁案例
案例说明:
- 1.共享资源变量num=100,我们开100个线程,每个线程将资源变量num 减 1
- 2.预期通过100个线程执行完后,最后num=0.
- 3.由于线程用资源时,没有加锁,可能会出现某几个线程拿到的资源一样
- 4.比如当num=50时,有两个线程同时拿到num=50这个资源,就会出现100个线程都减一后,最后num不等于0,而是其它数值
python
import threading
import time
#定义任务方法add_num
def add_num():
global num
temp = num
# 模拟任务耗时
time.sleep(0.001)
num = temp-1
print(f'线程:{threading.current_thread().name}执行.OK,拿到值为:{temp},减一后,num为:{num}')
# 设定一个共享变量,模拟共享资源,如后续的数据库
num = 100
# 线程组
thread_list = []
#循环开启100个线程执行任务
for i in range(100):
t = threading.Thread(target=add_num)
t.start()
thread_list.append(t)
# 主线程等待所有线程都执行完
for i in thread_list:
i.join()
#输出最终结果
print('Final num:',num)
2.4.2.加锁案例
python中threading模块提供了Lock接口,来看加锁以后的效果
python
import threading
import time
#创建一把锁
r = threading.Lock()
#定义任务方法add_num
def add_num():
global num
# 加锁
r.acquire()
temp = num
# 模拟任务耗时
time.sleep(0.001)
num = temp-1
print(f'线程:{threading.current_thread().name}执行.OK,拿到值为:{temp},减一后,num为:{num}')
# 释放锁
r.release()
# 设定一个共享变量,模拟共享资源,如后续的数据库
num = 100
# 线程组
thread_list = []
#循环开启100个线程执行任务
for i in range(100):
t = threading.Thread(target=add_num)
t.start()
thread_list.append(t)
# 主线程等待所有线程都执行完
for i in thread_list:
i.join()
#输出最终结果
print('Final num:',num)
2.5.线程池
虽然python线程底层实现不是基于内核线程,而是基于用户态空间实现的轻量级线程,但毕竟频繁创建线程对象,释放线程对象,资源利用率不高。从资源复用的角度考虑,池化是一个不错的选择方案。
通过线程池改造上面的案例:
python
import threading
import time
import concurrent.futures
# 创建一个线程池,最大线程数为5
executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
#创建一把锁
r = threading.Lock()
#定义任务方法add_num
def add_num():
global num
# 加锁
r.acquire()
temp = num
# 模拟任务耗时
time.sleep(0.001)
num = temp-1
print(f'线程:{threading.current_thread().name}执行.OK,拿到值为:{temp},减一后,num为:{num}')
# 释放锁
r.release()
# 设定一个共享变量,模拟共享资源,如后续的数据库
num = 100
#循环开启100个线程执行任务
for i in range(100):
future = executor.submit(add_num)
#关闭线程池
executor.shutdown()
#输出最终结果
print('Final num:',num)