晟辉智能制造

C语言如何实现轻量级并行技术?

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

C语言如何实现轻量级并行技术?-图1
(图片来源网络,侵删)

POSIX 线程

这是最传统、最通用的在 C 语言中进行多线程编程的方式,虽然“线程”本身听起来可能不那么“轻量”,但与进程相比,它们共享内存空间,创建和切换的开销要小得多,因此是实现并行的标准工具。

核心概念:

  • 线程: 轻量级的执行单元,是进程内的一个实体。
  • 共享内存: 所有线程共享进程的地址空间,数据共享非常方便。
  • 同步原语: 需要使用互斥锁、条件变量、信号量等来防止数据竞争。

优点:

  • 成熟稳定: POSIX 标准存在已久,几乎所有类 Unix 系统(Linux, macOS)都原生支持。
  • 功能强大: 提供了完整的线程生命周期管理和丰富的同步工具。
  • 广泛应用: 大量现有库和系统都基于 Pthreads。

缺点:

C语言如何实现轻量级并行技术?-图2
(图片来源网络,侵删)
  • 相对较重: 与下面要讲的技术相比,创建一个线程仍然需要分配栈空间、内核数据结构等,开销较大。
  • 同步复杂性: 手动管理锁非常容易出错,可能导致死锁、活锁等问题。

简单示例:

#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_t vs pthread_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 语言中实现协程的方式:

  1. : 使用成熟的协程库,如 libco (腾讯开源,高性能)、ucontext.h (较底层,不推荐直接使用)。
  2. 编译器扩展: 使用支持协程的编译器,如 GCC 的 --coroutines 或 Clang 的协程 TS。
  3. 现代 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 指令集 第三方库/编译器扩展

如何选择?

  1. 如果你的任务是循环密集型,比如矩阵运算、图像处理、大规模数值模拟,首选 OpenMP,它是最简单、最高效的解决方案。
  2. 如果你的任务是通用的、逻辑上独立的任务并行,并且你追求代码的跨平台性使用 C11 <threads.h>,如果你的代码主要运行在 Linux/macOS,且需要更丰富的功能,使用 Pthreads 也完全没问题。
  3. 如果你的应用需要处理成千上万的并发连接,且主要是 I/O 操作(如网络服务器、爬虫),协程是不二之选,它能让你在单线程内实现极高的并发,同时避免锁的噩梦。

在现代 C++ 开发中,std::thread (Pthreads的封装)、std::async 和 C++20 协程已经成为主流,但在纯 C 语言环境中,以上四种技术仍然是实现轻量级并行的核心工具。

分享:
扫描分享到社交APP
上一篇
下一篇