当前位置:  首页>> 技术小册>> 深入C语言和程序运行原理

章节 12:标准库:你需要了解的 C 并发编程基础知识

在深入探讨C语言的并发编程时,我们不得不提到C标准库(C Standard Library)及其扩展,这些库提供了构建多线程程序的基础。尽管C标准本身直到C11标准之前都没有直接支持多线程编程,但C程序员一直通过POSIX线程(pthreads)、OpenMP等第三方库来实现并发性。从C11开始,C标准库引入了<threads.h>头文件(在部分实现中可能作为可选功能),以及内存模型和原子操作的支持,这标志着C语言在并发编程领域迈出了重要一步。然而,鉴于<threads.h>的普及程度和广泛支持度,本章节将主要围绕更广泛接受的方法和概念展开讨论,同时也会简要提及C11及之后版本的标准库对并发编程的支持。

1. 并发与并行的基本概念

在深入具体技术之前,了解并发(Concurrency)与并行(Parallelism)的基本概念至关重要。并发指的是同时处理多个任务的能力,但这些任务并非在同一时刻同时执行,而是轮流使用CPU时间片(时间共享)。而并行则是指同时有多个任务在同一时间点内真正同时执行,这通常需要多核处理器的支持。

2. POSIX线程(pthreads)

POSIX线程(pthreads)是C语言中最广泛使用的并发编程库之一,它定义了一套用于创建、同步、调度和管理线程的API。在POSIX兼容的系统(如Linux、MacOS)上,pthreads几乎成了多线程编程的标准。

2.1 创建线程

在pthreads中,pthread_create函数用于创建一个新线程。其原型如下:

  1. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
  2. void *(*start_routine) (void *), void *arg);

这里,thread是指向新线程ID的指针,attr是线程属性(通常为NULL表示默认属性),start_routine是新线程执行的函数,arg是传递给该函数的参数。

2.2 线程同步

为了避免数据竞争和其他并发问题,pthreads提供了多种同步机制,包括互斥锁(mutexes)、条件变量(condition variables)、读写锁(readers-writers locks)等。

  • 互斥锁:确保同一时间只有一个线程可以访问共享资源。pthread_mutex_lockpthread_mutex_unlock用于加锁和解锁。
  • 条件变量:与互斥锁一起使用,允许线程在等待某个条件成立时阻塞,并在条件满足时被唤醒。
2.3 线程终止与清理

线程可以通过调用pthread_exit函数终止,该函数允许线程指定退出状态。主线程可以调用pthread_join等待一个线程结束,并获取其退出状态。

3. C11及更高版本的并发支持

从C11开始,C标准库引入了对并发编程的初步支持,主要集中在原子操作和线程局部存储上。

3.1 原子操作

原子操作是指在执行过程中不会被线程调度机制中断的操作。C11在<stdatomic.h>(或<stdatomic>)头文件中定义了原子类型及其操作,如_Atomic类型说明符和一系列原子操作函数(如atomic_loadatomic_storeatomic_fetch_add等)。

3.2 线程支持(可选)

C11标准定义了<threads.h>头文件,该头文件提供了一套简化的线程管理API,包括线程的创建(thrd_create)、等待(thrd_join)、分离(thrd_detach)以及退出(thrd_exit)等。然而,由于该特性是可选的,并非所有C11编译器都实现了这一特性。

4. 并发编程的设计模式与最佳实践

  • 生产者-消费者模型:是一种常见的并发设计模式,其中一个或多个生产者线程生成数据,并将其放入一个缓冲区中,而一个或多个消费者线程从缓冲区中取出数据并处理。
  • 避免共享数据:尽可能设计不依赖共享状态的算法和数据结构,或者将共享数据的使用限制在最小范围内。
  • 使用锁保护共享资源:当必须使用共享数据时,通过互斥锁等同步机制确保数据的访问是互斥的。
  • 无锁编程:使用原子操作等技术,尝试实现无需锁同步的并发算法,这可以提高性能但实现复杂。
  • 测试与验证:并发程序往往难以调试,因此充分的测试(包括单元测试、压力测试等)和代码审查尤为重要。

5. 实际案例:使用pthreads实现生产者-消费者模型

下面是一个简单的生产者-消费者模型的实现示例,使用pthreads库。

  1. #include <pthread.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #define BUFFER_SIZE 10
  5. int buffer[BUFFER_SIZE];
  6. int count = 0;
  7. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  8. pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
  9. pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
  10. void* producer(void* arg) {
  11. for (int i = 0; i < 100; ++i) {
  12. pthread_mutex_lock(&lock);
  13. while (count == BUFFER_SIZE) {
  14. pthread_cond_wait(&not_full, &lock);
  15. }
  16. buffer[count] = i;
  17. ++count;
  18. pthread_cond_signal(&not_empty);
  19. pthread_mutex_unlock(&lock);
  20. }
  21. return NULL;
  22. }
  23. void* consumer(void* arg) {
  24. for (int i = 0; i < 100; ++i) {
  25. pthread_mutex_lock(&lock);
  26. while (count == 0) {
  27. pthread_cond_wait(&not_empty, &lock);
  28. }
  29. int item = buffer[--count];
  30. printf("Consumed %d\n", item);
  31. pthread_cond_signal(&not_full);
  32. pthread_mutex_unlock(&lock);
  33. }
  34. return NULL;
  35. }
  36. int main() {
  37. pthread_t prod_tid, cons_tid;
  38. pthread_create(&prod_tid, NULL, producer, NULL);
  39. pthread_create(&cons_tid, NULL, consumer, NULL);
  40. pthread_join(prod_tid, NULL);
  41. pthread_join(cons_tid, NULL);
  42. return 0;
  43. }

此示例展示了如何使用pthreads中的互斥锁和条件变量来实现基本的生产者-消费者同步。

6. 结论

C语言通过pthreads等第三方库以及C11及更高版本标准库中的原子操作和线程支持,为并发编程提供了强大的工具。掌握这些基础知识和工具,可以帮助开发者构建高效、可靠的多线程程序。然而,并发编程也带来了许多挑战,如数据竞争、死锁等问题,需要开发者在设计和实现时仔细考虑和测试。


该分类下的相关小册推荐: