第111集:进程与线程概念

学习目标

  1. 理解进程和线程的基本概念
  2. 掌握进程与线程的区别与联系
  3. 了解并行与并发的概念
  4. 理解多进程与多线程的优缺点
  5. 掌握Python中进程与线程的基本使用方法

一、进程与线程的基本概念

1.1 什么是进程

进程(Process)是操作系统进行资源分配和调度的基本单位,是程序在执行过程中的实例。当我们运行一个程序时,操作系统会为其创建一个进程,并分配内存、CPU时间片等资源。

每个进程都有自己独立的内存空间、文件描述符和系统资源,进程之间相互隔离,不会直接影响对方。

进程的特点:

  • 独立性:进程之间相互独立,拥有各自的资源
  • 动态性:进程是程序的执行过程,有创建、执行、终止等状态变化
  • 并发性:多个进程可以同时在系统中运行
  • 异步性:进程的执行是异步的,不可预知的

1.2 什么是线程

线程(Thread)是进程中的一个执行单元,是CPU调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。

线程有时也被称为"轻量级进程",因为线程之间的切换比进程之间的切换开销更小。

线程的特点:

  • 共享性:同一进程内的线程共享进程的内存空间和资源
  • 独立性:每个线程有自己的程序计数器、寄存器和栈
  • 并发性:线程之间可以并发执行
  • 开销小:线程创建和切换的开销比进程小

1.3 进程与线程的关系

┌─────────────────────────────────────────────────────────┐
│                       操作系统                          │
├───────────────────────┬─────────────────────────────────┤
│                       │                                 │
│  ┌─────────────────┐  │  ┌─────────────────┐           │
│  │     进程1       │  │  │     进程2       │           │
│  ├─────────────────┤  │  ├─────────────────┤           │
│  │  独立内存空间    │  │  │  独立内存空间    │           │
│  ├─────────────────┤  │  ├─────────────────┤           │
│  │  ┌───────────┐  │  │  │  ┌───────────┐  │           │
│  │  │  线程1    │  │  │  │  │  线程3    │  │           │
│  │  └───────────┘  │  │  │  └───────────┘  │           │
│  │  ┌───────────┐  │  │  │  ┌───────────┐  │           │
│  │  │  线程2    │  │  │  │  │  线程4    │  │           │
│  │  └───────────┘  │  │  │  └───────────┘  │           │
│  └─────────────────┘  │  └─────────────────┘           │
│                       │                                 │
└───────────────────────┴─────────────────────────────────┘

进程与线程的关系:

  1. 一个进程可以包含多个线程,但至少有一个线程(主线程)
  2. 线程是进程的一部分,共享进程的资源
  3. 进程之间相互独立,线程之间共享内存空间
  4. 进程切换比线程切换开销大

二、并行与并发

2.1 并发(Concurrency)

并发是指在同一时间间隔内,多个任务交替执行。从宏观上看,这些任务似乎是同时运行的,但从微观上看,它们是在CPU上交替执行的。

并发的实现依赖于操作系统的调度机制,通过时间片轮转等方式,让CPU在不同任务之间快速切换,从而给人一种同时执行的错觉。

2.2 并行(Parallelism)

并行是指在同一时刻,多个任务在不同的CPU核心上同时执行。并行需要多核CPU的支持,只有在多核环境下才能实现真正的并行。

2.3 并发与并行的区别

特性 并发(Concurrency) 并行(Parallelism)
执行方式 交替执行 同时执行
CPU核心 单核或多核均可 必须多核
资源共享 可以共享资源 通常不共享或通过同步机制共享
目的 提高资源利用率 提高任务执行速度
实现难度 较低,依赖调度机制 较高,需要处理同步和通信
并发示意图:
时间 ────────────────────────────────────>
CPU1: [任务A][任务B][任务A][任务B][任务A]

并行示意图:
时间 ────────────────────────────────────>
CPU1: [任务A][任务A][任务A][任务A][任务A]
CPU2: [任务B][任务B][任务B][任务B][任务B]

三、多进程与多线程

3.1 多进程(Multi-Process)

多进程是指在一个应用程序中同时创建多个进程来执行任务。每个进程有自己独立的内存空间和资源,进程之间通过IPC(Inter-Process Communication)机制进行通信。

多进程的优点:

  • 稳定性高:一个进程崩溃不会影响其他进程
  • 可以充分利用多核CPU
  • 适合CPU密集型任务

多进程的缺点:

  • 创建和切换开销大
  • 资源占用多
  • 进程间通信复杂

3.2 多线程(Multi-Thread)

多线程是指在一个进程中同时创建多个线程来执行任务。线程之间共享进程的内存空间和资源,通过共享内存进行通信。

多线程的优点:

  • 创建和切换开销小
  • 资源占用少
  • 线程间通信简单
  • 适合I/O密集型任务

