python学习笔记11 -- 进程和线程

进程和线程

1 相关的基本概念

1.1 并行和并发

  • 并发:单个 CPU 处理多个任务。各个任务交替执行一段时间。
  • 并行:多个 CPU 同时执行多个任务。

关于并行和并发,关注的是任务是否同时执行

1.2 同步和异步

  • 同步:只能有一个任务执行,当前任务执行的时候,其他任务要等待。即多个任务排队执行。
  • 异步:多个任务同时执行,任务之间互相不影响。

关于同步和异步关注的是调用者是否需要等待被调用任务完成才能继续执行。

案例1:

案例2:

2 进程

复制代码
from multiprocessing import Process
Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

os.getpid(): 获取进程id
os.getppid(): 获取父进程id
current_process():当前运行的进程
daemon: True守护进程 守护主进程
     主进程结束 守护进程自动结束

进程锁: 锁进程

注意:
1.为什么添加  if __name__ == '__main__': 防止无限递归
只要子进程执行 就会再次导入主模块 会发生无限递归操作

2.进程的默认名字  Process-1 ....

进程内的方法:
join(): 阻塞主进程执行的 等待子进程执行完毕
kill():杀死进程
start():启动进程

2.1 什么是进程

  • 操作系统中一个正在运行的程序或软件就是一个进程。
  • 进程是操作系统进行资源分配的基本单位。
  • 每个进程都有自己独立的一块内存空间。
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响。

2.2 创建进程的4种方式

Unix/Linux操作系统提供了一个 os.fork() 系统调用。Windows 中没有 fork() 调用,不过Python提供了一个跨平台的多进程模块 multiprocessing。

  • multiprocessing.current_process():获取当前进程对象

  • multiprocessing 模块提供了一个 Process 类来表示进程

  • multiprocessing 模块提供了一个Pool类表表示进程池

不同创建方式的对比

方式 优点 缺点 适用系统
创建Process 类对象 简单直观、跨平台、易扩展 创建大量进程时开销大 Windows/Linux
继承 Process 类 面向对象、封装性好 代码量略多 Windows/Linux
Pool 进程池 复用进程、降低开销、批量处理 任务需统一、灵活性稍低 Windows/Linux
os.fork() 轻量级、系统级原生支持 不跨平台、封装差、易出错 Linux/macOS

2.3 方式一:直接创建Process 类对象

语法格式:

python 复制代码
multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
  • group:应当始终为 None,它的存在仅是为了与 threading.Thread 兼容。

  • target:接收一个函数对象,该函数最终由进程的run() 方法来调用执行,默认为 None。

  • name:进程名称,默认为 None 则自动分配。

  • args:需要给target目标函数的参数元组或列表。

  • kwargs:需要给target目标函数的关键字参数字典。

  • daemon:是否为守护进程,True 或 False。默认为None则继承父进程的daemon值。

    Process类的一些属性和方法:

    • name属性:获取进程名称。
    • pid属性:获取进程号。
    • daemon属性:判断或设置进程是否为守护进程。
    • start():启动进程,调用传入 target 的对象。start() 只能被调用一次。
    • run():默认调用传入 target 的对象,子类可以重写此方法来自定义行为。
    • join(timeout):阻塞主进程,直到子进程结束或超时。timeout参数可选,意为阻塞多少秒。
    • terminate():强制终止子进程。
    • kill():杀死进程,与 terminate() 类似,但更彻底。
    • is_alive():检查进程是否仍在运行。
    • os.getpid():获取当前进程编号。
    • os.getppid():获取当前进程的父进程编号。
1.基本使用

示例代码1:龟兔赛跑

python 复制代码
from multiprocessing import Process, current_process
import time

def func1():
    for e in range(1, 15):
        print(f"乌龟跑.....{e}.")
        time.sleep(1)


def func2():
    for e in range(1, 11):
        print(f"乌龟跑..{e}....")
        time.sleep(1)
"""        
1.右键运行 python解释器开始执行
2.导入相关的资源
3.定义函数 fun1 fun2
4.创建新的进程对象 p1 p2
5.p1.start 开始申请创建子进程

6.会重写再开一个python 解释器
"""
if __name__ == '__main__':
    p1 = Process(target=func1)
    p2 = Process(target=func2)

    p1.start()
    p2.start()
2.参数设置

示例代码2:龟兔赛跑(相关参数)

current_process():获取当前进程

python 复制代码
from multiprocessing import Process, current_process
import time
import os

def func1(a,b):
    for e in range(1, 15):
        print(f"{a}---{b}----乌龟跑.....{e}.pid{os.getpid()} 父进程id{os.getppid()}----{current_process().name}")
        time.sleep(1)


def func2():
    for e in range(1, 11):
        print(f"兔子跑....{e}..pid{os.getpid()} 父进程id{os.getppid()}..")
        time.sleep(1)

"""
1.右键运行 python解释器开始执行
2.导入相关的资源
3.定义函数 fun1 fun2
4.创建新的进程对象 p1 p2
5.p1.start 开始申请创建子进程

6.会重写再开一个python 解释器
...... 无限递归
"""
if __name__ == '__main__':
    print(f"开始执行 进程id{os.getpid()}")
    #  给进程设置名字 传递参数
    p1 = Process(target=func1,name="乌龟进程",args=(10,20))
    p2 = Process(target=func2)
    # 申请创建子进程
    p1.start()
    p2.start()
    
    p2.join()# 阻塞主进程 p2 执行完 主进程才能执行
    p1.join()
    print("执行结束......")
3.守护进程
python 复制代码
import time
from multiprocessing import Process
import os

def work():
    while True:
        with open("log.txt", 'r', encoding='utf-8') as f:
            linenum = len(f.readlines())
            print(f"写了几行{linenum},{os.getpid()} 父进程:{os.getppid()}")
            time.sleep(0.5)


