在Python中创建线程主要依靠内置的threading模块。
threading.current_thread():获取到当前线程。
获取线程后可以得到两个比较重要的属性:name和ident,分别是线程名称和id。
创建线程可以使用两种方法:使用函数或类创建。
使用函数创建线程时,使用threading.Thread()函数,把线程里要执行的函数传进去。
import os
import time
import threading
def fun(n):
start = time.time()
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始运行...' % my_thread_name)
time.sleep(n)
my_thread_id = threading.current_thread().ident # 获取当前线程id
print('当前线程为:{},线程id为:{},所在进程为:{}'.format(my_thread_name, my_thread_id, os.getpid()))
print('%s线程运行结束,耗时%ds...' % (my_thread_name, time.time() - start))
t1 = time.time()
# 创建3个线程
for i in range(1, 4):
t = threading.Thread(target=fun, name='线程%s' % i, args=(i,))
t.start()
main_thread_name = threading.current_thread().name # 获取当前线程名称
main_thread_id = threading.current_thread().ident # 获取当前线程id
print('主线程为:{},线程id为:{},所在进程为:{}'.format(main_thread_name, main_thread_id, os.getpid()))
print("一共耗时%ds" % (time.time() - t1))
# 线程1开始运行...
# 线程2开始运行...
# 线程3开始运行...
# 主线程为:MainThread,线程id为:8637730304,所在进程为:19493
# 一共耗时0s
# 当前线程为:线程1,线程id为:13005955072,所在进程为:19493
# 线程1线程运行结束,耗时1s...
# 当前线程为:线程2,线程id为:13022744576,所在进程为:19493
# 线程2线程运行结束,耗时2s...
# 当前线程为:线程3,线程id为:13039534080,所在进程为:19493
# 线程3线程运行结束,耗时3s...
可以看到,我们开了三个子线程,分别执行1s,2s,3s,但是为什么我们一共耗时是0秒呢。因为创建子线程后,主线程的代码还在继续向后执行,可以看到当主线程结束后,子线程还在继续执行。后面将学习如何让主线程等待子线程。
使用类创建线程需要继承Thread,并实现run方法。
import os
import time
import threading
class MyThread(threading.Thread):
def __init__(self, n, name=None):
super().__init__()
self.name = name
self.n = n
def run(self):
start = time.time()
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始运行...' % my_thread_name)
time.sleep(self.n)
my_thread_id = threading.current_thread().ident # 获取当前线程id
print('当前线程为:{},线程id为:{},所在进程为:{}'.format(my_thread_name, my_thread_id, os.getpid()))
print('%s线程运行结束,耗时%ds...' % (my_thread_name, time.time() - start))
t1 = time.time()
# 创建3个线程
for i in range(1, 4):
t = MyThread(name='线程%d' % i, n=i)
t.start()
main_thread_name = threading.current_thread().name # 获取当前线程名称
main_thread_id = threading.current_thread().ident # 获取当前线程id
print('主线程为:{},线程id为:{},所在进程为:{}'.format(main_thread_name, main_thread_id, os.getpid()))
print("一共耗时%ds" % (time.time() - t1))
# 线程1开始运行...
# 线程2开始运行...
# 线程3开始运行...
# 主线程为:MainThread,线程id为:8678927872,所在进程为:20587
# 一共耗时0s
# 当前线程为:线程1,线程id为:13089185792,所在进程为:20587
# 线程1线程运行结束,耗时1s...
# 当前线程为:线程2,线程id为:13105975296,所在进程为:20587
# 线程2线程运行结束,耗时2s...
# 当前线程为:线程3,线程id为:13122764800,所在进程为:20587
# 线程3线程运行结束,耗时3s...
Thread类有一个名为deamon的属性,标志该线程是否为守护线程,默认值为False。
当deamon值为True,即设为守护线程后,只要主线程结束了,无论子线程代码是否结束,都得跟着结束。
修改deamon的值必须在线程start()方法调用之前,否则会报错。
上面的例子可以看到,主线程结束后,子线程还在运行,直到子线程结束,下面我们使用守护线程:
import os
import time
import threading
def fun(n):
start = time.time()
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始运行...' % my_thread_name)
time.sleep(n)
my_thread_id = threading.current_thread().ident # 获取当前线程id
print('当前线程为:{},线程id为:{},所在进程为:{}'.format(my_thread_name, my_thread_id, os.getpid()))
print('%s线程运行结束,耗时%ds...' % (my_thread_name, time.time() - start))
t1 = time.time()
for i in range(1, 4):
t = threading.Thread(target=fun, name='线程%s' % i, args=(i,))
t.daemon = True
t.start()
main_thread_name = threading.current_thread().name # 获取当前线程名称
main_thread_id = threading.current_thread().ident # 获取当前线程id
# 等待1秒,让线程1结束
time.sleep(1)
print('主线程为:{},线程id为:{},所在进程为:{}'.format(main_thread_name, main_thread_id, os.getpid()))
print("一共耗时%ds" % (time.time() - t1))
# 线程1开始运行...
# 线程2开始运行...
# 线程3开始运行...
# 当前线程为:线程1,线程id为:13072445440,所在进程为:21178
# 线程1线程运行结束,耗时1s...
# 主线程为:MainThread,线程id为:8670285312,所在进程为:21178
# 一共耗时1s
可以看到,让主线程阻塞1s,只有线程1运行结束了,线程2和线程3还没运行结束就随着主线程结束而结束了。
设置子线程join后,主线程会阻塞等待子进程完成,再执行join后面的代码
import os
import time
import threading
def fun(n):
start = time.time()
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始运行...' % my_thread_name)
time.sleep(n)
my_thread_id = threading.current_thread().ident # 获取当前线程id
print('当前线程为:{},线程id为:{},所在进程为:{}'.format(my_thread_name, my_thread_id, os.getpid()))
print('%s线程运行结束,耗时%ds...' % (my_thread_name, time.time() - start))
t1 = time.time()
t_list = []
for i in range(1, 4):
t = threading.Thread(target=fun, name='线程%s' % i, args=(i,))
t.daemon = True
t.start()
t_list.append(t)
for t in t_list:
t.join()
main_thread_name = threading.current_thread().name # 获取当前线程名称
main_thread_id = threading.current_thread().ident # 获取当前线程id
time.sleep(1)
print('主线程为:{},线程id为:{},所在进程为:{}'.format(main_thread_name, main_thread_id, os.getpid()))
print("一共耗时%ds" % (time.time() - t1))
# 线程1开始运行...
# 线程2开始运行...
# 线程3开始运行...
# 当前线程为:线程1,线程id为:12953964544,所在进程为:23305
# 线程1线程运行结束,耗时1s...
# 当前线程为:线程2,线程id为:12970754048,所在进程为:23305
# 线程2线程运行结束,耗时2s...
# 当前线程为:线程3,线程id为:12987543552,所在进程为:23305
# 线程3线程运行结束,耗时3s...
# 主线程为:MainThread,线程id为:8610835968,所在进程为:23305
# 一共耗时4s
注意join要在start之后,且不要和start在一个循环里,因为主线程遇到join就会阻塞,这样第一个循环创建线程1并start和join后,主线程开始阻塞,等线程1执行完毕才会下一个循环,才开始创建线程2,这样达不到并发的效果了。
从上面的结果可以看出,设置子线程join后,主线程就会等待子线程运行完毕,才开始执行join后面的代码,这样一共耗时4s。
多线程一个很大的问题是数据不安全,因为线程之间的数据是共享的。
以银行转账为例,一个银行的总额是大家一同影响的,创建多个线程(表示多个人进行取钱)共同使用一个变量,如下:
import random
import time
import threading
money = 1000
def fun(n):
global money
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始取钱...' % my_thread_name)
num = money # 开始查询银行有多少钱
num -= n # 取出钱
time.sleep(random.random())
money = num # 计算取出后银行还要多少钱
print('%s取了%d,还剩%d' % (my_thread_name, n, money))
t_list = []
for i in range(1, 4):
t = threading.Thread(target=fun, name='线程%s' % i, args=(100,))
t.daemon = True
t.start()
t_list.append(t)
for t in t_list:
t.join()
print("全部取钱结束,银行还剩%d" % money)
# 线程1开始取钱...
# 线程2开始取钱...
# 线程3开始取钱...
# 线程3取了100,还剩900
# 线程2取了100,还剩900
# 线程1取了100,还剩900
# 全部取钱结束,银行还剩900
可以看到,是那个人同时取钱时,读取的银行都是1000块,但是三个人取完后还是900,这就是因为数据共享的原因。
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源设置一个状态:锁定和非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
import random
import time
import threading
money = 1000
def fun(n, lock):
lock.acquire()
global money
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始取钱...' % my_thread_name)
num = money # 开始查询银行有多少钱
num -= n # 取出钱
time.sleep(random.random())
money = num # 计算取出后银行还要多少钱
print('%s取了%d,还剩%d' % (my_thread_name, n, money))
lock.release()
t_list = []
lock = threading.Lock()
for i in range(1, 4):
t = threading.Thread(target=fun, name='线程%s' % i, args=(100, lock))
t.daemon = True
t.start()
t_list.append(t)
for t in t_list:
t.join()
print("全部取钱结束,银行还剩%d" % money)
# 线程1开始取钱...
# 线程1取了100,还剩900
# 线程2开始取钱...
# 线程2取了100,还剩800
# 线程3开始取钱...
# 线程3取了100,还剩700
# 全部取钱结束,银行还剩700
使用了锁之后,代码运行速度明显降低,这是因为线程由原来的并发执行变成了串行,不过数据安全性得到保证。
还可以使用with lock这种上下文格式,自动管理上锁和释放锁。
import random
import time
import threading
money = 1000
def fun(n, lock):
with lock:
global money
my_thread_name = threading.current_thread().name # 获取当前线程名称
print('%s开始取钱...' % my_thread_name)
num = money # 开始查询银行有多少钱
num -= n # 取出钱
time.sleep(random.random())
money = num # 计算取出后银行还要多少钱
print('%s取了%d,还剩%d' % (my_thread_name, n, money))
t_list = []
lock = threading.Lock()
for i in range(1, 4):
t = threading.Thread(target=fun, name='线程%s' % i, args=(100, lock))
t.daemon = True
t.start()
t_list.append(t)
for t in t_list:
t.join()
print("全部取钱结束,银行还剩%d" % money)
# 线程1开始取钱...
# 线程1取了100,还剩900
# 线程2开始取钱...
# 线程2取了100,还剩800
# 线程3开始取钱...
# 线程3取了100,还剩700
# 全部取钱结束,银行还剩700
用Lock的时候必须注意是否会陷入死锁,所谓死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
以科学家吃面为例,爱因斯坦和记录夫人一起吃面,需要叉子和面条同时拥有才能吃面,但是各只有一个人可以使用,只有一个人同时获得面条和叉子才可以吃面。
import time
from threading import Thread, Lock
def eat_noodle1(name, noodle_lock, fork_lock):
noodle_lock.acquire()
print(f"{name} get noodle")
time.sleep(1)
fork_lock.acquire()
print(f"{name} get fork")
print(f"{name} start eat noodle")
fork_lock.release()
print(f"{name} put down noodle")
noodle_lock.release()
print(f"{name} put down noodle")
def eat_noodle2(name, noodle_lock, fork_lock):
fork_lock.acquire()
print(f"{name} get fork")
time.sleep(1)
noodle_lock.acquire()
print(f"{name} get noodle")
print(f"{name} start eat noodle")
noodle_lock.release()
print(f"{name} put down noodle")
fork_lock.release()
print(f"{name} put down noodle")
t_list = []
name_list = ["Einstein", "Curie"]
noodle_lock = Lock()
fork_lock = Lock()
Einstein = Thread(target=eat_noodle1, name="Einstein", args=("Einstein", noodle_lock, fork_lock))
t_list.append(Einstein)
Curie = Thread(target=eat_noodle2, name="Curie", args=("Curie", noodle_lock, fork_lock))
t_list.append(Curie)
for t in t_list:
t.start()
# Einstein get noodle
# Curie get fork
运行程序则程序死锁,因为Einstein拿到面条锁住,等待叉子,Curie拿到叉子锁住,等待面条,两个都在等对方释放锁,但是都在阻塞,造成死锁。
还有一种情况,在同一线程里,多次取获得锁,第一次获取锁后,还未释放,再次获得锁
import time
from threading import Thread, Lock
def eat_noodle1(name, lock):
lock.acquire()
print(f"{name} get noodle")
lock.acquire()
print(f"{name} get fork")
print(f"{name} start eat noodle")
lock.release()
print(f"{name} put down noodle")
lock.release()
print(f"{name} put down noodle")
t_list = []
name_list = ["Einstein", "Curie"]
lock = Lock()
for name in name_list:
Einstein = Thread(target=eat_noodle1, name=name, args=(name, lock))
t_list.append(Einstein)
for t in t_list:
t.start()
# Einstein get noodle
为了解决Lock死锁的情况,就有了递归锁:RLock。
所谓的递归锁也被称为“锁中锁”,指一个线程可以多次申请同一把锁,但是不会造成死锁,这就可以用来解决上面的死锁问题。
import time
from threading import Thread, RLock
def eat_noodle1(name, lock):
lock.acquire()
print(f"{name} get noodle")
lock.acquire()
print(f"{name} get fork")
print(f"{name} start eat noodle")
lock.release()
print(f"{name} put down noodle")
lock.release()
print(f"{name} put down noodle")
t_list = []
name_list = ["Einstein", "Curie"]
lock = RLock()
for name in name_list:
Einstein = Thread(target=eat_noodle1, name=name, args=(name, lock))
t_list.append(Einstein)
for t in t_list:
t.start()
# Einstein get noodle
# Einstein get fork
# Einstein start eat noodle
# Einstein put down noodle
# Einstein put down noodle
# Curie get noodle
# Curie get fork
# Curie start eat noodle
# Curie put down noodle
# Curie put down noodle
RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
Condition可以认为是一把比Lock和RLOK更加高级的锁,其在内部维护一个琐对象(默认是RLock),可以在创建Condigtion对象的时候把琐对象作为参数传入。Condition也提供了acquire, release方法,其含义与琐的acquire, release方法一致,其实它只是简单的调用内部琐对象的对应的方法而已。Condition内部常用方法如下:
使用condition实现一个生产者消费者模式
import threading
import time
# 生产者
def produce(con):
# 锁定线程
global num
con.acquire()
print("工厂开始生产……")
while True:
num += 1
print("已生产商品数量:{}".format(num))
time.sleep(1)
if num >= 5:
print("商品数量达到5件,仓库饱满,停止生产……")
con.notify() # 唤醒消费者
con.wait() # 生产者自身陷入沉睡
# 释放锁
con.release()
# 消费者
def consumer(con):
con.acquire()
global num
print("消费者开始消费……")
while True:
num -= 1
print("剩余商品数量:{}".format(num))
time.sleep(2)
if num <= 0:
print("库存为0,通知工厂开始生产……")
con.notify() # 唤醒生产者线程
con.wait() # 消费者自身陷入沉睡
con.release()
con = threading.Condition()
num = 0
p = threading.Thread(target=produce, args=(con,))
c = threading.Thread(target=consumer, args=(con,))
p.start()
c.start()
# 工厂开始生产……
# 已生产商品数量:1
# 已生产商品数量:2
# 已生产商品数量:3
# 已生产商品数量:4
# 已生产商品数量:5
# 商品数量达到5件,仓库饱满,停止生产……
# 消费者开始消费……
# 剩余商品数量:4
# 剩余商品数量:3
# 剩余商品数量:2
# 剩余商品数量:1
# 剩余商品数量:0
# 库存为0,通知工厂开始生产……
# 已生产商品数量:1
# 已生产商品数量:2
# 已生产商品数量:3
# 已生产商品数量:4
# 已生产商品数量:5
# 商品数量达到5件,仓库饱满,停止生产……
信号量。semaphore是python中的一个内置的计数器,内部使用了Condition对象,在程序中调用acquire()时,内置计数器-1,调用release()时,内置计数器+1。 计数器不能小于0,小于0初始化报错,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
使用场景:主要用在控制程序运行的线程数,防止密集CPU、IO、内存过高。
以银行取钱为例,加入只有3个窗口,则值允许同时3个人取钱,其他人必须排队等待窗口闲置:
from threading import Thread, Semaphore
import time
import random
MONEY = 1000
def fun(i, sem):
global MONEY
sem.acquire()
print('{}号到窗口开始取钱'.format(i))
time.sleep(random.random())
MONEY -= 100
print('{}号取完钱离开窗口'.format(i))
sem.release()
if __name__ == '__main__':
sem = Semaphore(3)
t_list = []
for i in range(1, 11):
t = Thread(target=fun, args=(i, sem))
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f"银行还剩钱:{MONEY}")
# 1号到窗口开始取钱
# 2号到窗口开始取钱
# 3号到窗口开始取钱
# 2号取完钱离开窗口
# 4号到窗口开始取钱
# 1号取完钱离开窗口
# 5号到窗口开始取钱
# 3号取完钱离开窗口
# 6号到窗口开始取钱
# 4号取完钱离开窗口
# 7号到窗口开始取钱
# 5号取完钱离开窗口
# 8号到窗口开始取钱
# 6号取完钱离开窗口
# 9号到窗口开始取钱
# 8号取完钱离开窗口
# 10号到窗口开始取钱
# 7号取完钱离开窗口
# 9号取完钱离开窗口
# 10号取完钱离开窗口
# 银行还剩钱:0
注意,虽然允许3个线程同时运行,但是因为Semaphore使用了Condition,线程之间仍然有锁保证线程数据安全,所以银行钱数可以正常计算。
事件。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时候就可以用threading为我们提供的Event对象。
事件处理的机制:全局定义了一个内置标志Flag,如果Flag值为 False,那么当程序执行 event.wait方法时就会阻塞,如果Flag值为True,那么event.wait 方法时便不再阻塞。
Event其实就是一个简化版的 Condition。Event没有锁,无法使线程进入同步阻塞状态。
方法:
我们使用红路灯为例,过马路都要经过红绿灯, 行人过马路和交通指示灯是两个不同的对象或者处理单元。我们可以把行人和红绿灯抽象为两个独立的线程,行人在绿灯的情况下通过马路,红灯时必须等待。红绿灯可以当做两个线程的通信机制event。
from threading import Thread, Event
import time, random
def now():
return str(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
def traffic_light(e): # 红绿灯
print(now() + ' \033[31m红灯亮\033[0m') # Flag 默认是False
while True:
if e.is_set(): # 如果是绿灯
time.sleep(2) # 2秒后
print(now() + ' \033[31m红灯亮\033[0m') # 转为红灯
e.clear() # 设置为False
else: # 如果是红灯
time.sleep(2) # 2秒后
print(now() + ' \033[32m绿灯亮\033[0m') # 转为绿灯
e.set() # 设置为True
def people(e, i):
if not e.is_set():
print(now() + ' people %s 在等待' % i)
e.wait()
print(now() +' people %s 通过了' % i)
if __name__ == '__main__':
e = Event() # 默认为 False,红灯亮
p = Thread(target=traffic_light, args=(e,)) # 红绿灯进程
p.daemon = True
p.start()
process_list = []
for i in range(1, 7): # 6人过马路
time.sleep(random.randrange(0, 4, 2))
p = Thread(target=people, args=(e, i))
p.start()
process_list.append(p)
for p in process_list:
p.join()
# 2021-12-04 13:59:07 红灯亮
# 2021-12-04 13:59:09 绿灯亮
# 2021-12-04 13:59:09 people 1 通过了
# 2021-12-04 13:59:11 红灯亮
# 2021-12-04 13:59:11 people 2 在等待
# 2021-12-04 13:59:11 people 3 在等待
# 2021-12-04 13:59:11 people 4 在等待
# 2021-12-04 13:59:13 people 5 在等待
# 2021-12-04 13:59:13 绿灯亮
# 2021-12-04 13:59:13 people 2 通过了
# 2021-12-04 13:59:13 people 4 通过了
# 2021-12-04 13:59:13 people 3 通过了
# 2021-12-04 13:59:13 people 5 通过了
# 2021-12-04 13:59:15 people 6 通过了
queue模块实现了各种消费者-生产者模型队列。可用于在执行的多个线程之间安全的交换信息。
queue具有3中不同的队列:
常用方法:
多线程的Queue是在queue模块中
from threading import Thread
from queue import Queue
import random, time
def getter(name, queue):
while True:
try:
time.sleep(random.random())
value = queue.get(True, 10)
print("Process getter get: %f" % value)
except Exception as e:
print(e)
break
def putter(name, queue):
for i in range(1, 11):
time.sleep(random.random())
queue.put(i)
print("Process putter put: %f" % i)
if __name__ == '__main__':
# set_start_method('fork')
queue = Queue()
getter_process = Thread(target=getter, args=("Getter", queue))
putter_process = Thread(target=putter, args=("Putter", queue))
getter_process.start()
putter_process.start()
# Process putter put: 1.000000
# Process getter get: 1.000000
# Process putter put: 2.000000
# Process getter get: 2.000000
# Process putter put: 3.000000
# Process getter get: 3.000000
# Process putter put: 4.000000
# Process getter get: 4.000000
# Process putter put: 5.000000
# Process putter put: 6.000000
# Process getter get: 5.000000
# Process putter put: 7.000000
# Process getter get: 6.000000
# Process putter put: 8.000000
# Process getter get: 7.000000
# Process putter put: 9.000000
# Process putter put: 10.000000
# Process getter get: 8.000000
# Process getter get: 9.000000
# Process getter get: 10.000000
在我们上面执行多个任务时,使用的线程方案都是“即时创建, 即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。一个线程的运行时间可以分为3部分:线程的启动时间、线程体的运行时间和线程的销毁时间。在多线程处理的情景中,如果线程不能被重用,就意味着每次创建都需要经过启动、销毁和运行3个过程。这必然会增加系统相应的时间,降低了效率。所以就有了线程池的诞生,
由于线程预先被创建并放入线程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性。
创建:executor = ThreadPoolExecutor(max_workers= )
方法:
下面以爬虫为例编写一个简单的线程池:
from concurrent.futures import ThreadPoolExecutor
import time
def get_html(times):
time.sleep(times) # 模拟爬取时间
print(f"get page {times}s finished")
return times
executor = ThreadPoolExecutor(max_workers=2)
# 通过submit函数提交执行的函数到线程池中,submit函数立即返回,不阻塞
task1 = executor.submit(get_html, 2)
task2 = executor.submit(get_html, 1)
# done方法用于判定某个任务是否完成
print(task1.done())
print(task2.done())
time.sleep(3) # 主线程阻塞3秒等待子线程执行完毕
print(task1.done())
print(task1.done())
print(task1.result())
print(task2.result())
# 执行结果
# False
# False
# get page 1s finished
# get page 2s finished
# True
# True
# 2
# 1
取消线程
# 取消线程
from concurrent.futures import ThreadPoolExecutor
import time
def get_html(times):
time.sleep(times) # 模拟爬取时间
print(f"get page {times}s finished")
return times
executor = ThreadPoolExecutor(max_workers=2)
# 通过submit函数提交执行的函数到线程池中,submit函数立即返回,不阻塞
task1 = executor.submit(get_html, 2)
task2 = executor.submit(get_html, 1)
task3 = executor.submit(get_html, 3)
task3.cancel()
# done方法用于判定某个任务是否完成
print(task1.done())
print(task2.done())
print(task3.done()) # 线程3被取消后,返回True
time.sleep(3) # 主线程阻塞3秒等待子线程执行完毕
print(task1.done())
print(task1.done())
print(task1.result())
print(task2.result())
# print(task3.result()) # 线程3被取消,获取result报错(concurrent.futures._base.CancelledError)
# 执行结果
# False
# False
# True
# get page 1s finished
# get page 2s finished
# True
# True
# 2
# 1
其实task.result()会等待等待子线程返回结果再往下执行,主线程可以不阻塞
from concurrent.futures import ThreadPoolExecutor
import time
def get_html(times):
time.sleep(times) # 模拟爬取时间
print(f"get page {times}s finished")
return times
executor = ThreadPoolExecutor(max_workers=2)
task1 = executor.submit(get_html, 2)
task2 = executor.submit(get_html, 1)
t1 = time.time()
print(task1.done())
print(task2.done())
print(task1.result())
print("----")
print(task2.result())
print("耗时%ds"%(time.time() - t1))
# 执行结果
# False
# False
# get page 1s finished
# get page 2s finished
# 2
# ----
# 1
# 耗时2s
有时候我们是得知某个任务结束了,就去立马获取结果,而不是自己判断每个任务有没有结束。这就用到as_completed方法。
as_completed()
方法是一个生成器,在没有任务完成的时候,会阻塞,当有任务完成的时候,就会yield
这个任务,就能执行for循环下面的语句,然后继续阻塞住,等待下一个任务完成,直到所有的任务结束。
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def get_html(times):
time.sleep(times)
print(f"get page {times}s finished")
return times
executor = ThreadPoolExecutor(max_workers=2)
urls = [3, 2, 4, 2] # 并不是真的url
all_task = [executor.submit(get_html, url) for url in urls]
for future in as_completed(all_task):
data = future.result()
print(f"in main: get page {data}s success")
# 执行结果
# get page 2s finished
# in main: get page 2s success
# get page 3s finished
# in main: get page 3s success
# get page 4s finished
# in main: get page 4s success
可以看到结果中,限制性完的先获得结果并打印出来。
除了as_completed
方法,还可以使用executor.map
方法获取运行完的线程,但是和as_completed不同,executor.map返回的结果顺序是按照任务列表的顺序,并且不用在使用get方法获取结果,直接就返回结果,如下:
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def get_html(times):
time.sleep(times)
print(f"get page {times}s finished")
return times
executor = ThreadPoolExecutor(max_workers=2)
urls = [3, 2, 4]
for data in executor.map(get_html, urls):
print(f"in main: get page {data}s success")
# 执行结果
# get page 2s finished
# get page 3s finished
# in main: get page 3s success
# in main: get page 2s success
# get page 4s finished
# in main: get page 4s success
wait
方法可以让主线程阻塞,直到满足设定的要求,比如,我们想让主线程等待所有的子线程运行结束:
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, FIRST_COMPLETED
import time
def get_html(times):
time.sleep(times)
print(f"get page {times}s finished")
return times
executor = ThreadPoolExecutor(max_workers=2)
urls = [3, 2, 4]
all_task = [executor.submit(get_html, (url)) for url in urls]
wait(all_task, return_when=ALL_COMPLETED)
print("main")
# 执行结果
# get page 2s finished
# get page 3s finished
# get page 4s finished
# main
如果我们把return_when改为FIRST_COMPLETED,结果将如下:
get page 2s finished
main
get page 3s finished
get page 4s finished
参考:
https://www.cnblogs.com/chenhuabin/p/10082249.html