多线程的缺点:

  • 稳定性低:一个线程崩溃可能导致整个进程崩溃
  • 存在线程安全问题
  • Python中受GIL限制,CPU密集型任务性能提升不明显

3.3 多进程与多线程的选择

任务类型 推荐使用 原因
CPU密集型 多进程 避免GIL限制,充分利用多核CPU
I/O密集型 多线程/异步IO 减少等待时间,提高资源利用率
高稳定性要求 多进程 进程隔离,提高系统稳定性
低开销要求 多线程 线程创建和切换开销小
复杂数据共享 多线程 共享内存,通信简单

四、Python中的进程与线程

4.1 Python中的进程

Python提供了multiprocessing模块用于创建和管理进程。该模块支持跨平台,可以在Windows、Linux和macOS上使用。

基本使用示例:

import multiprocessing
import time

def worker(num):
    """进程执行的任务"""
    print(f'进程 {num} 开始执行')
    time.sleep(2)
    print(f'进程 {num} 执行完成')
    return num

if __name__ == '__main__':
    # 创建进程池
    with multiprocessing.Pool(processes=3) as pool:
        # 提交任务
        results = pool.map(worker, range(5))
    
    print(f'所有进程执行完成,结果: {results}')

4.2 Python中的线程

Python提供了threading模块用于创建和管理线程。该模块提供了丰富的线程操作接口。

基本使用示例:

import threading
import time

def worker(num):
    """线程执行的任务"""
    print(f'线程 {num} 开始执行')
    time.sleep(2)
    print(f'线程 {num} 执行完成')

if __name__ == '__main__':
    # 创建线程列表
    threads = []
    
    # 创建并启动线程
    for i in range(5):
        t = threading.Thread(target=worker, args=(i,))
        threads.append(t)
        t.start()
    
    # 等待所有线程完成
    for t in threads:
        t.join()
    
    print('所有线程执行完成')

4.3 GIL(全局解释器锁)

GIL(Global Interpreter Lock)是Python解释器(如CPython)中的一个机制,它确保同一时刻只有一个线程在解释器中执行Python字节码。

GIL的存在主要是为了简化Python解释器的实现,尤其是内存管理部分。但GIL也带来了一些限制:

  1. 在多核CPU上,多线程的Python程序并不能充分利用多核优势
  2. CPU密集型任务在多线程环境下性能提升不明显,甚至可能下降
  3. I/O密集型任务受GIL影响较小,因为I/O操作会释放GIL

GIL对Python多线程的影响:

# CPU密集型任务示例
import threading
import time

def count(n):
    while n > 0:
        n -= 1

# 单线程执行
t1 = time.time()
count(100000000)
print(f'单线程执行时间: {time.time() - t1:.2f}秒')

# 多线程执行
thread1 = threading.Thread(target=count, args=(50000000,))
thread2 = threading.Thread(target=count, args=(50000000,))

t1 = time.time()
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f'多线程执行时间: {time.time() - t1:.2f}秒')

在大多数情况下,由于GIL的存在,多线程执行CPU密集型任务的时间可能比单线程还要长。

五、进程与线程的生命周期

5.1 进程的生命周期

进程从创建到终止会经历以下几个状态:

  1. 创建状态(New):进程正在被创建
  2. 就绪状态(Ready):进程已创建完成,等待CPU调度
  3. 运行状态(Running):进程正在CPU上执行
  4. 阻塞状态(Blocked):进程等待某个事件完成(如I/O操作)
  5. 终止状态(Terminated):进程执行完成或被终止

5.2 线程的生命周期

线程的生命周期与进程类似,也包括以下几个状态:

  1. 新建状态(New):线程对象已创建,但尚未启动
  2. 就绪状态(Runnable):线程已启动,等待CPU调度
  3. 运行状态(Running):线程正在CPU上执行
  4. 阻塞状态(Blocked):线程等待某个事件完成
  5. 死亡状态(Dead):线程执行完成或被终止

六、进程间通信

由于进程之间相互隔离,需要通过特定的机制进行通信。Python提供了多种进程间通信方式:

6.1 队列(Queue)

队列是一种线程安全、进程安全的数据结构,可以用于进程间通信。

示例:

import multiprocessing

def producer(queue):
    """生产者进程"""
    for i in range(5):
        queue.put(i)
        print(f'生产者生产了: {i}')

def consumer(queue):
    """消费者进程"""
    while True:
        item = queue.get()
        if item is None:  # 结束信号
            break
        print(f'消费者消费了: {item}')

if __name__ == '__main__':
    # 创建队列
    queue = multiprocessing.Queue()
    
    # 创建生产者和消费者进程
    p_producer = multiprocessing.Process(target=producer, args=(queue,))
    p_consumer = multiprocessing.Process(target=consumer, args=(queue,))
    
    # 启动进程
    p_producer.start()
    p_consumer.start()
    
    # 等待生产者完成
    p_producer.join()
    
    # 发送结束信号
    queue.put(None)
    
    # 等待消费者完成
    p_consumer.join()
    
    print('所有进程执行完成')

