python3从入门到精通(三): python多线程

九、python 中的多线程

单个进程中同时运行多个线程 ,提高CPU的利用率, 可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程

* 多线程(Multithreading):
  - 线程是进程中的一个执行单元。一个进程可以包含多个线程,它们共享同一个内存空间,共同完成任务
  - 多线程通常用于 I/O 密集型任务(例如网络请求、文件读写等),因为这些任务往往会因为 I/O 操作而被阻塞。
  - 通过多线程,我们可以让程序在等待 I/O 操作时继续处理其他任务,从而提升程序的整体响应速度

# 比如: 
1. Tomcat可以做并行处理,提升处理的效率,而不是一个一个排队
2. 处理一个网络等待的操作,开启一个线程去处理需要网络等待的任务,让当前业务线程可以继续往下执行逻辑,效率是可以得到大幅度提升

# 多线程的局限
1. 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
2. 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好
3. 线程安全问题:虽然多线程带来了一定的性能提升,但是在做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操锁时,会造成死锁问题

1、threading模块

python的threading模块是对thread做了一些包装的,可以更加方便被使用。

# threading 模块常用方法如下:
* threading.active_count(): 返回当前处于active状态的Thread对象
* threading.current_thread(): 返回当前Thread对象
* threading.get_ident(): 返回当前线程的线程标识符,线程标识符是一个非负整数,并无特殊含义,只是用来标识线程,该整数可能会被循环使用。py3.3及以后支持
* threading.enumerate(): 返回当前处于active状态的所有Thread对象列表
* threading.main_thread(): 返回主线程对象,即启动python解释器的线程对象,py3.4及以后支持
* threading.stack_size(): 返回创建线程时使用的栈的大小,如果指定size参数,则用来指定后续创建的线程使用的栈大小,size必须是0(标识使用系统默认值)或大于32K的正整数。

1.1、Thread类使用说明

threading模块提供了Thread,Lock,RLock,Condition,Event,Timer和Semaphore等类来支持多线程,Thread是其中最重要也是最基本的一个类,可以通过该类创建线程并控制线程运行。

# 使用Thread创建线程的方法:
1.为构造函数传递一个可调用对象
2.继承Thread类并在子类中重写__init__()和run()方法

# 语法格式:
threading.Thread(group=None,target=None, name=None,args=(),kwargs={},*,daemon=None)

# 参数说明:
* group: 通常默认即可,作为日后扩展ThreadGroup类实现而保留
* target: 用于run()方法调用的可调用对象,默认为None
* name: 线程名称,默认是Thread-N格式构成的唯一名称,其中N是十进制数
* args: 用于调用目标函数的参数元组,默认为()
* kwargs: 用于调用目标函数的关键字参数字典,默认为{}
* daemon: 设置线程是否为守护模式,默认为None

# 线程对象threading.Thread的方法和属性:
* start(): 启动线程
* run(): 线程代码,用来实现线程的功能和业务逻辑,可以在子类中重写该方法来自定义线程的行为
* init(self,group=None,target=None, name=None,args=(),kwargs=None,daemon=None): 构造函数
* is_alive(): 判断线程是否存活
* getName(): 返回线程名
* setName(): 设置线程名
* isDaemon(): 判断线程是否为守护线程
* setDaemon(): 设置线程是否为守护线程
* name: 用来读取或设置线程的名字
* ident: 线程标识,用非0数字或None(线程未被启动)
* daemon: 标识线程是否为守护线程,默认为False
* join(timeout=None): 当timeout为None时,会等待至子线程结束;当timeout不为None时,会等待至timeout时间结束,单位为秒

1.1.1、第一种:自定义线程对象

  • join 用法: 等所有线程全部执行完成,再执行后面的代码
'''
t = Thread(target=线程执行的任务对象, args=(任务需要的参数))
每个线程都会执行 worker 函数。程序在所有线程执行完毕后才会继续往下执行
'''
from threading import Thread
import time
import random
def work(host):
   print(host, "----->检测开始")
   time.sleep(random.randint(2, 5))
   print(host, "----->检测结束")

if __name__ == '__main__':
    threads = []
    for i in range(5):
        thread = Thread(target=work, args=(f'服务器{i}',)) # 创建线程对象
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join() 

1.1.2、第二种:继承Thread类,重写run方法

class MyThread(Thread):
    def __init__(self, host):
        self.host = host
        super(MyThread, self).__init__()
    def run(self) -> None:
        print("检测开始", self.host)
        time.sleep(random.randint(3, 5))
        print("检测结束", self.host)
if __name__ == '__main__':
    host_list = ["服务器1", "服务器2", "服务器3"]
    thread_list = []
    for host in host_list:
        thread = MyThread(host)
        thread.start()
        thread_list.append(thread)
 
    for t in thread_list:
        t.join()
    print("服务器检测完成")     

1.2、GIL 与 Python 多线程的局限

由于 Python 的全局解释器锁(Global Interpreter Lock, GIL),Python 多线程在 CPU 密集型任务中无法充分利用多核 CPU 的优势。
GIL 使得在任一时刻只有一个线程能执行 Python 字节码,这意味着多线程在处理 CPU 密集型任务时,并不会带来性能上的提升。
因此,多线程适合用于 I/O 密集型任务,如网络请求、文件读写等,而不适合用于需要大量计算的 CPU 密集型任务