if __name__ == '__main__':
    print(f"主进程开始了{os.getpid()}")
    p = Process(target=work, daemon=True)
    p.start()
    with open("log.txt", "w", encoding="utf-8") as f:
        for i in range(10):
            f.write(f"尚硅谷{i}\n")
            f.flush()
            time.sleep(1)


    print(f"主进程结束了{os.getpid()}")
5.锁操作
python 复制代码
from multiprocessing import Process, current_process,RLock
import time
import os

def func1(lock):
    for e in range(1, 15):
        with lock:
            print("乌")
            print("龟")
            print("跑")
        time.sleep(0.5)


def func2(lock):
    for e in range(1, 11):
        with lock:
            print("兔")
            print("子.")
            print("跑.")
        time.sleep(1)

if __name__ == '__main__':
    print(f"开始执行 进程id{os.getpid()}")
    lock = RLock()
    #  给进程设置名字 传递参数
    p1 = Process(target=func1,name="乌龟进程",args=(lock,))
    p2 = Process(target=func2,args=(lock,))
    # 申请创建子进程
    p1.start()

    p2.start()

    print("执行结束......")

2.4 方式二:继承Process类创建进程

多个进程执行相同的任务

python 复制代码
class Work(multiprocessing.Process):

    def run(self):
        print(f"hello{os.getpid()} {os.getppid()}")


if __name__ == '__main__':
    for i in range(10):
        w = Work()
        w.start()

2.5 方式三:进程池

当需要创建大量进程时,手动创建Process效率低,Pool会预先创建固定数量的进程,复用进程执行多个任务,减少进程创建 / 销毁的开销。

适用场景:任务数量多(如几十 / 上百个)、任务逻辑统一的场景(如批量计算、批量处理文件)。

创建进程池的语法格式:

python 复制代码
multiprocessing.Pool([processes[,initializer[,initargs[,maxtasksperchild[,context]]]]])

注意:使用时一般只指定 processes 参数。

  • processes:要使用的工作进程数量。如果 processes 为 None 则使用 os.cpu_count() 所返回的数值。

    • I/O 密集型任务的特点是:大部分时间花费在等待磁盘读取数据上,而不是 CPU 计算。过多的进程反而有可能导致资源竞争(如文件句柄、内存等),拖慢速度。
    • CPU密集型任务增加进程数量对效率有提升,但超过一定数量(物理CPU可用核数)会拖慢速度,上下文切换(Context Switching)开销增加。
  • initializer:每个工作进程启动时会调用的初始化函数。

    ​ 如果不为 None,则每个工作进程将会在启动时调用 initializer(*initargs)。

  • maxtasksperchild:每个工作进程在执行完指定数量的任务后,会被重启(退出并新建)。

    默认 maxtasksperchild 是 None,意味着工作进程寿与池齐。

  • context:可被用于指定启动的工作进程的上下文。通常一个进程池是使用函数 multiprocessing.Pool() 或者一个上下文对象的 Pool() 方法创建的。

  • initargs (tuple, 可选) 函数的参数元组。不需要参数,可以省略或传空元组 ()

Pool类的属性和方法:

  • map(func, args):一键批量提交任务,map会自动将参数列表args中的每个元素传给任务函数func,在返回结果前会阻塞主进程;
  • apply(func[, args[, kwds]]):使用 args 参数以及 kwds 命名参数同步调用 func , 在返回结果前会阻塞主进程。
  • apply_async(func[, args[, kwds[, callback[, error_callback]]]]):使用 args 参数以及 kwds 命名参数异步调用 func,并立即返回一个 AsyncResult 对象,不会阻塞。AsyncResult 对象可以通过get()或者 callback回调函数 获取结果

​ 可以将多个任务结果添加到列表内 然后遍历列表获取结果

  • close():阻止后续任务提交到进程池,当所有任务执行完成后,工作进程会退出。
  • terminate():不必等待未完成的任务,立即停止工作进程。当进程池对象被垃圾回收时,会立即调用 terminate()。
  • join():阻塞主进程,等待工作进程结束。调用 join() 前必须先调用 close() 或者 terminate()。

注意:从3.8版本后,进程池的进程默认是守护进程,所以需 join() 确保主进程等待。

特性 pool.map() pool.apply() pool.apply_async()
任务提交方式 批量提交(传入参数列表) 单个提交(传入单个参数) 单个提交(传入单个参数)
执行方式 并行执行(多进程同时处理) 同步执行(串行,等当前任务完成才下一个) 异步执行(并行,提交后立即返回,后台执行)
阻塞性 阻塞主进程(等所有任务完成才返回) 阻塞主进程(等当前单个任务完成才返回) 不阻塞主进程(返回任务对象,需get()阻塞取结果)
返回值 结果列表(按参数顺序) 单个任务的返回值 AsyncResult对象(需get()取结果)get() 的作用是阻塞等待 任务完成并获取返回结果
参数格式 map(函数, 列表) apply(函数, args=(参数,)) apply_async(函数, args=(参数,))
代码简洁度 最高(一键批量处理) 中等(需循环) 中等(需循环 + 收集任务对象)
灵活性 低(仅支持简单批量任务) 低(串行无并行优势) 高(支持回调函数、超时、动态参数)
适用场景 简单批量、参数列表固定的并行任务 几乎不用(串行失去进程池意义) 复杂场景(需回调、动态提交、并行控制)
1、示例1:map方法一键提交任务

map会自动将参数列表args中的每个元素传给任务函数func,在返回结果前会阻塞主进程;

按照提交的顺序返回结果

python 复制代码
import os
import time
from multiprocessing import Pool, Process, pool


def fun(x):
    print(x, os.getpid(),"父id",os.getppid())
    time.sleep(1)
    return x * x

if __name__ == '__main__':
    print(f"开始执行{os.getpid()}")
    p = Pool(4)
    list2 = [2, 3, 4, 5, 6, 7]
    #引起进程阻塞
    results = p.map(fun, list2)
    #拿到结果后才会继续执行
    print("--->",results)
    print(f"结束执行{os.getpid()}")
