计算机有两种常见的任务类型
- 计算密集型,时间多用在I/O操作上,比如文件读写、网络请求、数据库查询
- I/O密集型,时间多用在计算上,如数值计算、图像处理、排序、搜索
由于Python存在GIL(全局解释器锁),同一时间只有一个线程可以执行Python字节码,使得在计算密集型任务中无法充分利用多核CPU,因此,Python的多线程一般用于I/O密集型任务。
注:上述Python指Python的官方实现CPython,一些其他的实现如PyPy、Jython都没有GIL
使用Thread类创建线程
from time import sleep, perf_counter
def task():
print("任务开始...")
sleep(1)
print("任务结束...")
if __name__ == "__main__":
start = perf_counter()
task()
task()
end = perf_counter()
print(f"任务耗时:{end - start: 0.2f}s")
如果我们有两个耗时1s的任务,全部放在主线程(main thread)执行
任务开始...
任务结束...
任务开始...
任务结束...
任务耗时: 2.00s
结果是大约是2s,我们可以使用多线程优化程序
from time import sleep, perf_counter
from threading import Thread
def task(task_id: int):
print(f"任务 {task_id}开始...")
sleep(1)
print(f"任务 {task_id}结束...")
if __name__ == "__main__":
threads = []
start = perf_counter()
for i in range(1, 6):
t = Thread(target=task, args=(i, ))
threads.append(t)
t.start()
for t in threads:
t.join()
end = perf_counter()
print(f"任务耗时{end - start: 0.2f}s ")
使用Thread构造一个线程,target是目标函数(注意不要写成函数调用),args是一个tuple,tuple中的元素会依次传递给目标函数作为参数
start启动线程,join阻塞调用它的线程(这里是主线程)直到子线程任务结束。
任务 1开始...
任务 2开始...
任务 3开始...
任务 4开始...
任务 5开始...
任务 1结束...
任务 2结束...
任务 4结束...
任务 3结束...
任务 5结束...
任务耗时 1.00s
继承Thread类
有时我们需要自定义线程行为,或者线程逻辑较为复杂,可以继承Thread类封装自己的线程。
import urllib.request
import urllib.error
from threading import Thread
class HttpRequestThread(Thread):
def __init__(self, url: str):
super().__init__()
self.url = url
def run(self):
print(f"checking {self.url}")
try:
response = urllib.request.urlopen(self.url)
print(response.code)
except urllib.error.HTTPError as e:
print(e.code)
except urllib.error.URLError as e:
print(e.reason)
if __name__ == "__main__":
urls = [
'http://httpstat.us/200',
'http://httpstat.us/400'
]
threads = [HttpRequestThread(url) for url in urls]
for thread in threads:
thread.start() # 这里要调用start而不是run,否则不会开启新的线程
for thread in threads:
thread.join()
继承Thread类,重写init方法添加需要的属性。重写run方法,定义线程的行为。
注意,启动线程仍要调用start,不要直接调用run方法。
从线程中返回数据
要从线程中返回数据,可以扩展线程类定义属性来返回值
from threading import Thread
import urllib.request
import urllib.error
class HttpRequestThread(Thread):
def __init__(self, url: str):
super().__init__()
self.url = url
self.http_stat_code = None
self.reason = None
def run(self):
print(f"checking {self.url}")
try:
response = urllib.request.urlopen(self.url)
self.http_stat_code = response.code
except urllib.error.HTTPError as e:
self.http_stat_code = e.code
except urllib.error.URLError as e:
self.reason = e.reason
if __name__ == "__main__":
urls = [
'http://httpstat.us/200',
'http://httpstat.us/400'
]
threads = [HttpRequestThread(url) for url in urls]
[t.start() for t in threads]
[t.join() for t in threads]
[print(f"{t.url}: {t.http_stat_code}") for t in threads]
守护线程
守护线程是在后台执行的线程,不会影响程序的退出。用于定时任务、日志、python垃圾回收
的循环引用检查等。
from threading import Thread
import time
def show_timer():
count = 0
while True:
time.sleep(1)
count += 1
print(f"已执行{count}秒")
if __name__ == "__main__":
# thread = Thread(target=show_timer) # 非守护线程
thread = Thread(target=show_timer)
thread.start()
answer = input("按任意键退出程序\n")
print("Done")
线程不是守护线程,main thread结束后,线程并未结束,程序不会终止
from threading import Thread
import time
def show_timer():
count = 0
while True:
time.sleep(1)
count += 1
print(f"已执行{count}秒")
if __name__ == "__main__":
# thread = Thread(target=show_timer) # 非守护线程
thread = Thread(target=show_timer, daemon=True)
thread.start()
answer = input("按任意键退出程序\n")
print("Done")
创建线程时daemon=True设置为守护线程,此时main thread结束后,无论子线程是否结束,程序终止。
线程池
线程池是用于管理和复用线程的机制。预先创建一些线程,避免频繁创建和销毁线程的开销
Executor是线程池(ThreadPoolExecutor)和进程池(ProcessPoolExecutor)的基类
Executor.submit为线程池分配任务, 返回一个future对象。
from concurrent.futures import ThreadPoolExecutor, Future
from time import sleep, perf_counter
def task(task_id: int):
print(f"任务{task_id}开始...")
sleep(1)
return f"任务{task_id}结束"
if __name__ == "__main__":
start = perf_counter()
with ThreadPoolExecutor() as executor:
f1: Future = executor.submit(task, 1)
f2: Future = executor.submit(task, 2)
print(f1.result())
print(f2.result())
end = perf_counter()
print(f"程序耗时{end - start: 0.2f}秒")
结果
任务1开始...
任务2开始...
任务1结束
任务2结束
程序耗时 1.00秒
Process finished with exit code 0
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。