6.2 管道(Pipe)

管道是一种双向通信机制,可以在两个进程之间传递数据。

示例:

import multiprocessing

def sender(conn):
    """发送数据的进程"""
    data = [1, 2, 3, 4, 5]
    for item in data:
        conn.send(item)
        print(f'发送了: {item}')
    conn.close()

def receiver(conn):
    """接收数据的进程"""
    while True:
        try:
            item = conn.recv()
            print(f'接收了: {item}')
        except EOFError:
            break

if __name__ == '__main__':
    # 创建管道
    parent_conn, child_conn = multiprocessing.Pipe()
    
    # 创建发送者和接收者进程
    p_sender = multiprocessing.Process(target=sender, args=(child_conn,))
    p_receiver = multiprocessing.Process(target=receiver, args=(parent_conn,))
    
    # 启动进程
    p_sender.start()
    p_receiver.start()
    
    # 等待进程完成
    p_sender.join()
    p_receiver.join()
    
    print('所有进程执行完成')

6.3 共享内存(Shared Memory)

共享内存允许多个进程访问同一块内存区域,是一种高效的进程间通信方式。

示例:

import multiprocessing

def update_shared_value(shared_value):
    """更新共享值"""
    for i in range(10):
        shared_value.value += 1
        print(f'更新后的值: {shared_value.value}')

if __name__ == '__main__':
    # 创建共享值
    shared_value = multiprocessing.Value('i', 0)  # 'i'表示整数类型
    
    # 创建两个进程
    p1 = multiprocessing.Process(target=update_shared_value, args=(shared_value,))
    p2 = multiprocessing.Process(target=update_shared_value, args=(shared_value,))
    
    # 启动进程
    p1.start()
    p2.start()
    
    # 等待进程完成
    p1.join()
    p2.join()
    
    print(f'最终值: {shared_value.value}')

七、线程同步与互斥

由于线程共享进程的内存空间,当多个线程同时访问共享资源时,可能会导致数据不一致的问题,这称为线程安全问题。

7.1 线程安全问题

示例:

import threading

# 共享变量
counter = 0

def increment():
    global counter
    for _ in range(100000):
        counter += 1

# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)

# 启动线程
thread1.start()
thread2.start()

# 等待线程完成
thread1.join()
thread2.join()

print(f'最终计数器值: {counter}')  # 预期值为200000,但实际可能小于这个值

问题分析:
counter += 1看似是一个原子操作,但实际上它包含三个步骤:

  1. 读取counter的当前值
  2. 将值加1
  3. 将新值写回counter

当两个线程同时执行这个操作时,可能会出现以下情况:

  • 线程1读取counter的值为100
  • 线程2也读取counter的值为100
  • 线程1将值加1得到101,并写回counter
  • 线程2也将值加1得到101,并写回counter

最终counter的值为101,而不是预期的102。

7.2 锁机制

为了解决线程安全问题,Python提供了锁机制。锁可以确保在同一时刻只有一个线程可以访问共享资源。

示例:

import threading

# 共享变量
counter = 0

# 创建锁
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:  # 获取锁
            counter += 1
        # 自动释放锁

# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)

# 启动线程
thread1.start()
thread2.start()

# 等待线程完成
thread1.join()
thread2.join()

print(f'最终计数器值: {counter}')  # 预期值为200000

使用锁机制可以确保counter += 1操作的原子性,从而解决线程安全问题。

八、总结与练习

8.1 总结

  1. 进程是操作系统资源分配的基本单位,线程是CPU调度的最小单位
  2. 并发是指任务交替执行,并行是指任务同时执行
  3. 多进程适合CPU密集型任务,多线程适合I/O密集型任务
  4. Python中由于GIL的存在,多线程在CPU密集型任务上性能提升不明显
  5. 进程间通信可以通过队列、管道、共享内存等方式实现
  6. 线程安全问题可以通过锁机制解决

8.2 练习

  1. 基础练习:

    • 编写一个多进程程序,计算1到100000000的和
    • 编写一个多线程程序,模拟下载多个文件
  2. 进阶练习:

    • 使用队列实现生产者-消费者模式
    • 使用共享内存实现两个进程之间的通信
    • 实现一个线程安全的计数器
  3. 思考问题:

    • 为什么Python中的多线程在CPU密集型任务上性能不好?
    • 进程与线程的主要区别是什么?
    • 什么是线程安全?如何保证线程安全?

九、扩展阅读

  1. Python官方文档:

  2. 推荐书籍:

    • 《Python并行编程手册》
    • 《Python Cookbook》(第3版)第12章:并发编程
  3. 在线资源:


下集预告:第112集将学习线程的创建与启动,包括如何创建线程、启动线程、传递参数等内容。

« 上一篇 网络编程综合练习 下一篇 » 线程创建与启动