1.3、线程间同步

同步就是协同步骤,按预定的先后次序进行运行。如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据正确性,需要对多个线程进行同步。

使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法release方法,对于那些只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间

1.3.1、共享全局变量问题

'''
多线程开发可能遇到的问题:假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算
,t1和t2都各对g_num加10次,g_num的最终结果应该为20
但是由于多线程同时进行操作,可能出现下面情况:
1.在g_num=0时,t1取得g_num=0。此时系统把t1调度成sleeping状态,那t2转换为running状态,t2也获得g_num=0
2.然后t2对得到的值进行加1并赋值给g_num,使得g_num=1
3.然后系统又把t2调度为sleeping,把t1转为running。线程t1又把它之前得到的0加1后赋值给g_num
4.这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
'''
g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        g_num += 1 
    print("---in work1,g_num is %d---" % g_num)
 
def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("---in work2,g_num is %d---" % g_num)
print("---线程创建之前g_num is %d" % g_num)
 
t1 = threading.Thread(target=work1, args=(100,))
t1.start()
 
t2 = threading.Thread(target=work2, args=(100,))
t2.start()
 
while len(threading.enumerate()) != 1:
    time.sleep(1)
 
print("2个线程对同一个全局变量进行操作之后的最终结果是:%s" % g_num)

对于上面提出的计算错误问题,可以通过线程同步来进行解决思路,如下:

  1. 系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
  2. t1对g_num的值进行+1
  3. t1解锁,此时g_num的值为1,其他线程就可以使用g_num了,而且是g_num的值不是0而是1
  4. 同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证数据正确性

1.3.2、互斥锁

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

threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
# 上锁/解锁过程:
1. 当一个线程调用锁acquire()方法获得锁时,锁就进入locked状态
2. 每次只有一个线程可以获得锁,如果此时另一个线程试图获得这个锁,该线程就会变为block状态,称为阻塞,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入unlocked状态
3. 线程调度程序从处于同步阻塞时状态的线程中选择一个来获得锁,并使得该线程进入运行状态

# 总结:
# 锁的好处:
确保了某段关键代码只能由一个线程从头到尾完成地执行
# 锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际只能单线程模式执行,效率就大大下降了,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有锁时,可能会造成死锁。

使用互斥锁完成两个线程对同一个全局变量各加100万次的操作:

g_num = 0
# 创建锁
mutex = threading.Lock()
 
def demo1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
    print("---demo1---g_num=%d" % g_num)
 
def demo2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
    print("---demo2---g_num=%d" % g_num)
 
# 创建两个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=demo1, args=(1000000,))
p1.start()
 
p2 = threading.Thread(target=demo2, args=(1000000,))
p2.start()
 
while len(threading.enumerate()) != 1:
    time.sleep(1)
 
print("2个线程对同一个全局变量操作之后的最终结果是:%d" % g_num)
1.3.2.1、死锁

在线程间共享多个资源的时候,如果两个线程分别占由一部分资源并且同时等待对方的资源,就会造成死锁

import threading
import time
class MyThread1(threading.Thread):
    def run(self):
        # 上锁
        mutexA.acquire()
        # 延时1s,等待另外线程,把mutexB上锁
        print(self.name+'---do1---up---')
        time.sleep(1)
 
        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name + '---do1---down---')
        mutexB.release()
        mutexA.release()
 
class MyThread2(threading.Thread):
    def run(self):
        mutexB.acquire()
 
        print(self.name+'---do2---up---')
        time.sleep(1)
 
        mutexA.acquire()
        print(self.name + '---do2---down---')
        mutexA.release()

        mutexB.release()
 
mutexA = threading.Lock()
mutexB = threading.Lock()
 
if __name__ =='__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

1.3.3、Lock()

from threading import Thread, Lock
import time

class MyThread(Thread):
    def __init__(self, data, lock):
        super(MyThread, self).__init__(daemon=True)
        self.lock = lock
        self.data = data

    def run(self):
        for i in range(10000):
            self.lock.acquire()
            self.data['count'] = self.data.get('count', 0) + 1
            self.lock.release()
        self.lock.acquire()
        self.data['done'] = self.data.get('done', 0) + 1
        self.lock.release()


def main():
    lock = Lock()
    data = dict()
    num = 100
    for i in range(num):
        thread = MyThread(data, lock)
        thread.start()

    while True:
        print("count = [{0}], done = [{1}]".format(data.get('count', -1), data.get('done', -1)))
        if data.get('done', 0) == num:
            break
        time.sleep(1)

if __name__ == "__main__":
    main()

1.3.4、Condition 条件变量

import random
import time
from threading import Thread, Condition

class NotifyThread(Thread):
    def __init__(self, name, cond):
        super(NotifyThread, self).__init__()
        self.name = name
        self.cond = cond

    def run(self):
        # 等待 wait 线程 的 wait() 操作, notify()操作必须能满足所有的wait操作,否则 wait()线程会卡主一直等待
        time.sleep(random.randint(5, 6))
        for i in range(3 * 5): # notify 3*5次,每次唤醒一个线程,因为有5个线程在等待,每个wait线程会wait()3次
            print("thread-{0} - {1} - send notify".format(self.name, i))
            self.cond.acquire()
            self.cond.notify()
            self.cond.release()
            print("thread-{0} - {1} - send notify ok".format(self.name, i))
            time.sleep(random.randint(1, 2))
        print("thread-{0} over".format(self.name))


