进程、线程、协程

进程(Process)

简单来说,进程是操作系统分配资源的基本单位。

单进程

  • 当你双击一个 Python 程序:

    bash 复制代码
    python main.py
  • 操作系统会创建一个进程:

    bash 复制代码
    操作系统
    │
    └── Python进程
          ├── 代码
          ├── 内存
          ├── 文件句柄
          ├── 网络连接
          └── 线程
  • 每个进程拥有独立的:

    • 内存空间
    • 全局变量
    • 文件描述符
    • 网络资源
  • 例如:

    python 复制代码
    a = 100
  • 进程A:

    python 复制代码
    a = 100
  • 进程B:

    python 复制代码
    a = 100

    看起来一样,但实际上存放在不同内存中

多进程

  • 例如

    python 复制代码
    import multiprocessing
    import os
    
    # 进程要执行的任务
    def task(num):
        print(f"进程{num} 启动,PID:{multiprocessing.current_process().pid},PPID:{os.getppid()}")
    
    if __name__ == "__main__":
        process_list = []
        # 创建3个进程
        for i in range(1, 4):
            p = multiprocessing.Process(target=task, args=(i,))
            process_list.append(p)
            p.start()  # 启动进程
    
    # 主进程等待3个子进程全部执行完毕
    for p in process_list:
        p.join()
    
    print(f"所有3个子进程运行结束,主进程退出,主进程PID:{os.getpid()}")
    
    # 打印结果:
    # 进程1 启动,PID:20498,PPID:20496
    # 进程2 启动,PID:20499,PPID:20496
    # 进程3 启动,PID:20500,PPID:20496
    # 所有3个子进程运行结束,主进程退出,主进程PID:20496
  • 优点

    • 真正并行

      bash 复制代码
      CPU核心1 -> 进程1
      
      CPU核心2 -> 进程2
      
      CPU核心3 -> 进程3
  • 缺点

    • 创建成本高

      bash 复制代码
      例如:
      
      内存:
      100MB
      
      启动:
      几十毫秒

线程(Thread)

简单来说,线程是 CPU 调度的基本单位。

单线程

  • 线程存在于进程内部。

  • 例如:

    python 复制代码
    Python进程
    │
    ├── 主线程
    ├── 线程1
    ├── 线程2
    └── 线程3
  • 线程共享同一个进程的资源:

    bash 复制代码
    进程内存
    │
    ├── 全局变量
    ├── 堆
    ├── 文件句柄
    └── socket
  • 所有线程都能访问

  • 例如:

    python 复制代码
    count = 0
  • 线程1:

    python 复制代码
    count += 0
  • 线程2:

    python 复制代码
    count += 0

    都在修改同一个变量。

多线程

  • 例如

    python 复制代码
    from threading import Thread
  • 创建

    bash 复制代码
    Python进程
    │
    ├── Thread1
    ├── Thread2
    └── Thread3
  • 优点

    • 创建快
    • 切换快
    • 共享数据方便
  • 缺点

    • 多个线程修改同一个变量时容易出问题

      python 复制代码
      count += 1
    • 可能变成

      bash 复制代码
      线程1读取 count=0
      
      线程2读取 count=0
      
      线程1写回 1
      
      线程2写回 1
    • 结果

      bash 复制代码
      应该是2
      
      实际是1
    • 线程安全问题(Race Condition)

      • 可以使用互斥锁解决,在这里不过多赘述,主要研究协程和事件循环

进程和线程的关系

可以把它想成公司:

  • 进程

    bash 复制代码
    公司A
    公司B
    公司C

    每个公司独立。

  • 线程

    bash 复制代码
    公司A
    │
    ├── 员工1
    ├── 员工2
    └── 员工3

    员工共享公司的资源。

所以,层级关系是这样的:

bash 复制代码
进程
 └── 线程
      └── 协程

协程(Coroutine)

  • 线程切换
    • 操作系统决定
  • 协程切换
    • 程序员决定

    • 例如:

      python 复制代码
      await asyncio.sleep(1)
      # 这里主动告诉事件循环 我先暂停 你去执行别的协程

协程和线程的关系

  • 线程

    bash 复制代码
    线程1
    线程2
    线程3
    
    操作系统切换
  • 协程

    bash 复制代码
    协程1
    协程2
    协程3
    
    EventLoop切换
  • 线程切换需要(成本高)

    bash 复制代码
    保存CPU寄存器
    切换内核态
    恢复现场
  • 协程切换只需要(成本极低)

    bash 复制代码
    保存函数执行位置

操作系统、进程、线程和协程四者关系图

bash 复制代码
操作系统
│
├── 进程A
│    │
│    ├── 线程1
│    │     │
│    │     ├── 协程1
│    │     ├── 协程2
│    │     └── 协程3
│    │
│    └── 线程2
│
└── 进程B
      │
      └── 线程1