python 复制代码
import os
import time
from multiprocessing import Pool, Process, pool
def fun(x):
    print(x, os.getpid(),"父id",os.getppid())
    if x == 2:
        time.sleep(10)
    if x ==3:
        time.sleep(5)
    else:
        time.sleep(1)
    return f"返回参数{x}的结果{x * x}"

if __name__ == '__main__':
    print(f"开始执行{os.getpid()}")
    p = Pool(4)
    list2 = [2, 3, 4, 5, 6, 7]
    #引起进程阻塞
    results = p.map(fun, list2)
    #拿到结果后才会继续执行
    print("--->",results)
    print(f"结束执行{os.getpid()}")
python 复制代码
import multiprocessing
import time
import os

# 定义进程池要执行的核心任务函数
def count_file_chars(file_path):
    """
    计算单个文件的字符数(含空格、换行符)
    :param file_path: 文件路径
    :return: 元组(文件名, 字符数),方便主进程接收结果
    """
    try:
        print(f"正在处理文件:{file_path}")

        time.sleep(0.5) # 模拟更多耗时

        # 读取文件并计算字符数
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
            char_count = len(content)

        print(f"文件处理完成:{file_path}")

        # 返回带文件名的结果,方便后续整理
        return (os.path.basename(file_path), char_count)
    except Exception as e:
        # 异常处理:文件不存在/编码错误等情况
        return (os.path.basename(file_path), f"错误:{str(e)}")


if __name__ == "__main__":
    # 第一步:准备测试文件
    test_files = [r"document\python1.txt", r"document\python2.txt", r"document\python3.txt", r"document\python4.txt", r"document\python5.txt"]

    # 第二步:使用进程池批量计算字符数
    start_time = time.time()  # 记录开始时间,对比效率

    # 使用with语句,自动管理进程池的关闭,无需手动close()
    with multiprocessing.Pool(processes=2) as pool:
        # 批量提交任务:map方法接收「任务函数」和「参数列表」
        # 进程池会自动分配进程处理每个文件,结果按参数顺序返回
        results = pool.map(count_file_chars, test_files)
        # 因为map方法会等待所有任务完成,所以不需要join()

    # 第三步:整理并打印结果
    print("文件字符数统计结果:")
    for file_name, count in results:
        print(f"{file_name}: {count}")

    # 计算总耗时
    total_time = round(time.time() - start_time, 2)
    print(f"\n总耗时:{total_time} 秒")
2、示例2:apply逐个提交任务
同步执行任务,并获取返回值,但会阻塞主进程
python 复制代码
import os
import time
from multiprocessing import Pool, Process, pool

def fun(x):
    print(x, os.getpid(), "父id", os.getppid())
    if x == 2:
        time.sleep(10)
    if x == 3:
        time.sleep(5)
    else:
        time.sleep(1)
    return f"返回参数{x}的结果{x * x}"

if __name__ == '__main__':
    print(f"开始执行{os.getpid()}")
    p = Pool(4)
    list2 = [2, 3, 4, 5, 6, 7]
    for e in list2:
        r = p.apply(fun, (e,))
        print(r)
    #执行完所有的任务才会向下有阻塞效果
    print(f"结束执行{os.getpid()}")
3、示例3:apply_async逐个提交任务
python 复制代码
import os
import time
from multiprocessing import Pool


def fun(x):
    print(x, os.getpid(), "父id", os.getppid())
    if x == 2:
        time.sleep(1)
    if x == 3:
        time.sleep(2)
    else:
        time.sleep(3)
    return f"返回参数{x}的结果{x * x}"


if __name__ == '__main__':
    print("主进程开始",os.getpid())
    start = time.time()
    pool = Pool(3)
    list2 = [2, 3, 4, 5]

    list3=[]
    for e in list2:
        # 获取的是 异步提交结果
        c=pool.apply_async(fun,(e,))
        #添加到集合内
        list3.append(c)

    print("任务提交结束",list3)
    for e in list3:
        #调用get() 阻塞主线程 等待任务结果
        print(e.get())

    print("主进程结束", time.time() - start)
4、示例4:apply_async结合回调函数

​ 谁先执行完 谁返回结果

python 复制代码
import os
import time
from multiprocessing import Pool, Process, pool
def fun(x):
    print(x, os.getpid(), "父id", os.getppid())
    if x == 2:
        time.sleep(5)
    elif x == 3:
        time.sleep(2)
    else:
        time.sleep(1)
    return f"返回参数{x}的结果{x * x}"


def fun2(x):
    print(x)

if __name__ == '__main__':
    print(f"开始执行{os.getpid()}")
    start = time.time()
    # 存储异步任务对象
    async_task_list = []
    list2 = [2, 3, 4, 5, 6, 7]

    with Pool(4) as p:
        for e in list2:
            # 返回一个异步对象
            r = p.apply_async(fun, [e],callback=fun2)
            async_task_list.append(r)
            print("任务已经提交", r)

        p.close()#不再接收新的任务
        p.join()

    print(f"结束执行{os.getpid()}", time.time()-start)

2.6 进程间通信

1、进程之间不共享数据
python 复制代码
"""
    主进程、不同子进程之间都不共享全局变量
"""
import multiprocessing
import os

# 向list中添加10个元素
def func(num_list):
    for i in range(10):
        num_list.append(i)
        print(f"pid = {os.getpid()}, num_list_id={id(num_list)}, num_list = {num_list}")

if __name__ == "__main__":
    num_list = []
    p1 = multiprocessing.Process(target=func, args=(num_list,))
    p2 = multiprocessing.Process(target=func, args=(num_list,))
    
    p1.start()
    p2.start()

    p1.join()
    p2.join()
    print(f"主进程pid = {os.getpid()}, num_list_id={id(num_list)}, num_list = {num_list}")
2、使用 multiprocessing.Queue 通信

进程拥有独立的内存空间,默认无法共享数据,Queue 和 Pipe 是multiprocessing模块提供的两种进程间通信(IPC) 机制,本质是操作系统层面提供的 "数据传输通道",让不同进程能通过这个通道交换数据。

