“轻量级并行”的核心思想是在单个进程内,利用更少、更高效的资源来实现并发执行,以避免传统多进程(fork)或多线程(pthread)带来的高开销(如内存占用、上下文切换、同步复杂性),在 C 语言中,主要有以下几种主流的轻量级并行技术:

POSIX 线程
这是最传统、最通用的在 C 语言中进行多线程编程的方式,虽然“线程”本身听起来可能不那么“轻量”,但与进程相比,它们共享内存空间,创建和切换的开销要小得多,因此是实现并行的标准工具。
核心概念:
- 线程: 轻量级的执行单元,是进程内的一个实体。
- 共享内存: 所有线程共享进程的地址空间,数据共享非常方便。
- 同步原语: 需要使用互斥锁、条件变量、信号量等来防止数据竞争。
优点:
- 成熟稳定: POSIX 标准存在已久,几乎所有类 Unix 系统(Linux, macOS)都原生支持。
- 功能强大: 提供了完整的线程生命周期管理和丰富的同步工具。
- 广泛应用: 大量现有库和系统都基于 Pthreads。
缺点:

- 相对较重: 与下面要讲的技术相比,创建一个线程仍然需要分配栈空间、内核数据结构等,开销较大。
- 同步复杂性: 手动管理锁非常容易出错,可能导致死锁、活锁等问题。
简单示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
// 线程函数,每个线程都会执行这个函数
void* print_hello(void* thread_id) {
long tid = (long)thread_id;
printf("Hello World! It's me, thread #%ld!\n", tid);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int rc;
long t;
for (t = 0; t < NUM_THREADS; t++) {
printf("Creating thread %ld\n", t);
// 创建线程
rc = pthread_create(&threads[t], NULL, print_hello, (void*)t);
if (rc) {
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
}
// 等待所有线程结束
for (t = 0; t < NUM_THREADS; t++) {
pthread_join(threads[t], NULL);
}
printf("Main: program completed. Exiting.\n");
pthread_exit(NULL);
}
C11 <threads.h> 标准库
这是 C11 标准中引入的官方线程库,旨在提供一个更现代、更简洁、与平台无关的线程 API,它是对 Pthreads 的一种高级抽象。
核心概念:
thrd_create: 创建线程。thrd_join: 等待线程结束。mtx_t: 互斥锁。cnd_t: 条件变量。call_once: 确保某段代码只执行一次(常用于初始化)。
优点:
- 标准化: C 语言标准的一部分,可移植性好。
- API 更简洁: 相比 Pthreads,API 设计更现代化,命名更清晰(
thrd_tvspthread_t)。 - 类型安全: 类型定义更明确。
缺点:
- 支持度: 较新的标准,一些老旧的编译器或平台可能不完全支持。
- 生态系统: 相比 Pthreads,相关的教程和库支持较少。
简单示例:
#include <stdio.h>
#include <stdlib.h>
#include <threads.h>
#define NUM_THREADS 5
int print_hello(void* thread_id) {
long tid = (long)thread_id;
printf("Hello World! It's me, thread #%ld (C11)!\n", tid);
return 0;
}
int main() {
thrd_t threads[NUM_THREADS];
int rc;
long t;
for (t = 0; t < NUM_THREADS; t++) {
printf("Creating thread %ld\n", t);
// 创建线程
rc = thrd_create(&threads[t], print_hello, (void*)t);
if (rc != thrd_success) {
printf("ERROR; failed to create thread %ld\n", t);
exit(-1);
}
}
// 等待所有线程结束
for (t = 0; t < NUM_THREADS; t++) {
thrd_join(threads[t], NULL);
}
printf("Main: program completed. Exiting.\n");
return 0;
}
OpenMP
OpenMP (Open Multi-Processing) 是一个基于指令的并行编程模型,特别适合于数据并行——即对同一组数据的不同部分执行相同的操作,它通过在代码中插入编译器指令(pragmas)来并行化循环。
核心概念:
#pragma omp parallel: 创建一个线程团队,代码块将在所有线程上并行执行。#pragma omp for: 将一个for循环的迭代分配给线程团队中的不同线程。#pragma omp critical: 定义一个临界区,确保同一时间只有一个线程执行其中的代码。- 环境变量: 通过
OMP_NUM_THREADS可以轻松控制线程数。
优点:
- 极其简单易用: 只需添加几行指令,就能将串行代码并行化,开发效率极高。
- 可伸缩性好: 可以轻松地从双核扩展到多核服务器,无需修改代码。
- 自动负载均衡: OpenMP 运行时会自动处理任务分配。
缺点:
- 不适合任务并行: 对于逻辑上完全不同、难以拆分的任务,OpenMP 并不适用。
- 隐式控制: 并行化是隐式的,有时难以精确控制线程间的交互和调试。
简单示例:
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 8
int main() {
int nthreads = NUM_THREADS;
// 获取或设置线程数
#pragma omp parallel
{
int id = omp_get_thread_num();
int nthrds = omp_get_num_threads();
if (id == 0) {
nthreads = nthrds; // 主线程获取总线程数
}
}
printf("Number of threads used: %d\n", nthreads);
// 并行化一个 for 循环
#pragma omp parallel for
for (int i = 0; i < 20; i++) {
int thread_id = omp_get_thread_num();
printf("Iteration %d processed by thread %d\n", i, thread_id);
}
return 0;
}
协程
协程是“轻量级”的极致,它不是操作系统内核调度的,而是由用户代码或运行时库在用户态进行调度的,一个协程可以在执行过程中主动“让出”(yield)CPU,稍后再“恢复”(resume)执行,这种切换的开销极小,几乎等同于一次函数调用的开销。
核心概念:
- 用户态调度: 不依赖内核,切换速度极快。
- 单线程内: 多个协程运行在同一个线程中,通过协作式调度切换。
- 非抢占式: 必须由协程主动让出控制权,避免了线程同步的复杂性。
在 C 语言中实现协程的方式:
- 库: 使用成熟的协程库,如 libco (腾讯开源,高性能)、ucontext.h (较底层,不推荐直接使用)。
- 编译器扩展: 使用支持协程的编译器,如 GCC 的
--coroutines或 Clang 的协程 TS。 - 现代 C++: C++20 引入了标准协程,但这里我们只讨论 C。
优点:
- 极致轻量: 创建和切换协程的成本极低,可以轻松创建成千上万个。
- 同步简单: 因为是协作式调度,没有数据竞争问题,大大减少了锁的需求。
- 高并发: 非常适合处理大量 I/O 密集型任务,如网络服务器。
缺点:
- 编程模型复杂: 需要转换思维,从“指令流”变为“协程流”,代码结构可能更复杂。
- 调试困难: 协程的切换点非常多,调试起来比传统线程更困难。
- 生态系统: 相比线程,协程的生态系统和工具链还不够成熟。
使用 libco 的简单示例:
// 需要先安装 libco 库
#include <stdio.h>
#include <stdlib.h>
#include <libco.h>
// 协程函数
void* coroutine_func(void* arg) {
int id = *(int*)arg;
printf("Coroutine %d: started\n", id);
// 模拟让出 CPU
co_yield();
printf("Coroutine %d: resumed\n", id);
// 再次让出
co_yield();
printf("Coroutine %d: finished\n", id);
return NULL;
}
int main() {
cothread_t co1, co2;
int id1 = 1, id2 = 2;
// 创建协程
co1 = co_create(1024 * 1024, coroutine_func, &id1);
co2 = co_create(1024 * 1024, coroutine_func, &id2);
// 主协程运行
printf("Main: running\n");
// 切换到协程1
co_switch(co1);
printf("Main: back from co1\n");
// 切换到协程2
co_switch(co2);
printf("Main: back from co2\n");
// 再次切换回协程1,让它结束
co_switch(co1);
printf("Main: co1 finished\n");
// 销毁协程
co_delete(co1);
co_delete(co2);
printf("Main: all done.\n");
return 0;
}
技术对比与选型指南
| 特性/技术 | POSIX 线程 (pthread) |
C11 <threads.h> |
OpenMP | 协程 (如 libco) |
|---|---|---|---|---|
| 并行模型 | 多线程 | 多线程 | 数据并行 (指令级) | 协作式任务/数据并行 |
| 重量级 | 中 | 中 | 低 (编译器指令) | 极低 |
| 创建/切换开销 | 中 | 中 | 低 (编译器优化) | 极低 (函数调用级) |
| 易用性 | 中 (需手动同步) | 中 (需手动同步) | 高 (几行指令搞定) | 低 (编程模型复杂) |
| 适用场景 | 通用并行计算、任务并行 | 跨平台多线程任务 | 循环密集型计算、科学计算 | I/O密集型高并发、网络编程 |
| 同步复杂性 | 高 | 高 | 低 (编译器管理) | 极低 (无竞争) |
| 标准/依赖 | POSIX 标准 | C11 标准 | OpenMP 指令集 | 第三方库/编译器扩展 |
如何选择?
- 如果你的任务是循环密集型,比如矩阵运算、图像处理、大规模数值模拟,首选 OpenMP,它是最简单、最高效的解决方案。
- 如果你的任务是通用的、逻辑上独立的任务并行,并且你追求代码的跨平台性,使用 C11
<threads.h>,如果你的代码主要运行在 Linux/macOS,且需要更丰富的功能,使用 Pthreads 也完全没问题。 - 如果你的应用需要处理成千上万的并发连接,且主要是 I/O 操作(如网络服务器、爬虫),协程是不二之选,它能让你在单线程内实现极高的并发,同时避免锁的噩梦。
在现代 C++ 开发中,std::thread (Pthreads的封装)、std::async 和 C++20 协程已经成为主流,但在纯 C 语言环境中,以上四种技术仍然是实现轻量级并行的核心工具。
