一文吃透python多线程(全面总结)

1 创建线程

在Python中创建线程主要依靠内置的threading模块。

threading.current_thread():获取到当前线程。

获取线程后可以得到两个比较重要的属性:name和ident,分别是线程名称和id。

创建线程可以使用两种方法:使用函数或类创建。

1.1 函数创建

使用函数创建线程时,使用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秒呢。因为创建子线程后,主线程的代码还在继续向后执行,可以看到当主线程结束后,子线程还在继续执行。后面将学习如何让主线程等待子线程。

1.2 类创建

使用类创建线程需要继承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...

2 线程守护

2.1 deamon

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还没运行结束就随着主线程结束而结束了。

2.2 join

设置子线程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。

3 线程锁

多线程一个很大的问题是数据不安全,因为线程之间的数据是共享的。

以银行转账为例,一个银行的总额是大家一同影响的,创建多个线程(表示多个人进行取钱)共同使用一个变量,如下:

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,这就是因为数据共享的原因。

3.1 Lock

线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源设置一个状态:锁定和非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

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

3.2 死锁

用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。

3.3 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,其他的线程才能获得资源。

4 线程通信

4.1 Condition

Condition可以认为是一把比Lock和RLOK更加高级的锁,其在内部维护一个琐对象(默认是RLock),可以在创建Condigtion对象的时候把琐对象作为参数传入。Condition也提供了acquire, release方法,其含义与琐的acquire, release方法一致,其实它只是简单的调用内部琐对象的对应的方法而已。Condition内部常用方法如下:

  • acquire(): 上线程锁
  • release(): 释放锁
  • wait(timeout): 线程挂起,直到收到一个notify通知或者超时(可选的,浮点数,单位是秒s)才会被唤醒继续运行。wait()必须在已获得Lock前提下才能调用,否则会触发RuntimeError
  • notify(n=1): 通知其他线程,那些挂起的线程接到这个通知之后会开始运行,默认是通知一个正等待该condition的线程,最多则唤醒n个等待的线程。notify()必须在已获得Lock前提下才能调用,否则会触发RuntimeError。notify()不会主动释放Lock
  • notifyAll(): 如果wait状态线程比较多,notifyAll的作用就是通知所有线程

使用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件,仓库饱满,停止生产……

4.2 Semaphore

信号量。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,线程之间仍然有锁保证线程数据安全,所以银行钱数可以正常计算。

4.3 Event

事件。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时候就可以用threading为我们提供的Event对象。

事件处理的机制:全局定义了一个内置标志Flag,如果Flag值为 False,那么当程序执行 event.wait方法时就会阻塞,如果Flag值为True,那么event.wait 方法时便不再阻塞。

Event其实就是一个简化版的 Condition。Event没有锁,无法使线程进入同步阻塞状态。

方法:

  1. set(): 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态。
  2. clear(): 将标志设为False。
  3. wait(timeout): 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()。
  4. is_set(): 获取内置标志状态,返回True或False。

我们使用红路灯为例,过马路都要经过红绿灯, 行人过马路和交通指示灯是两个不同的对象或者处理单元。我们可以把行人和红绿灯抽象为两个独立的线程,行人在绿灯的情况下通过马路,红灯时必须等待。红绿灯可以当做两个线程的通信机制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 通过了

4.4 Queue

queue模块实现了各种消费者-生产者模型队列。可用于在执行的多个线程之间安全的交换信息。

queue具有3中不同的队列:

  • FIFO(先进先出)队列, 第一加入队列的任务, 被第一个取出
  • LIFO(后进先出)队列,最后加入队列的任务, 被第一个取出
  • PriorityQueue(优先级)队列, 保持队列数据有序, 最小值被先取出

常用方法:

  • qsize() 返回队列的规模
  • empty() 如果队列为空,返回True,否则False
  • full() 如果队列满了,返回True,否则False
  • **get([block[, timeout]])**获取队列,timeout等待时间
  • get_nowait() 相当get(False)
  • put(item[, timeout]) 写入队列,timeout等待时间,如果队列已满再调用该方法会阻塞线程
  • put_nowait(item) 相当put(item, False)
  • task_done() 在完成一项工作之后,task_done()函数向任务已经完成的队列发送一个信号
  • join() 实际上意味着等到队列为空,再执行别的操作

多线程的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

5 线程池

在我们上面执行多个任务时,使用的线程方案都是“即时创建, 即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。一个线程的运行时间可以分为3部分:线程的启动时间、线程体的运行时间和线程的销毁时间。在多线程处理的情景中,如果线程不能被重用,就意味着每次创建都需要经过启动、销毁和运行3个过程。这必然会增加系统相应的时间,降低了效率。所以就有了线程池的诞生,

由于线程预先被创建并放入线程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性。

创建:executor = ThreadPoolExecutor(max_workers= )

方法:

  • task = executor.submit(func,(args)) 提交任务到线程池
  • task.result() 获取线程执行结果
  • task.done() 判断线程是否执行完毕
  • task.cancel() 取消线程,如果已经在线程池中了,就取消不了

5.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)
# 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

5.2 as_completed

有时候我们是得知某个任务结束了,就去立马获取结果,而不是自己判断每个任务有没有结束。这就用到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

可以看到结果中,限制性完的先获得结果并打印出来。

5.3 map

除了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

5.4 wait

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

  • 16
    点赞
  • 107
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ethan-running

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值