Queue 就像一个进程安全的 "快递柜"

  • 一个进程(生产者)往 "快递柜" 里放数据,另一个进程(消费者)从 "快递柜" 里取数据;
  • 自带锁机制,支持多进程同时读写,不会出现数据错乱(线程 / 进程安全);
  • 遵循FIFO(先进先出) 原则,先放进去的数据先取出来。
  • 默认队列是无限大小的,可以通过 maxsize 参数限制。

Queue的方法:

  • qsize():返回队列的大致长度。由于多线程或者多进程的上下文,这个数字是不可靠的。
  • empty():如果队列是空的返回 True。由于多线程或多进程的环境,该状态是不可靠的。
  • full():如果队列是满的返回 True。由于多线程或多进程的环境,该状态是不可靠的。
  • put(obj, block\[, timeout]):将 obj 放入队列。
    • 如果可选参数 block 是 True(默认值)而且 timeout 是 None(默认值),将会阻塞当前进程,直到有空的缓冲槽。如果 timeout 是正数,将会在阻塞了最多 timeout 秒之后还是没有可用的缓冲槽时抛出 queue.Full 异常。
    • 反之(block 是 False 时),仅当有可用缓冲槽时才放入对象,否则抛出 queue.Full 异常(在这种情形下 timeout 参数会被忽略)。
  • put_nowait(obj):相当于 put(obj, False)。
  • get(block\[, timeout]):从队列中取出并返回对象。(注意:put一次只能get一次)
    • 如果可选参数 block 是 True (默认值)而且 timeout 是 None(默认值),将会阻塞当前进程,直到队列中出现可用的对象。如果 timeout 是正数,将会在阻塞了最多 timeout 秒之后还是没有可用的对象时抛出 queue.Empty 异常。
    • 反之(block 是 False 时),仅当有可用对象能够取出时返回,否则抛出 queue.Empty 异常(在这种情形下 timeout 参数会被忽略)。
  • get_nowait():相当于 get(False)。

Windows vs Unix 系统差异:在 Windows 系统上,进程创建使用 spawn 方式,而 Unix 系统默认使用 fork,这可能导致行为不一致

序列化限制:队列依赖 pickle 模块进行对象序列化,在不同 Python 版本间可能存在兼容性差异,只有可序列化的对象才能通过队列传递,如文件对象、线程锁等无法使用

示例代码

python 复制代码
"""
    使用queue实现进程间通信
"""
# 间隔随机时间向queue中放入随机数
def put_in_queue_func(queue):
    current_process = multiprocessing.current_process()
    while True:
        data = random.randint(1, 50)
        queue.put(data)
        print(f"{current_process.name}已放入数据:{data}")
        time.sleep(random.random())


# 间隔随机时间从queue中取出数据
def get_from_queue_func(queue):
    current_process = multiprocessing.current_process()
    while True:
        data = queue.get()
        print(f"\t{current_process.name}拿到数据:", data)
        time.sleep(random.random())


