并发与并行

并发

并发指的是多个任务同一时间段内交替执行。

例如:

1
2
3
任务 A 执行一会儿
切换到任务 B
再切换回任务 A

在单核 CPU 上,也可以通过时间片轮转实现并发。

并行

并行指的是多个任务同一时刻真正同时执行。

例如在多核 CPU 上:

1
2
核心 1 执行任务 A
核心 2 执行任务 B

进程与线程

进程

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

每个进程有独立的:

  • 地址空间
  • 文件描述符
  • 堆空间
  • 栈空间
  • 全局变量

线程

线程是CPU调度的基本单位。

同一个进程中的多个线程共享:

  • 进程地址空间
  • 全局变量
  • 堆空间
  • 文件描述符

每个线程独有:

  • 线程栈
  • 寄存器上下文
  • 程序计数器

进程间通信(IPC)

常用方式:

  • 管道(pipe)pipe() 创建匿名管道,用于父子进程通信。
  • 命名管道(FIFO):用于无亲缘关系进程。
  • 信号(signal):异步通知,如 SIGTERMSIGKILL
  • 共享内存(shm):最快的数据交换方式,需同步机制。
  • 消息队列信号量等。

多线程的优势

多线程常用于:

  • 提高 CPU 利用率
  • 提升程序响应速度
  • 处理 I/O 密集型任务
  • 并行计算
  • 后台任务处理

例如:

1
2
3
主线程负责 UI
工作线程负责网络请求
另一个线程负责文件读写

多线程的风险

多线程会带来一些问题:

  • 数据竞争
  • 死锁
  • 活锁
  • 线程饥饿
  • 上下文切换开销
  • 调试困难

数据竞争

当多个线程同时访问同一份共享数据,并且至少有一个线程进行写操作,同时没有使用同步机制,就会产生数据竞争。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>

int counter = 0;

void add() {
for (int i = 0; i < 100000; ++i) {
++counter;
}
}

int main() {
std::thread t1(add);
std::thread t2(add);

t1.join();
t2.join();

std::cout << counter << std::endl;
return 0;
}

理论上结果应该是:

1
200000

但实际可能小于 200000,因为 ++counter 不是原子操作。

它大致包含三步:

1
2
3
读取 counter
加 1
写回 counter

多个线程交叉执行时就可能出错。