class WaitThread(Thread):
    def __init__(self, name, cond):
        super(WaitThread, self).__init__()
        self.name = name
        self.cond = cond

    def run(self):
        time.sleep(random.randint(1, 3))
        for i in range(3):  # 每个线程会等待3次
            print("thread-{0} - {1} - wait for notify ...".format(self.name, i))
            self.cond.acquire()
            self.cond.wait()
            self.cond.release()
            print("thread-{0} - {1} - get notify !".format(self.name, i))
        print("thread-{0} over !".format(self.name))

def main():
    tlist = list()
    cond = Condition()
    n = 5

    nt = NotifyThread("notify", cond)
    tlist.append(nt)

    for i in range(n):
        wt = WaitThread(str(i), cond)
        tlist.append(wt)

    for t in tlist:
        t.start()

    for t in tlist:
        print("waiting for {0} end".format(t.name))
        t.join() # 等待所有线程执行完毕

if __name__ == "__main__":
    main()

1.3.5、Event 信号传递

import random
import time
from threading import Thread, Event
from queue import Queue, Empty

class Task(object):
    def __init__(self, event, option=''):
        self.event = event
        self.option = option
        self.data = None


class ReadFileThread(Thread):
    def __init__(self, q):
        super(ReadFileThread, self).__init__()
        self.q = q

    def run(self):
        for i in range(5):
            time.sleep(random.randint(1, 3))
            task = None
            try:
                task = self.q.get(timeout=10) # 从队列中取出一个任务 (如果10秒内没有取到,则抛出异常)
                if task.option == 'readfile':
                    task.data = 'read content:' + str(random.randint(100, 200))
                task.event.set() # 通知主线程,数据已经处理完成, 可以开始执行主线程中的其他任务了 (设置event的标志位为True)
            except Empty as e:
                print('queue is empty, before timeout:,', str(e))


class PrintDataThread(Thread):
    def __init__(self, q):
        super(PrintDataThread, self).__init__()
        self.q = q

    def run(self):
        for i in range(5):
            task = Task(Event(), 'readfile') # 创建一个task对象, 并且设置option为'readfile'
            self.q.put(task) # 将任务放入队列中
            task.event.wait() # 等待读取文件线程处理完成,再执行下面的打印任务 (阻塞在这里,等待event的标志位为True)
            print("get file data: [{0}]".format(task.data))

def main():
    q = Queue()
    reader = ReadFileThread(q)
    printer= PrintDataThread(q)

    reader.start()
    printer.start()

    printer.join()
    reader.join()
    print('exit')


if __name__ == '__main__':
    main()

1.3.6、Queue 队列

from queue import Queue, Empty
import threading
import time, random
 
 
class Writer(threading.Thread):
    def __init__(self, q):
        threading.Thread.__init__(self, daemon=True)
        self.q = q
        self.done = False
 
    def run(self):
        for i in range(10):
            p = 'product-' + str(i)
            print("make product : [{0}]".format(p))
            self.q.put(p)
            time.sleep(random.randint(1, 3))
        self.done = True
 
 
class Reader(threading.Thread):
    def __init__(self, q):
        threading.Thread.__init__(self, daemon=True)
        self.q = q
        self.done = False
 
    def run(self):
        while True:
            try:
                d = self.q.get(timeout=5)
                print('get product :', d)
            except Empty as e:
                print("no product get")
                break
        self.done = True
 
 
def main():
    q = Queue()
    maker = Writer(q)
    reader = Reader(q)
 
    maker.start()
    reader.start()
 
    while True:
        time.sleep(1)
        if maker.done == True and reader.done == True:
            print("maker and reader is done, quit")
            break
 

if "__main__" == __name__:
    main()

1.4、线程池

1.4.1、ThreadPoolExecutor 基本概念

ThreadPoolExecutor 是 Python 标准库 concurrent.futures 模块中的一个类,它提供了一个高级接口,用于在一个线程池中异步执行可调用对象(函数或方法)。它是实现并发编程的强大工具,尤其适用于处理 I/O 密集型任务(如网络请求、文件读写、数据库操作等)

ThreadPoolExecutor 可以理解为一个线程池管理器。它维护着一个预先创建好的线程集合。当你提交一个任务(即一个函数和它的参数)时,ThreadPoolExecutor 会从池中取出一个空闲的线程来执行这个任务,而不是每次都创建一个新线程。任务执行完毕后,线程不会被销毁,而是返回到池中等待下一个任务

# 使用上下文管理器(with 语句)管理,确保资源自动释放
with ThreadPoolExecutor (max_workers=None) as executor
  * max_workers: 指定进程池中的最大进程数,默认为None(自动设为CPU核心数,推荐保持默认以避免资源浪费)min(32, os.cpu_count() + 4)
  * 上下文管理器(with): 自动调用 shutdown() 方法释放资源,无需手动关闭进程池