if __name__ == "__main__":
    queue = multiprocessing.Queue()
    p1 = multiprocessing.Process(name="p1", target=put_in_queue_func, args=(queue,))
    p2 = multiprocessing.Process(name="p2", target=get_from_queue_func, args=(queue,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
3、进程池的进程使用Manager().Queue通信

Manager().Queue进程池(Pool)场景下的专用队列,解决了普通 Queue 不支持进程池的问题。Manager 不仅能创建 Queue,还能创建其他共享数据结构,用法类似:manager.list()、manager.dict()、manager.Lock()等。

示例1

python 复制代码
"""
    进程池中的进程之间使用multiprocessing.Manager().Queue()通信
"""
import time
import random
import multiprocessing

# 间隔随机时间向queue中放入随机数
def put_in_queue_func(queue):
    while True:
        queue.put(random.randint(1, 50))
        time.sleep(random.random())

# 从queue中取出数据
def get_from_queue_func(queue):
    while True:
        print("=" * queue.get())

if __name__ == "__main__":
    queue = multiprocessing.Manager().Queue()
    pool = multiprocessing.Pool(2)
    pool.apply_async(put_in_queue_func, (queue,))
    pool.apply_async(get_from_queue_func, (queue,))
    pool.close()
    pool.join()

示例2

python 复制代码
import os
import time
import random
import multiprocessing

# 定义进程池要执行的核心任务函数
def count_file_chars(file_path,queue):
    """
    计算单个文件的字符数(含空格、换行符)
    :param file_path: 文件路径
    """
    try:
        print(f"正在处理文件:{file_path}")
        # 读取文件并计算字符数
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
            char_count = len(content)

        print(f"文件处理完成:{file_path}")
        queue.put(f"{os.path.basename(file_path)}的字符数量为:{char_count}")
    except Exception as e:
        # 异常处理:文件不存在/编码错误等情况
        queue.put(f"{os.path.basename(file_path)}的错误:{str(e)}")


if __name__ == "__main__":
    # 第一步:准备测试文件
    test_files = [r"document\python1.txt", r"document\python2.txt", r"document\python3.txt", r"document\python4.txt", r"document\python5.txt"]

    # 第二步:使用进程池批量计算字符数
    # 1.创建进程池
    pool = multiprocessing.Pool(processes=2)
    # 2.创建管理器 + Manager队列。注意:不是普通Queue
    queue = multiprocessing.Manager().Queue()
    # 3.遍历文件列表,逐个提交异步任务
    for file_path in test_files:
        pool.apply_async(count_file_chars, (file_path,queue))
    # 4. 关闭进程池,等待所有任务完成
    pool.close()
    pool.join()

    # 第三步:获取结果
    while not queue.empty():
        print(queue.get())
4、使用multiprocessing.Pipe双向通信

Pipe 就像一根双向的 "水管"

  • 创建 Pipe 时会返回两个 "端口"(conn1conn2),数据可以从一端进、另一端出;
  • 支持双向通信 (默认):conn1 可以写也可以读,conn2 同理;
    • conn.send(数据):写数据
    • conn.recv():读数据
  • 无内置锁机制,多进程同时读写可能出现数据错乱(需手动加锁);
  • 比 Queue 更轻量,效率更高,但安全性稍低。
python 复制代码
"""
1.
  进程池 读取 5个文件 获取每一个文件的大小 并返回
  len(read())
  返回 python1.txt 字符数量是 xxx
  map
2.  
A进程随机发送一个数字 [1,100]
B进程随机发送一个字母 [A-Z]
两个进程互相接收对方发送的信息,将收到的数据写到磁盘内
"""
import random
import string
from multiprocessing import Process,Pipe

def send_char(c1):
    """
    发送字母 接收数字 将数字存储
    :param c1:
    :return:
    """
    with open("./data/send_char.txt", "w", encoding="utf-8") as f:
        while True:
            #发送
            upper_str = string.ascii_uppercase
            message = random.choice(upper_str)
            c1.send(message)
            print("send_char 要发送:", message)
            #接收
            data = c1.recv()
            f.write(f"{data}\n")
            f.flush()



def send_num(c2):
    """
    发送数字 接收字母 将字母存储
    :param c2:
    :return:
    """
    with open("./data/send_num.txt", "w", encoding="utf-8") as f:
        while True:
            #发送数据  [1,100] 随机数字
            num = random.randint(1,100)
            print("send_num 要发送:",num)
            c2.send(num)

            #接收数据并写出
            data = c2.recv()
            f.write(f"{data}\n")
            f.flush()


if __name__ == '__main__':
    #创建线程通信对象
    c1,c2 = Pipe()
    #创建子进程
    p1 = Process(target=send_char, args=(c1,))
    p2 = Process(target=send_num, args=(c2,))
    #启动子进程
    p1.start()
    p2.start()

3 线程

3.1 什么是线程

  • 线程是处理器任务调度和执行的基本单位。

  • 一个进程至少有一个线程,也可以运行多个线程。

  • 多个线程之间可共享数据。

  • 线程运行出错异常后,如果没有捕获,会导致整个进程崩溃。

  • 多线程是指在同一进程中同时执行多个任务。

3.2 创建线程的3种方式

  • threading.Thread()直接创建线程对象
  • 继承threading.Thread类
  • concurrent.futures.ThreadPoolExecutor()线程池方式
创建方式 灵活性 资源开销 适用场景 核心优缺点
方式 1:直接使用 threading.Thread 中(仅能指定单个函数) 中(每次创建新线程) 简单的单任务场景、一次性少量线程(如 2-5 个) 优点:代码极简、入门友好;缺点:不适合批量创建,参数多时代码可读性差
方式 2:继承 threading.Thread 并重写run() 高(可封装属性 / 方法、重写生命周期) 中(每次创建新线程) 线程需要封装状态 / 方法、逻辑复杂的场景(如自定义线程行为) 优点:封装性好、可扩展;缺点:代码量稍多,新手易出错
方式 3:ThreadPoolExecutor 线程池 中高(支持批量提交、获取返回值) 低(线程复用,避免频繁创建 / 销毁) 批量任务、高频创建 / 销毁线程的场景(如爬虫、接口并发请求) 优点:资源利用率高、支持获取返回值、无需手动管理线程生命周期;缺点:依赖concurrent.futures模块,简单场景略繁琐

3.3 方式一:直接创建Thread类对象

语法格式:

python 复制代码
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
  • group:应为 None,保留给将来实现 ThreadGroup 类的扩展使用。
  • target:用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。
  • name:线程名称。 在默认情况下,会以 "Thread-N" 的形式构造唯一名称,其中 N 为一个较小的十进制数值,或是 "Thread-N (target)" 的形式,其中 "target" 为 target.name,如果指定了 target 参数的话。
  • args:用于发起调用目标函数的参数列表或元组。 默认为 ()。
  • kwargs:用于调用目标函数的关键字参数字典。默认是 {}。
  • daemon:True 或 False 来设置该线程是否为守护模式。如果是 None (默认值),线程将继承当前线程的守护模式属性。
python 复制代码
"""
    使用threading.Thread创建线程
"""
import time
import threading

def odd_print():
    """
     每隔一秒打印奇数
    """
    odd = 1
    while True:
        print(threading.current_thread().name, "奇数:", odd)
        odd += 2
        time.sleep(1)

def even_print():
    """
         每隔一秒打印偶数
        """
    even = 0
    while True:
        print(threading.current_thread().name, "偶数:", even)
        even += 2
        time.sleep(1)


if __name__ == "__main__":
    t1 = threading.Thread(target=odd_print, name="线程1")
    t2 = threading.Thread(target=even_print, name="线程2")
    t1.start()
    t2.start()

3.4 方式二:继承Thread类创建线程

Thread 的属性和方法与其他常用方法

  • name:线程的名称。
  • daemon:线程是否为守护线程。
  • ident:线程标识符。
  • native_id:此线程的线程id(tid),由 OS(内核)分配。
  • start():启动线程,调用线程的 run() 方法。
  • run():定义线程的行为,默认调用传入的 target 对象。
  • join(timeout=None):阻塞主线程,直到当前线程运行完成或达到超时时间。
  • is_alive():线程是否在运行。
  • threading.enumerate():查看都有哪些线程。
  • threading.current_thread():返回当前线程实例。
python 复制代码
"""
    继承Thread类创建线程
"""
import threading
import time

class OddThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        odd = 1
        while True:
            print(threading.current_thread().name, "奇数:", odd)
            odd += 2
            time.sleep(1)

class EvenThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        even = 0
        while True:
            print(threading.current_thread().name, "偶数:", even)
            even += 2
            time.sleep(1)

if __name__ == "__main__":
    t1 = OddThread("线程1")
    t2 = EvenThread("线程2")
    t1.start()
    t2.start()

3.5 方式三:线程池

ThreadPoolExecutor 是 concurrent.futures 模块中的线程池实现,它允许我们轻松地提交任务到线程池,并管理任务的执行和结果。

语法格式:

python 复制代码
concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix="", initializer=None, initargs=())
  • max_workers:线程池的最大线程数(默认取决于系统资源)。
  • thread_name_prefix:线程名称前缀。
  • initializer:可选的初始化函数。
  • initargs:传递给初始化函数的参数。

线程池的常用方法

  • submit(func, *args, **kwargs):提交一个任务到线程池,返回一个 Future 对象。可使用 Future.result() 获取任务结果。

    • func:线程任务函数
    • *args, **kwargs:给func函数的参数
  • map(func, *iterables, timeout=None, chunksize=1):一键提交任务,多个任务在线程池中并行执行。

    • func:线程任务函数
    • Iterables:传递给目标函数的参数序列,多个序列会按位置一一对应传参。例如:func函数需要2个参数,参数序列1,2,3,10,20,30,第一个任务的参数是1,10;第二个任务的参数是2,20;第三个任务的参数是3,30。多个序列长度需要一致,否则按短的截断。
    • timeout(可选):设置超时时间,超过时间未返回会抛出 TimeoutError
    • chunksize :(可选):仅对大数据量迭代器有效,指定每次分给线程的参数块大小(提升效率)
  • shutdown(wait=True, cancel_futures=False):关闭线程池,等待所有任务完成。

    • wait 表示是否等待线程池中的所有线程完成任务。
    • cancel_futures 表示是否取消尚未开始的任务。
1、示例1:submit方法逐个提交任务
py 复制代码
"""
    使用线程池完成多线程
"""
import concurrent.futures
import time

# 定义线程任务函数:处理奇数
def odd_add(tname):
    odd = 1
    sum = 0
    while odd<=100:
        print(tname, "奇数:", odd)
        sum += odd
        odd += 2
        time.sleep(0.1)
    return sum

# 定义线程任务函数:处理偶数
def even_add(tname):
    even = 0
    sum = 0
    while even<=100:
        print(tname, "偶数:", even)
        sum += even
        even += 2
        time.sleep(0.1)
    return sum

# 线程任务函数:处理字母
def char_append(tname):
    str = ""
    letter = 'a'
    while letter<='z':
        print(tname, "字母:", letter)
        str +=  letter
        letter = chr(ord(letter) + 1)
        time.sleep(0.1)
    return  str

if __name__ == "__main__":
    # 使用 with 语句来确保线程被迅速清理
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        future1 = executor.submit(odd_add, "线程1")
        future2 = executor.submit(even_add, "线程2")
        future3 = executor.submit(char_append, "线程3")
        result1 = future1.result()
        result2 = future2.result()
        result3 = future3.result()
        print("线程1的结果:", result1)
        print("线程2的结果:", result2)
        print("线程3的结果:", result3)
2、示例2:map方法一键提交任务
python 复制代码
from concurrent.futures import ThreadPoolExecutor
import time

# 定义任务函数
def task(x, y):
    time.sleep(1)
    return f"x={x}, y={y},结果:{x + y}"
if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=2) as executor:
        # 两个参数序列,长度需一致(否则截断到短的长度)
        params1 = [10, 20, 30]
        params2 = [1, 2, 3]
        # 传入多个参数序列
        results = executor.map(task, params1, params2)

        for res in results:
            print(res)

