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

第十三章 标准库:如何使用互斥量等技术协调线程运行

在编写并发程序时,如何有效地管理线程间的同步与互斥,是确保程序正确性和性能的关键。C语言标准库本身并不直接提供高级的多线程编程接口,但POSIX线程(pthread)库作为在UNIX及类UNIX系统上广泛使用的线程库,为C语言程序提供了丰富的线程同步机制,包括互斥量(Mutexes)、条件变量(Condition Variables)、信号量(Semaphores)等。本章将重点介绍如何使用这些技术,特别是互斥量,来协调线程的运行。

13.1 线程同步与互斥的基础

13.1.1 线程同步的概念

线程同步是指多个线程在执行过程中,按照一定的顺序或条件相互协作,以完成共同的任务。它确保了在多线程环境下,数据的访问和修改是安全的,避免了数据竞争(Race Conditions)和死锁(Deadlocks)等问题。

13.1.2 互斥量的作用

互斥量(Mutex,Mutual Exclusion的缩写)是一种用于保护共享资源的数据结构,它确保在任何时刻,只有一个线程可以访问该资源。当一个线程想要访问被互斥量保护的资源时,它必须先锁定该互斥量;访问结束后,再解锁互斥量,以便其他线程可以访问。

13.2 POSIX线程库中的互斥量

在POSIX线程库中,互斥量的相关操作主要通过以下几个函数实现:

  • pthread_mutex_init():初始化互斥量。
  • pthread_mutex_lock():尝试锁定互斥量。如果互斥量已被锁定,则调用线程将被阻塞,直到互斥量被解锁。
  • pthread_mutex_unlock():解锁互斥量,允许其他线程访问被保护的资源。
  • pthread_mutex_destroy():销毁互斥量,释放其占用的资源。

示例代码

  1. #include <pthread.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. pthread_mutex_t lock;
  5. int counter = 0;
  6. void* increment(void* arg) {
  7. pthread_mutex_lock(&lock);
  8. counter++;
  9. printf("Counter = %d\n", counter);
  10. pthread_mutex_unlock(&lock);
  11. return NULL;
  12. }
  13. int main() {
  14. pthread_t t1, t2;
  15. pthread_mutex_init(&lock, NULL); // 初始化互斥量
  16. pthread_create(&t1, NULL, increment, NULL);
  17. pthread_create(&t2, NULL, increment, NULL);
  18. pthread_join(t1, NULL);
  19. pthread_join(t2, NULL);
  20. pthread_mutex_destroy(&lock); // 销毁互斥量
  21. return 0;
  22. }

在上述示例中,两个线程都试图增加同一个全局变量counter的值。通过使用互斥量lock,我们确保了每次只有一个线程能访问并修改counter,从而避免了数据竞争。

13.3 互斥量的属性与类型

POSIX线程库允许通过pthread_mutexattr_t结构体和相关的函数来设置互斥量的属性,如类型(如快速互斥量、递归互斥量等)。

  • 快速互斥量(Default Mutex):标准的互斥量类型,适用于大多数情况。
  • 递归互斥量(Recursive Mutex):允许同一个线程多次锁定同一个互斥量,而不会导致死锁。每次锁定操作都需要对应的解锁操作来解锁。

设置互斥量属性示例

  1. pthread_mutexattr_t attr;
  2. pthread_mutexattr_init(&attr);
  3. pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
  4. pthread_mutex_init(&lock, &attr);
  5. pthread_mutexattr_destroy(&attr);

13.4 使用条件变量与互斥量协同工作

条件变量(Condition Variables)通常与互斥量一起使用,以等待或通知线程某些条件的发生。条件变量允许线程挂起(阻塞)在一个条件上,直到另一个线程修改了条件并通知它。

关键函数

  • pthread_cond_init():初始化条件变量。
  • pthread_cond_wait():在调用线程上阻塞,等待条件变量的条件为真,同时释放与之关联的互斥量。
  • pthread_cond_signal():唤醒等待该条件变量的一个线程。
  • pthread_cond_broadcast():唤醒等待该条件变量的所有线程。

示例:生产者-消费者问题(简化版)

  1. #include <pthread.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  5. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  6. int buffer = 0;
  7. int full = 0;
  8. void* producer(void* arg) {
  9. while (1) {
  10. pthread_mutex_lock(&mutex);
  11. while (full) {
  12. pthread_cond_wait(&cond, &mutex); // 等待缓冲区不满
  13. }
  14. buffer = 1; // 生产数据
  15. full = 1;
  16. printf("Produced\n");
  17. pthread_cond_signal(&cond); // 通知消费者
  18. pthread_mutex_unlock(&mutex);
  19. sleep(1); // 模拟耗时操作
  20. }
  21. return NULL;
  22. }
  23. void* consumer(void* arg) {
  24. while (1) {
  25. pthread_mutex_lock(&mutex);
  26. while (!full) {
  27. pthread_cond_wait(&cond, &mutex); // 等待缓冲区有数据
  28. }
  29. buffer = 0; // 消费数据
  30. full = 0;
  31. printf("Consumed\n");
  32. pthread_cond_signal(&cond); // 可选,如果多个消费者可能需要
  33. pthread_mutex_unlock(&mutex);
  34. sleep(1); // 模拟耗时操作
  35. }
  36. return NULL;
  37. }
  38. int main() {
  39. pthread_t prod, cons;
  40. pthread_create(&prod, NULL, producer, NULL);
  41. pthread_create(&cons, NULL, consumer, NULL);
  42. pthread_join(prod, NULL);
  43. pthread_join(cons, NULL); // 实际应用中,通常不会在这里等待终止
  44. return 0;
  45. }

在这个示例中,生产者和消费者通过互斥量和条件变量协同工作,确保了数据的正确生产和消费,避免了数据竞争和死锁的问题。

13.5 注意事项与最佳实践

  1. 避免死锁:确保互斥量的锁定顺序在所有线程中保持一致,避免循环等待条件。
  2. 使用锁的范围最小化:只在必要时锁定互斥量,并在最短时间内释放,以减少线程阻塞时间。
  3. 检查返回值:线程同步函数的返回值提供了操作是否成功的反馈,应总是检查这些值。
  4. 使用递归锁需谨慎:虽然递归锁允许同一个线程多次锁定,但过度使用可能会导致性能问题或隐藏的逻辑错误。
  5. 考虑使用高级同步机制:对于复杂的同步需求,如读者-写者问题,可以考虑使用读写锁(Read-Write Locks)等更高级的同步机制。

通过以上内容的介绍,您应该已经对如何在C语言中使用POSIX线程库中的互斥量等技术来协调线程运行有了较为全面的了解。在实际编程中,根据具体需求选择合适的同步机制,并遵循最佳实践,是编写高效、稳定并发程序的关键。


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