# 核心优势:
1. 减少线程创建和销毁的开销:
线程的创建和销毁是有性能成本的。线程池复用线程,大大提高了效率,特别是在需要处理大量短期任务时。
2. 限制并发线程数量:
你可以指定线程池的最大大小,防止因创建过多线程而导致系统资源耗尽或严重的上下文切换开销。
3. 简化并发编程:
它提供了简单易用的API(submit()map()),使得编写并发代码变得更加直观和简洁,无需手动管理线程的生命周期。

# 工作原理简述
1. 初始化:
创建一个ThreadPoolExecutor实例时,指定一个max_workers参数,它定义了池中的最大线程数。如果不指定,ThreadPoolExecutor会根据系统的CPU核心数来动态调整一个默认值(通常是min(32, os.cpu_count()+4)2. 提交任务:
  - 通过submit()方法提交一个任务,返回一个Future 对象。Future对象代表了任务的异步执行结果。可通过 future.result()来获取任务的返回值(这会阻塞直到任务完成),或者通过future.add_done_callback()来注册一个回调函数,当任务完成时自动调用。
  - 通过map()方法批量提交任务,用法类似于内置的map()函数。会将可迭代对象中的每个元素作为参数传递给指定的函数,并返回一个迭代器,迭代器的每个元素是对应任务的返回值。map()会阻塞直到所有任务都完成。
3. 任务执行:
线程池内部会管理一个任务队列。当有空闲线程时,它会从队列中取出一个任务并执行。
4. 关闭线程池:
当所有任务都执行完毕后,或者不再需要线程池时,应该调用shutdown()方法来优雅地关闭它。这会等待所有已提交但尚未开始执行的任务完成,然后终止所有工作线程。ThreadPoolExecutor也可以用作上下文管理器(with语句),它会在代码块结束时自动调用shutdown()
# ThreadPoolExecutor 类常用方法:
"""
Future 对象的常用方法在进程池ProcessPoolExecutor那里有说明,使用方法一模一样 
"""
* submit(fn, *args, **kwargs):
  - 用于异步提交单个任务到线程池的方法
  - 支持向目标函数传递任意数量的位置参数和关键字参数,并返回一个用于获取结果的Future对象
  - 适合任务参数动态生成或需要单独处理每个任务结果/异常的场景
  - fn: 需要在线程池中执行的目标函数(任务)
  - *args: 传递给 fn 的位置参数
  - **kwargs: 传递给 fn 的关键字参数。
  - 返回值:Future 对象,用于后续获取结果、设置回调或查询状态
# submit核心特性:
  - 异步非阻塞: 调用submit后会立即返回一个Future对象,主进程无需等待任务完成,可继续执行其他操作
  - 单个任务提交: 每次调用仅提交一个任务(与map批量处理迭代对象不同),适合处理独立的单任务
  - 结果获取: 通过Future对象的future.result(timeout=None) 获取结果,会阻塞当前线程,直到任务完成并返回结果;若任务执行超时(指定 timeout),抛出 TimeoutError
  - 异常捕获: 若任务执行过程中抛出异常,future.result() 会重新抛出该异常(需通过 try-except 捕获)
  - 回调函数: 通过 future.add_done_callback(fn) 注册回调函数,任务完成后自动执行(无需阻塞等待)

* map(fn, *iterables, timeout=None, chunksize=1):
  - 用于批量并行处理可迭代对象的方法,它会将多个可迭代对象中的元素作为参数传递给目标函数,并返回按输入顺序排列的结果迭代器
  - fn: 需要并行执行的目标函数,接收的参数数量需与 iterables 的数量一致
    -: iterables 有2个可迭代对象时,fn 需接收2个参数
  - *iterables: 一个或多个可迭代对象(如列表、元组等),元素会按位置依次传递给 fn(类似内置 map 函数的参数传递逻辑)
  - timeout(可选): 超时时间(秒)
    - 若所有任务在超时时间内未完成,会抛出 concurrent.futures.TimeoutError
  - chunksize(可选,默认 1: 数据分块大小
    - 指定每次分配给工作进程的元素数量,当数据量较大时,调大chunksize可减少进程间通信开销(建议根据数据量和进程数调整)
# map工作原理:
  - map() 返回的不是一个惰性迭代器,而是一个已经包含了所有任务执行结果的可迭代对象
  - 在内部已经完成了所有任务的调度和执行,并将结果按顺序存储起来,等待你去迭代获取
  - 当你遍历这个返回的对象时,你只是在读取已经计算好的结果,而不会触发任何新的任务执行
  - 你调用它之后,主程序会在这里卡住,一直等到进程池中的所有子进程都完成了它们的任务,map 方法才会返回。返回的这个对象,其实就是一个装满了所有结果的 “篮子”

# map核心特性:
  - 批量并行处理: 自动将iterables中的元素分组,并行提交给进程池中的工作进程执行,无需手动逐个提交任务
  - 返回迭代器: 调用后返回一个迭代器,迭代时会按输入顺序返回结果(即使任务完成顺序不同,结果也会按输入顺序排列)。
  - 阻塞行为: 迭代返回的迭代器时,若结果未准备好,会阻塞等待;若设置了 timeout,则总等待时间超过超时值会抛出异常
    - list(executor.map(...)) 会阻塞主线程,直到所有任务完成(若需非阻塞,需结合as_completed或 wait)
  - 多参数支持:通过 *iterables 传递多个可迭代对象,fn 可接收多个参数(例如,iterables=(list1, list2) 时,fn 会接收 list1[i] 和 list2[i] 作为参数)
# map关键注意事项
  - 结果顺序: 即使任务完成顺序不同,map 返回的迭代器也会严格按输入 iterables 的元素顺序返回结果(与 imap 一致,区别于 imap_unordered)。
  - 迭代器特性: 结果是惰性生成的,迭代时才会获取结果(若某个结果未准备好,会阻塞等待),内存效率较高(无需一次性缓存所有结果)。
  - 超时机制: timeout 是所有任务的总超时时间,而非单个任务。若总耗时超过 timeout,迭代时会抛出 TimeoutError
  - 异常传递: 若任何一个任务执行抛出异常,迭代results时会立即抛出该异常(需在try-except块中处理)
# map适用场景
  1. 处理批量结构相似的任务(如对列表中每个元素执行相同操作,或多列表元素按位置组合计算)。
  2. 需保持结果与输入顺序一致的场景(如数据按顺序处理后写入文件)
# map核心语法:
  1. 把多个可迭代对象(*iterables) 按索引位置一一配对,生成一个个任务;
  2. 每个任务的参数,就是所有可迭代对象在同一索引下的元素;
  简单说:如果有 3 个可迭代对象 A=[a1,a2]、B=[b1,b2]、C=[c1,c2]map(fn, A,B,C) 会自动生成 2 个任务:fn(a1,b1,c1)、fn(a2,b2,c2)3. 多可迭代对象长度需一致:*iterables 中多个可迭代对象长度不同,map 会以最短的为准(其余元素被忽略)。
  4. 避免在map中处理耗时过长的任务: 若需单独控制每个任务的超时,建议使用submit+ Future.result(timeout)

* shutdown(wait=True, cancel_futures=False):
  - 用于关闭进程池,释放相关资源,并控制未完成任务的处理方式。它是进程池生命周期管理的核心方法,通常在所有任务提交完成后调用
  - wait(可选,默认 True:
    - 若为True,调用shutdown后,主进程会阻塞等待所有已提交的任务(包括正在执行和排队的)完成后再关闭进程池。
    - 若为False,主进程不会等待,直接返回,但进程池会在后台继续处理已提交的任务,直到完成后再关闭。
  - cancel_futures(可选,默认 False:
    - 若为True,会取消所有尚未开始执行的任务(已在工作进程中运行的任务不受影响)。
    - 若为False,则保留所有未执行的任务,等待它们完成(即使 wait=False,未执行的任务也会在后台完成)

	wait   cancel_futures	        行为描述
	True	False(默认)  主进程阻塞,等待所有已提交的任务(包括排队的)完成后,关闭进程池
	True	True	       取消所有未开始的任务,主进程阻塞等待已运行的任务完成后,关闭进程池
	False	False	       主进程不阻塞,直接返回;进程池在后台继续处理所有已提交的任务,完成后关闭
	False	True	       取消所有未开始的任务,主进程不阻塞,直接返回;已运行的任务继续执行至完成
# shutdown核心作用:
  1. 阻止新任务提交: 调用 shutdown 后,进程池不再接受新的 submit 或 map 任务,否则会抛出 RuntimeError。
  2. 管理未完成任务: 根据 wait 和 cancel_futures 的组合,决定是否等待任务完成或取消未执行的任务。
  3. 释放资源:关闭所有工作进程,释放与进程池相关的系统资源(如进程、内存等)
# shutdown适用场景
  1. 正常关闭: 任务全部提交后,调用 shutdown(wait=True)(或依赖 with 自动关闭),确保所有任务完成后再退出程序。
  2. 紧急终止: 若需提前结束进程池(如程序异常),可设置 cancel_futures=True 取消未执行的任务,减少资源占用。
  3. 非阻塞关闭: 若主进程需在等待任务完成期间执行其他操作,可设置 wait=False,但需注意进程池在后台仍会处理任务。
# shutdown注意事项
  1. 线程池关闭后不可再提交任务: 调用shutdown()后,再次调用submit()map()会抛出RuntimeError
  2. 使用with语句自动关闭: with ThreadPoolExecutor(...) as executor: 会在代码块结束时自动调用 shutdown(wait=True),无需手动关闭
  3. cancel_futures仅对未执行任务有效: 若任务已分配给线程并开始执行,cancel_futures=True 无法取消该任务

1.4.2、生产案例:【submit()方法】

1.4.2.1、多服务器批量运维检查与报告生成
import random
import time
import logging
from concurrent.futures import ThreadPoolExecutor, Future
from typing import Dict, Tuple, List

# 配置日志(模拟回调中写入日志系统的场景)
logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler()]
)

def _simulate_ssh_command(server_ip: str, command: str) -> Tuple[int, str]:
    """
    模拟SSH执行命令,返回(返回码, 输出)
    实际项目中应使用paramiko等库
    """
    print(f"线程【{server_ip.split('.')[-1]}】正在执行命令: {command}")
    time.sleep(random.uniform(0.5, 2.0))  # 模拟执行延迟

    if "cpu" in command.lower():
        usage = random.uniform(10, 95)
        return 0, f"CPU Usage: {usage:.1f}%"
    elif "mem" in command.lower():
        usage = random.uniform(20, 90)
        return 0, f"Mem Usage: {usage:.1f}%"
    elif "disk" in command.lower():
        usage = random.uniform(30, 90)
        return 0, f"Disk Usage: {usage:.1f}%"
    elif "process" in command.lower():
        is_running = random.choice([True, True, True, False])  # 75%概率为True
        if is_running:
            return 0, "Process is running"
        else:
            return 1, "Process is not running"
    else:
        return 127, "Command not found"

def check_server(server_ip: str) -> Dict:
    """
    对单台服务器执行所有检查项
    """
    print(f"--- 开始检查服务器: {server_ip} ---")
    check_results = {
        "server_ip": server_ip,
        "status": "OK",
        "details": {}
    }

    checks = [
        ("cpu", "check cpu usage"),
        ("mem", "check mem usage"),
        ("disk", "check disk usage"),
        ("process", "check process nginx")
    ]
    for check_name, command in checks:
        exit_code, output = _simulate_ssh_command(server_ip, command)  # 模拟SSH执行
        check_results['details'][check_name] = output  # 存储检查结果

        if exit_code != 0:
            check_results['status'] = "ERROR"  # 任一检查失败则标记为错误
        elif "usage" in output.lower():
            try:
                usage = float(output.split(':')[-1].strip().strip("%"))
                if usage > 90:
                    check_results['status'] = "ERROR"  # 使用率超过90%标记为错误
                elif usage > 80 and check_results['status'] != "ERROR":
                    check_results['status'] = "WARNING"  # 使用率超过80%标记为警告
            except (IndexError, ValueError):
                check_results['status'] = 'WARNING'
                check_results['details'][check_name] += " (解析失败)"

    print(f"--- 服务器【{server_ip}】检查完毕,状态:【{check_results['status']}】 ---")
    return check_results

def handle_server_check_result(future: Future, global_results: List[Dict]):
    """
    单台服务器检查完成后的回调函数
    功能:1. 实时处理结果(告警、写日志);2. 将结果存入全局列表(供最终汇总)
    :param future: 完成的Future对象
    :param global_results: 全局结果列表(用于汇总报告)
    """
    try:
        # 1. 获取任务结果(如果任务抛出异常,这里会重新抛出)
        result = future.result(timeout=5)  # 超时保护(避免无限阻塞)
        server_ip = result['server_ip']
        status = result['status']

        # 2. 实时处理:根据状态打印不同级别日志(模拟运维告警)
        if status == 'ERROR':
            logging.error(f"【告警】服务器 {server_ip} 检查异常!详情:{result['details']}")
            # 实际场景可扩展:调用邮件/短信告警接口、写入Zabbix/Prometheus监控
        elif status == 'WARNING':
            logging.warning(f"【警告】服务器 {server_ip} 检查存在风险!详情:{result['details']}")
        else:
            logging.info(f"【正常】服务器 {server_ip} 检查通过,详情:{result['details']}")

        # 3. 将结果存入全局列表(注意:列表是线程安全的吗?append操作是原子的,此处可直接用)
        global_results.append(result)

    except TimeoutError:
        # 处理超时异常(比如任务执行太久)
        logging.error(f"【超时】服务器检查任务超时未返回结果")
    except Exception as e:
        # 处理任务执行过程中的其他异常(比如SSH连接失败)
        logging.error(f"【异常】服务器检查任务执行失败:{str(e)}")
        # 补充异常结果到全局列表
        global_results.append({
            'server_ip': '未知IP',
            'status': 'EXCEPTION',
            'details': {'error': str(e)},
            'check_time': time.strftime("%Y-%m-%d %H:%M:%S")
        })

def generate_report(all_results: List[Dict]):
    """
    根据所有服务器的检查结果生成报告。
    """
    print("\n" + "=" * 50)
    print("           服务器批量检查报告")
    print("=" * 50)

    ok_servers = [r for r in all_results if r['status'] == 'OK']
    warning_servers = [r for r in all_results if r['status'] == 'WARNING']
    error_servers = [r for r in all_results if r['status'] == 'ERROR']

    print(f"\n[正常服务器] ({len(ok_servers)}台):")
    for r in ok_servers:
        print(f"  - {r['server_ip']}")

    if warning_servers:
        print(f"\n[警告服务器] ({len(warning_servers)}台):")
        for r in warning_servers:
            print(f"  - {r['server_ip']}:")
            for check, detail in r['details'].items():
                if 'usage' in detail.lower() and any(80 < float(x.strip('%')) <= 90 for x in detail.split(':')[1:] if
                                                     x.strip('%').replace('.', '').isdigit()):
                    print(f"    * {detail}")

    if error_servers:
        print(f"\n[异常服务器] ({len(error_servers)}台):")
        for r in error_servers:
            print(f"  - {r['server_ip']}:")
            for check, detail in r['details'].items():
                if detail.startswith('Process') or ('usage' in detail.lower() and any(
                        float(x.strip('%')) > 90 for x in detail.split(':')[1:] if
                        x.strip('%').replace('.', '').isdigit())):
                    print(f"    * {detail}")

    print("\n" + "=" * 50)

def main():
    # 模拟多台服务器检查
    server_ips = [f"192.168.1.{i}" for i in range(1, 5)]

    all_results = []

    # 使用 with 语句可以确保executor正确关闭
    with ThreadPoolExecutor(max_workers=10) as executor:
        print(f"启动线程池,最大并发数:{executor._max_workers}")
        # 提交所有任务,并将 Future 对象与服务器IP关联
        for ip in server_ips:
            future = executor.submit(check_server, ip)
            #  为每个future绑定回调函数,处理结果
            future.add_done_callback(lambda f, ar=all_results: handle_server_check_result(f, ar))

    print("\n所有服务器检查任务已完成,正在生成报告...")
    generate_report(all_results)

if __name__ == "__main__":
    start_time = time.time()
    main()
    end_time = time.time()
    print(f"\n总耗时: {end_time - start_time:.2f} 秒")

1.4.3、生产案例:【map()方法】

1.4.3.1、批量处理订单数据并同步至多个下游系统(电商 / 支付场景)

每个订单需同步至所有 3 个下游系统,缺一不可;
结果需与原始订单列表顺序一致(便于后续对账);
单个 API 调用超时不阻塞整体流程,需记录失败订单以便重试

from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Tuple, Optional
import requests
from pydantic import BaseModel, ValidationError
import time
import logging

# 配置日志(生产级日志输出)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(threadName)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# 1. 配置项(生产中从配置中心读取)
# 下游系统API配置
DOWNSTREAM_SERVICES = {
    "logistics": "https://api.logistics.com/create-order",  # 物流系统
    "member": "https://api.member.com/update-points",  # 会员系统
    "analytics": "https://api.analytics.com/report-order"  # 数据分析系统
}
# 超时配置(每个API调用最大超时时间)
API_TIMEOUT = 3
# 线程池并发数(根据下游系统QPS限制调整)
MAX_WORKERS = 30

# 2. 数据模型(订单数据校验)
class Order(BaseModel):
    order_id: str
    user_id: str
    amount: float
    pay_time: str  # 格式:YYYY-MM-DD HH:MM:SS
    product_ids: List[str]

# 3. 单个下游系统同步函数
def sync_to_service(order: Order, service_name: str, service_url: str) -> Tuple[str, str, str]:
    """
    单个订单同步至单个下游系统
    :param order: 订单对象
    :param service_name: 下游系统名称(用于日志和结果标识)
    :param service_url: 下游系统API地址
    :return: (order_id, service_name, 同步结果:success/failed+原因)
    """
    try:
        # 构造API请求参数(根据下游系统要求调整)
        request_data = {
            "order_id": order.order_id,
            "user_id": order.user_id,
            "amount": order.amount,
            "pay_time": order.pay_time,
            "product_ids": order.product_ids
        }
        # 发送POST请求(I/O密集型操作)
        response = requests.post(
            url=service_url,
            json=request_data,
            timeout=API_TIMEOUT,
            headers={"Content-Type": "application/json"}
        )
        # 校验响应状态(假设200为成功)
        if response.status_code == 200 and response.json().get("code") == 0:
            logger.info(f"订单{order.order_id}同步{service_name}成功")
            return order.order_id, service_name, "success"
        else:
            error_msg = f"响应异常:状态码{response.status_code},内容{response.text}"
            logger.error(f"订单{order.order_id}同步{service_name}失败:{error_msg}")
            return order.order_id, service_name, f"failed: {error_msg}"
    except Exception as e:
        error_msg = str(e)
        logger.error(f"订单{order.order_id}同步{service_name}失败:{error_msg}")
        return order.order_id, service_name, f"failed: {error_msg}"


# 4. 批量订单同步(核心:使用map处理多可迭代对象)
def batch_sync_orders(orders: List[Order]) -> Dict[str, Dict[str, str]]:
    """
    批量订单同步至所有下游系统
    :param orders: 待同步订单列表
    :return: 同步结果字典:{order_id: {service_name: 结果}}
    """
    # 构造多可迭代对象(map支持多个可迭代对象,按索引一一对应)
    # 例如:orders[0] 对应 (DOWNSTREAM_SERVICES.keys()[0], DOWNSTREAM_SERVICES.values()[0])
    service_names = list(DOWNSTREAM_SERVICES.keys())
    service_urls = list(DOWNSTREAM_SERVICES.values())
    # 扩展订单列表:每个订单需要对应所有3个下游系统(重复订单列表3次)
    expanded_orders = []
    for order in orders:
        expanded_orders.extend([order] * len(DOWNSTREAM_SERVICES))

    # 线程池批量执行(map的3个可迭代对象长度必须一致)
    with ThreadPoolExecutor(max_workers=MAX_WORKERS, thread_name_prefix="OrderSync") as executor:
        # map参数:函数 + 多个可迭代对象(expanded_orders, service_names*N, service_urls*N)
        results = executor.map(
            sync_to_service,
            expanded_orders,
            service_names * len(orders),  # 每个订单对应3个系统名称
            service_urls * len(orders),  # 每个订单对应3个系统URL
            timeout=API_TIMEOUT * 2  # 单个任务总超时(防止API卡死)
        )

    # 整理结果(按订单ID分组)
    final_results = {}
    for order_id, service_name, result in results:
        if order_id not in final_results:
            final_results[order_id] = {}
        final_results[order_id][service_name] = result
    return final_results


# 5. 主函数(模拟订单数据并执行同步)
if __name__ == "__main__":
    # 模拟1000个订单数据(生产中从消息队列/Kafka读取)
    mock_orders = [
        Order(
            order_id=f"ORDER{10000 + i}",
            user_id=f"USER{5000 + i}",
            amount=round(100 + i * 10, 2),
            pay_time=f"2025-11-27 10:{i // 60}:{i % 60}",
            product_ids=[f"PROD{2000 + i}", f"PROD{2001 + i}"]
        ) for i in range(10)
    ]

    logger.info(f"开始同步{len(mock_orders)}个订单至{len(DOWNSTREAM_SERVICES)}个下游系统")
    start_time = time.time()

    # 批量同步
    sync_results = batch_sync_orders(mock_orders)

    # 统计结果
    total_orders = len(sync_results)
    success_orders = 0  # 所有系统同步成功的订单数
    failed_details = []  # 失败订单详情
    for order_id, service_results in sync_results.items():
        all_success = all(res == "success" for res in service_results.values())
        if all_success:
            success_orders += 1
        else:
            failed_details.append(f"订单{order_id}{service_results}")

    end_time = time.time()
    # 输出统计报告
    logger.info("=" * 60)
    logger.info(f"批量同步完成!总耗时:{end_time - start_time:.2f}秒")
    logger.info(
        f"总订单数:{total_orders} | 全量同步成功:{success_orders} | 部分/全部失败:{total_orders - success_orders}")
    if failed_details:
        logger.warning("失败订单详情:")
        for detail in failed_details[:10]:  # 只输出前10条失败详情
            logger.warning(detail)
    logger.info("=" * 60)
1.4.3.2、解析map中多个可迭代对象的参数含义
results = executor.map(
            sync_to_service,
            expanded_orders,
            service_names * len(orders),  # 每个订单对应3个系统名称
            service_urls * len(orders),  # 每个订单对应3个系统URL
            timeout=API_TIMEOUT * 2  # 单个任务总超时(防止API卡死)
        )
1. 第一个参数:sync_to_service(任务函数)
这是每个线程要执行的单个任务逻辑,定义了一个订单同步到一个下游系统的完整流程,接收3个参数:
order: Order:单个订单对象(要同步的数据);
service_name: str:下游系统名称(如 "logistics",用于日志和结果标识);
service_url: str:下游系统 API 地址(如 "https://api.logistics.com/create-order")。

2. 第二个参数:expanded_orders(订单可迭代对象)
作用:为每个(订单 × 系统)的组合,提供对应的订单对象。
原始orders是N个订单的列表(比如N=1000);
expanded_orders是把每个订单重复 3 次(因为要同步3个系统),最终长度为N×31000×3=3000)。
orders = [order1, order2]
expanded_orders = [order1, order1, order1, order2, order2, order2]

3. 第三个参数:service_names * len(orders)(系统名称可迭代对象)
作用:为每个(订单 × 系统)的组合,提供对应的下游系统名称。
原始service_names = ["logistics", "member", "analytics"]3 个系统);
service_names * len(orders) 是把3个系统名称重复N次(每个订单对应一轮3个系统),最终长度也是N×3。
service_names * 2 = ["logistics", "member", "analytics", "logistics", "member", "analytics"]