3.6 互斥锁

1、线程安全问题
python 复制代码
"""
    线程不安全
"""
import threading
import time

# 售票函数
def sale_ticket():
    global ticket
    while True:
        if ticket > 0:
            time.sleep(0.5)
            print(threading.current_thread().name, "正在出售第", ticket, "张票")
            ticket -= 1
        else:
            print(threading.current_thread().name, "票已售完")
            break

if __name__ == "__main__":
    ticket = 10
    threads = [threading.Thread(target=sale_ticket, name=f"线程{i}") for i in range(3)]
    [t.start() for t in threads]
    [t.join() for t in threads]

运行结果:

复制代码
线程0 正在出售第 10 张票
线程2 正在出售第 9  张票
线程1 正在出售第 9 张票
线程0 正在出售第 7 张票
线程1 正在出售第 6 张票
线程2 正在出售第 6 张票
线程0 正在出售第 4 张票
线程1 正在出售第 3 张票
线程2 正在出售第 2 张票
线程0 正在出售第 1 张票
线程0 票已售完
线程1 正在出售第 0 张票
线程1 票已售完
线程2 正在出售第 -1 张票
线程2 票已售完

发现上述结果中有重复票,有负数票。

分析原因:因为多个线程之间共享数据会存在线程安全的问题。CPU切换会发生在任意2个指令之间。

2、互斥锁

某个线程要使用共享数据(包含修改操作),先将其锁定,此时其他线程不能使用。直到该线程释放资源,将资源的状态变成"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

  • 可以通过 threading.Lock() 创建互斥锁。
  • 使用 lock.acquire(blocking=True, timeout=-1) 来获取锁(blocking 如果为 True,线程会阻塞直到获取到锁。如果为 False,线程立即返回。获取锁成功返回 True,否则返回 False。timeout 为等待的超时时间,单位为秒。如果超时仍未获取到锁,则返回 False。)。
  • 使用 lock.release() 释放锁。
python 复制代码
"""
    互斥锁保证线程安全
"""
import threading
import time

# 售票函数
def sale_ticket():
    global ticket
    thread_name = threading.current_thread().name
    current_ticket = 0 # 当前线程正在出售的票。保证正确初始化
    while True:
        lock.acquire()
        try:
            if ticket > 0:
                time.sleep(0.5)
                current_ticket = ticket
                ticket -= 1
            else:
                break
        finally:
            lock.release()
        print(f"{thread_name}卖出第{current_ticket}张票")


