Python 多线程

一、线程核心参数及创建

1.1 线程最核心的两个参数 

# 线程名称
threading.current_thread().name
# 线程唯一 ID
threading.current_thread().ident

1.2 函数创建

import time
import threading

def fun(n):
    print("%d thread name %s 开始运行" % (n, threading.current_thread().name))
    print("%d thread id %s 开始运行" % (n, threading.current_thread().ident))
    time.sleep(n)

for i in range(10):
    t = threading.Thread(target=fun, name="Thread Name %s" % i, args=(i,))
    t.start()

1.3 类创建

class MyThread(threading.Thread):
    def __init__(self, n, name=None, group=None):
        super().__init__()
        self.name = name
        self.n = n
        self.group = group

    def run(self) -> None:
        start = time.time()
        print("%d thread name %s 开始运行" % (self.n, threading.current_thread().name))
        print("%d thread id %s 开始运行" % (self.n, threading.current_thread().ident))
        time.sleep(self.n)
        print("group %s cost time %d" % (self.group, time.time() - start))


for i in range(10):
    t = MyThread(i, name="Thread %s" % i)
    t.start()

二、线程阻塞

2.1 守护线程

Thread 类有 deamon 属性,默认为 False,含义: 

1. 当deamon值为True,即设为守护线程后,只要主线程结束了,无论子线程代码是否结束,都得跟着结束

2. 修改deamon的值必须在线程start()方法调用之前,否则会报错。

举例:

import time
import threading


def fun(n):
    print("%d thread name %s 开始运行" % (n, threading.current_thread().name))
    print("%d thread id %s 开始运行" % (n, threading.current_thread().ident))
    time.sleep(n)


for i in range(10):
    t = threading.Thread(target=fun, name="Thread Name %s" % i, args=(i,))
    t.daemon = True
    t.start()

2.2 join 阻塞

  设置子线程join后,主线程会阻塞等待子进程完成,再执行join后面的代码

import time
import threading


def fun(n):
    print("%d thread name %s 开始运行" % (n, threading.current_thread().name))
    print("%d thread id %s 开始运行" % (n, threading.current_thread().ident))
    time.sleep(n)


thread_list = []

for i in range(3):
    t = threading.Thread(target=fun, name="Thread Name %s" % i, args=(i,))
    t.start()
    thread_list.append(t)

for thread in thread_list:
    # 子线程执行结束后,父线程才执行
    thread.join()

print("主线程阻塞")

# 输出结果:
# 0 thread name Thread Name 0 开始运行
# 0 thread id 6172913664 开始运行
# 1 thread name Thread Name 1 开始运行
# 1 thread id 6189740032 开始运行
# 2 thread name Thread Name 2 开始运行
# 2 thread id 6206566400 开始运行

三、线程锁

3.1 Lock

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

# 声明锁
lock = threading.Lock()
# 获取锁
lock.acquire()
# 释放锁
lock.release()

银行存取案例

import threading
import time

global_money = 1000

lock = threading.Lock()


def count_down_money(n):
    lock.acquire()
    global global_money
    global_money = global_money - n
    print("剩余 %s" % global_money)
    time.sleep(10)
    lock.release()


for i in range(3):
    t = threading.Thread(target=count_down_money, name="Thread Name %s" % i, args=(i,))
    t.start()

with lock这种上下文格式,自动管理上锁和释放锁。

import threading

global_money = 1000

lock = threading.Lock()


def count_down_money(n):
    with lock:
        global global_money
        global_money = global_money - n
        print("剩余 %s" % global_money)


for i in range(3):
    t = threading.Thread(target=count_down_money, name="Thread Name %s" % i, args=(i,))
    t.start()

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

还有一种情况,在同一线程里,多次取获得锁,第一次获取锁后,还未释放,再次获得锁

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

3.3 Rlock(可重入锁)

所谓的递归锁也被称为“锁中锁”,指一个线程可以多次申请同一把锁,但是不会造成死锁,这就可以用来解决上面的死锁问题。

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")


name_list = ["Einstein", "Curie"]
lock = RLock()
t_list = []
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()

四、线程通信

4.1 Condition

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

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

4.3 Event

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

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

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

方法:set(): 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态。
clear(): 将标志设为False。
wait(timeout): 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()。
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

五、线程池

爬虫案例

爬虫为例编写一个简单的线程池

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

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

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

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

参考

https://www.cnblogs.com/chenhuabin/p/10082249.html

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值