4. 第四个参数:service_urls * len(orders)(系统 URL 可迭代对象)
逻辑和 service_names * len(orders) 完全一致:
原始 service_urls 是 3 个系统的 API 地址列表;
重复 N 次后,长度为 N×3,与「订单 + 系统名称」一一对应。

5. 第五个参数:timeout=API_TIMEOUT * 2(任务超时时间)
作用:限制单个任务的最大执行时间(防止某个 API 调用卡死,占用线程不放);
取值逻辑:API_TIMEOUT 是单个 API 调用的超时(3 秒),这里设为 2 倍(6 秒),预留网络波动、服务响应延迟的缓冲;
触发后果:如果某个任务执行超过 6 秒,会抛出 TimeoutError,但不影响其他任务(线程池会继续执行剩余任务)

# 最后调用参数组合如下:
sync_to_service(order1, "logistics", 物流API)	    订单 1 同步到 物流系统
sync_to_service(order1, "member", 会员API)	        订单 1 同步到 会员系统
sync_to_service(order1, "analytics", 数据分析API)	订单 1 同步到 数据分析系统
sync_to_service(order2, "logistics", 物流API)	    订单 2 同步到 物流系统
sync_to_service(order2, "member", 会员API)	        订单 2 同步到 会员系统
sync_to_service(order2, "analytics", 数据分析API)	订单 2 同步到 数据分析系统
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一位不知名民工

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

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

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

打赏作者

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

抵扣说明:

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

余额充值