if __name__ == "__main__":
    ticket = 10
    lock = threading.Lock()
    threads = [threading.Thread(target=sale_ticket, name=f"线程{i}") for i in range(3)]
    [t.start() for t in threads]
    [t.join() for t in threads]

13.3.7 GIL

Python 全局解释器锁(Global Interpreter Lock, 简称 GIL)是一个锁,同一时间只允许一个线程保持 Python 解释器的控制权,这意味着在任何时间点都只能有一个线程处于执行状态。执行单线程程序时看不到 GIL 的影响,但它可能是 CPU 密集型和多线程代码中的性能瓶颈。GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

Python于1991年诞生,从操作系统没有线程概念的时代就已经存在了。由于物理上的限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了利用多核,Python开始支持多线程。而为了解决多线程之间数据完整性和状态同步,于是有了GIL,GIL 提供了线程安全的内存管理。

GIL 的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。我们可能会想 GIL只要释放的勤快效率也不会差,至少也不会比单线程的效率差。理论上是这样。

但实际上,Python为了让各个线程能够平均利用CPU时间,会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。从释放 GIL 到获取 GIL 之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到 GIL 了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着 GIL 执行。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。

上述实现方式是较为原始的,Python的每个版本中也在逐渐改进GIL和线程调度之间的互动关系。例如先尝试持有GIL在做线程上下文切换,在IO等待时释放GIL等尝试。但是无法改变的是GIL的存在使得操作系统线程调度的这个本来就昂贵的操作变得更奢侈了。

总之,当你的程序需要进行大量的CPU计算时,GIL会成为性能的瓶颈。即使你有多个线程,GIL也会阻止它们在多个CPU核心上并行执行。实际上,多个线程会轮流获取GIL,这样就不能真正并行地使用多个处理器核心。而对于涉及I/O操作(如文件读写、网络请求等)的程序,GIL的影响较小。因为在I/O操作时,线程会释放GIL,其他线程可以在此时执行,这使得多线程在I/O密集型任务中能更有效地并发。

13.4 进程和线程的对比

13.4.1 区别

1、资源分配

进程拥有独立的内存空间和系统资源,每个进程都有自己的代码段、数据段和堆栈等。而线程共享所属进程的内存空间和资源,同一进程内的线程之间可以直接访问共享内存。

2、开销

创建进程需要分配独立的内存、打开文件等系统资源,开销较大。

创建线程只需在所属进程的内存空间内进行少量资源分配,开销较小。

3、并发性

在多核心 CPU 环境下,进程和线程都可以异步执行但进程之间的异步是真正的异步(每个进程在不同核心上同时执行),而线程之间的异步在单核心 CPU 上是通过时间片轮转实现的 "伪异步"(在同一时刻只有一个线程执行),在多核心 CPU 上可以实现异步。但是在Cpython中,因为GIL的存在,也不是真正的异步

4、独立性

进程之间相互独立,一个进程的崩溃通常不会影响其他进程。而同一进程内的线程之间相互影响,一个线程出现问题可能导致整个进程崩溃。

5、通信

进程间通信相对复杂,需要使用特殊的机制,如管道、消息队列、共享内存等。

线程间通信相对简单,因为它们共享内存,可以直接访问共享变量。

4.2 使用场景

1、适合使用多线程的情况:

I/O 密集型任务:如网络请求、文件读写等。线程共享内存,切换开销小,在等待 I/O 操作完成的时间内可以切换到其他线程执行,提高整体效率。例如一个程序需要同时从多个网站下载数据,使用多线程可以在等待网络响应时执行其他下载任务。

对资源共享要求高:线程间共享内存,方便数据共享和通信。例如在一个图形界面程序中,多个线程需要共享界面数据并进行实时更新。

2、适合使用多进程的情况:

CPU 密集型任务:多进程可以利用多核心 CPU 实现真正的并行计算,充分发挥硬件性能。例如进行复杂的科学计算、数据处理等任务,每个进程在不同核心上独立计算,提高计算速度。

需要隔离的任务:进程相互独立,一个进程崩溃不会影响其他进程。对于一些可能出现异常或不稳定的任务,使用多进程可以保证系统的稳定性。例如运行多个独立的服务,每个服务作为一个进程,避免一个服务出错影响其他服务。

相关代码

1.创建进程

python 复制代码
import time,os
from multiprocessing import Process,current_process

def f1(a):
    for i in range(5):
        a -=1
        time.sleep(1)
        print(f"{current_process().name} 跑第 {i+1}次,进程id:{os.getpid()},函数参数:{a}")

def f2(x,y):
    for i in range(5):
        x+=1
        y+=1
        time.sleep(1)
        print(f"{current_process().name} 跑第 {i+1}次,进程id:{os.getpid()},函数的参数:{x=} {y=}")

if __name__ == '__main__':
    p1 = Process(target=f1,name="巴西龟进程",args=(33,))
    # p2 = Process(target=f2,name="长毛兔进程",args=[10,20])
    p2 = Process(target=f2,name="长毛兔进程",kwargs={"y":2,"x":1})

    p1.start()
    p2.start()

2.进程锁的使用

python 复制代码
from multiprocessing import Lock
复制代码
定义r = Lock()
传参
r.acquire() -- 获取锁,上锁
r.release()  -- 释放锁
python 复制代码
import time
from multiprocessing import Process,Lock

def f1(l1):
    for i in range(10):
        l1.acquire() # 获取锁
        print("乌")
        print("龟")
        time.sleep(0.5)
        print("跑")
        l1.release()# 释放锁

def f2(l2):
    for i in range(10):
        with l2:
            print("兔")
            print("子")
            time.sleep(0.5)
            print("跳")

if __name__ == '__main__':
    r = Lock()

    p1 = Process(target=f1,args=(r,))
    p2 = Process(target=f2,kwargs={"l2":r})

    p1.start()
    p2.start()

练习

1.主进程内 向一个文件中写入数据,创建守护进程读取文件的行数

python 复制代码
from multiprocessing import Process
import time

def my_readline():
    while True:
        with open("./a.txt","r",encoding="utf-8") as f:
            # readlines() 将文件内的数据读取到列表内
            f.seek(0)  # 将光标重置,这样每次可以从头开始
            lines = len(f.readlines())
            print(f"p1进程进行读取,获取行数为{lines}")
            time.sleep(1.5)
def my_readline2():
    while True:
        with open("./a.txt","r",encoding="utf-8") as f:
            # readlines() 将文件内的数据读取到列表内
            f.seek(0)  # 将光标重置,这样每次可以从头开始
            lines = len(f.readlines())
            print(f"p2进程进行读取,获取行数为{lines}")
            time.sleep(1)


if __name__ == '__main__':
    p1 =Process(target=my_readline,daemon=True)
    p2 =Process(target=my_readline2,daemon=True)
    p1.start()
    p2.start()

    with open("./a.txt","a",encoding="utf-8") as f:
        for i in range(5):
            print(f"主进程写入,第{i+1}次写入一行数据")
            f.write("kyrie\n")
            f.flush()
            time.sleep(2)

2.进程间通信 -- Pipe

复制代码
简单实现:
	A进程随机发送一个数字 [1,100]
	B进程随机发送一个字母 [A-Z]
	两个进程互相接收对方发送的信息,将收到的数据写到磁盘内

代码:

python 复制代码
from multiprocessing import Process,Pipe,current_process
import random ,string

def f1(c1):
    l = random.choice(string.digits)
    c1.send(l)
    print(f"{current_process().name} 发送了{l}")
    data = c1.recv()
    print(f"c1接收的数据是:{data}")
    with open("./num.txt","a",encoding="utf-8") as f:
        f.write(data)
        f.flush()

def f2(c2):
    x = random.choice(string.ascii_uppercase)
    c2.send(x)
    print(f"{current_process().name} 发送了{x}")
    data = c2.recv()
    print(f"c2接收的数据:{data}")
    with open("./num2.txt","a",encoding="utf-8") as f:
        f.write(data)
        f.flush()

if __name__=='__main__':
    c1,c2=Pipe()
    p1=Process(target=f1,args=(c1,))
    p2=Process(target=f2,args=(c2,))

    p1.start()
    p2.start()

运行结果:

因为不是覆盖写,所以下面测试结果文件里面有之前测试的遗留数据

3.进程池 读取 5个文件 获取每一个文件的大小 并返回

复制代码
len(read())
返回 python1.txt 字符数量是 xxx
map
list1=[r"./data/python1.txt",]

代码实现:

python 复制代码
import os
from multiprocessing import Pool

def func1(x):
    print(f"参数{x}开始执行", os.getpid())


if __name__ == '__main__':
    print("主进程开始")
    pool = Pool(3)
    # todo 批量处理任务 体系进程的复用 阻塞主进程执行

    list1 = [os.path.getsize(os.path.join(root,file))  for root,dir,files in os.walk(r"./测试用例") for file in files]
    print(list1)
    print(sum(list1))

    r = pool.map(func1,list1)
    print(r)
	
	  # todo 单独任务的处理 同步操作
    #同步操作apply
    # for e in list1:
    #     r= pool.apply(func1,args=(e,))
    #     print(r)


    # todo 单独任务的处理 异步操作
    # result = []
    # for e in list1:
    #     # 拿到应用结果
    #     r = pool.apply_async(func1, args=(e,))
    #     result.append(r)
    # # result = [pool.apply_async(func1, args=(i,)) for i in list1]
    #
    # for e in result:
    #     print(e.get())
    #
    # print("主进程结束")

测试结果:

这里再当目录下新建了一个目录,存放五个测试用例

4. 线程池实现买票

复制代码
4个窗口卖100张票
不能有重票和负票

代码实现:

因为GIL的存在,测试用例里面依旧是单线程,但是会切换进程执行程序

python 复制代码
from threading import Thread, Lock, current_thread
import time

def sell_ticket(l):
    global ticket

    while True:
        with lock:
            if ticket <= 0:
                break
            ticket -= 1 # 获取值 更改  赋值
            time.sleep(0.1)
            print(f"{current_thread().name} 卖了一张票 还有张 {ticket} 票")

if __name__ == '__main__':
    lock = Lock()
    ticket = 1000
    t1 = Thread(target=sell_ticket, name="窗口一",args=(lock,))
    t2 = Thread(target=sell_ticket, name="窗口二",args=(lock,))
    t3 = Thread(target=sell_ticket, name="窗口三",args=(lock,))
    t4 = Thread(target=sell_ticket, name="窗口四",args=(lock,))

    t1.start()
    t2.start()
    t3.start()
    t4.start()
相关推荐
自小吃多13 分钟前
PCB Editor软件差分对添加与设置笔记
笔记·嵌入式硬件
Jialu.31 分钟前
中文 NLP 模型部署实战:FastAPI 接口 + Streamlit 看板
人工智能·python·自然语言处理·fastapi
高亦真42 分钟前
今天是学习嵌入式的第34天
linux·学习·算法
2501_931819701 小时前
桂柳方言标注公司错误标注溯源追踪体系搭建诉求落地 桂柳方言标注公司学习信实翻译全链路追踪技术
学习·语音识别
Capricorn19881 小时前
防编造架构实战:对比 Gemini Notebook 解析知芽 Notebook Skill 的工程实现
人工智能·笔记·elasticsearch·架构·知识图谱·论文笔记
Asum1ta2 小时前
K8s 学习环境搭建:Python 与 PyCharm 开发环境配置指南
python·学习·kubernetes
知识汲取者2 小时前
FastAPI 学习教程(写给想学 FastAPI 的 Java 工程师的)
python·学习·fastapi
飞Link2 小时前
动作方法中的分割与过度分割方法全解析(含代码实战与踩坑案例)
人工智能·python·算法·计算机视觉
Shell运维手记2 小时前
Linux mdadm 软 RAID + LVM 完整综合笔记
linux·运维·笔记·5g