《Linux应用多线程(一) — 线程的同步与互斥机制》
1.互斥锁
互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后会释放(解锁)互斥量。
#include<pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); 返回值: 成功:0; 失败:返回错误编号 attr: PTHREAD_MUTEX_INITIALIZER创建快速互斥锁 PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP创建递归互斥锁 PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP创建检错互斥锁要用默认的属性初始化互斥锁(快速锁),只需把attr设置为NULL。
在销毁互斥量的时候需要注意:
· 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量无须销毁。
· 不要销毁一个已加锁的互斥量,或者是真正配合条件变量使用的互斥量。
· 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值:成功:0; 失败:错误编号
线程调用pthread_mutex_lock时候,如果互斥量已经上锁,则线程会被阻塞直到互斥量解锁。
如果不希望线程阻塞,可以调用pthread_mutex_unlock。如果互斥量已经上锁,会立即返回EBUSY。
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
返回值:成功:0;失败:错误编号
这里的时间是绝对时间,而不是相对时间。
不同种类得锁:
1. 普通互斥锁(Basic Mutex)
- 特点:最基本的互斥锁类型,用于保护共享资源。线程在访问共享资源之前必须获取锁,访问完成后释放锁。
- 适用场景:简单的线程同步需求。
- 行为:如果锁已被其他线程持有,调用线程会被阻塞,直到锁被释放。
2. 递归互斥锁(Recursive Mutex)
- 特点:允许同一个线程多次获取锁,而不会导致死锁。每次获取锁后,必须释放相同次数的锁才能完全释放资源。
- 适用场景:当同一个线程需要多次访问被互斥锁保护的资源时。
- 行为:线程可以多次获取锁,但必须配对地释放锁,否则其他线程无法获取该锁。
嵌套调用demo:
嵌套调用:当一个函数需要调用另一个函数,而这两个函数都需要获取同一个锁时。递归锁允许同一线程多次获取锁而不会导致死锁。
#include <stdio.h> #include <pthread.h> #include <unistd.h> // 定义递归互斥锁 pthread_mutex_t recursive_mutex; // 初始化递归互斥锁 void init_recursive_mutex() { pthread_mutexattr_t mutex_attr; pthread_mutexattr_init(&mutex_attr); pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&recursive_mutex, &mutex_attr); pthread_mutexattr_destroy(&mutex_attr); } // 函数 A:获取锁并调用函数 B void function_a(int id) { pthread_mutex_lock(&recursive_mutex); printf("Thread %d: function_a acquired the lock\n", id); // 调用另一个需要相同锁的函数 function_b(id); printf("Thread %d: function_a releasing the lock\n", id); pthread_mutex_unlock(&recursive_mutex); } // 函数 B:也需要获取相同的锁 void function_b(int id) { pthread_mutex_lock(&recursive_mutex); printf("Thread %d: function_b acquired the lock\n", id); sleep(1); // 模拟一些工作 printf("Thread %d: function_b releasing the lock\n", id); pthread_mutex_unlock(&recursive_mutex); } // 线程函数 void *thread_func(void *arg) { int id = *(int *)arg; function_a(id); return NULL; } int main() { pthread_t thread1, thread2; int id1 = 1, id2 = 2; // 初始化递归互斥锁 init_recursive_mutex(); // 创建线程 pthread_create(&thread1, NULL, thread_func, &id1); pthread_create(&thread2, NULL, thread_func, &id2); // 等待线程完成 pthread_join(thread1, NULL); pthread_join(thread2, NULL); // 销毁互斥锁 pthread_mutex_destroy(&recursive_mutex); return 0; }
递归调用demo:
递归函数:在递归函数中,如果每次递归调用都需要访问共享资源,则可以使用递归锁来保护这些访问。
#include <stdio.h> #include <pthread.h> #include <unistd.h> // 定义递归互斥锁 pthread_mutex_t recursive_mutex; // 初始化递归互斥锁 void init_recursive_mutex() { pthread_mutexattr_t mutex_attr; pthread_mutexattr_init(&mutex_attr); pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&recursive_mutex, &mutex_attr); pthread_mutexattr_destroy(&mutex_attr); } // 共享资源 int shared_resource = 0; // 递归函数 void recursive_function(int depth) { // 获取递归锁 pthread_mutex_lock(&recursive_mutex); printf("Thread %ld: Entering recursive_function at depth %d\n", (long)pthread_self(), depth); // 访问共享资源 shared_resource++; printf("Thread %ld: shared_resource = %d\n", (long)pthread_self(), shared_resource); // 递归调用 if (depth < 3) { recursive_function(depth + 1); } printf("Thread %ld: Leaving recursive_function at depth %d\n", (long)pthread_self(), depth); // 释放递归锁 pthread_mutex_unlock(&recursive_mutex); } // 线程函数 void *thread_func(void *arg) { int depth = 0; recursive_function(depth); return NULL; } int main() { pthread_t thread1, thread2; // 初始化递归互斥锁 init_recursive_mutex(); // 创建线程 pthread_create(&thread1, NULL, thread_func, NULL); pthread_create(&thread2, NULL, thread_func, NULL); // 等待线程完成 pthread_join(thread1, NULL); pthread_join(thread2, NULL); // 销毁互斥锁 pthread_mutex_destroy(&recursive_mutex); return 0; }
3. 检查互斥锁(Trylock Mutex)
- 特点:提供非阻塞的锁获取尝试。线程尝试获取锁,如果锁可用则获取成功,否则立即返回失败,而不会阻塞。
- 适用场景:当线程需要避免长时间阻塞时,例如在实时系统中。
- 行为:
pthread_mutex_trylock:尝试获取锁,如果锁不可用,则返回错误码EBUSY。
4. 错误检查互斥锁(Error-Checking Mutex)
- 特点:在调试过程中非常有用,能够检测到一些常见的错误,如重复锁定或未锁定的释放。
- 适用场景:主要用于调试,以帮助发现锁的使用错误。
- 行为:如果线程尝试获取已经持有的锁,或者尝试释放未持有的锁,通常会返回错误。
demo:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <stdbool.h> #include <errno.h> // 共享资源 int shared_counter = 0; // 定义带有错误检查的互斥锁 pthread_mutex_t mutex; // 检查并报告互斥锁错误 void check_mutex_error(int rc, const char* operation) { if (rc != 0) { const char* error_msg; switch(rc) { case EINVAL: error_msg = "无效的互斥锁或操作"; break; case EAGAIN: error_msg = "资源暂时不可用"; break; case EDEADLK: error_msg = "检测到死锁情况"; break; case EPERM: error_msg = "当前线程不持有锁"; break; default: error_msg = "未知错误"; } fprintf(stderr, "错误: %s 失败 - %s (%d)\n", operation, error_msg, rc); exit(EXIT_FAILURE); } } // 线程函数,增加共享计数器 void* increment_counter(void* arg) { long thread_id = (long)arg; for (int i = 0; i < 3; i++) { // 尝试加锁并检查错误 int rc = pthread_mutex_lock(&mutex); check_mutex_error(rc, "pthread_mutex_lock"); // 临界区开始 - 访问共享资源 printf("线程 %ld 进入临界区\n", thread_id); shared_counter++; printf("线程 %ld 增加计数器到 %d\n", thread_id, shared_counter); // 模拟一些工作 usleep(100000); // 100毫秒 // 临界区结束 printf("线程 %ld 离开临界区\n", thread_id); // 尝试解锁并检查错误 rc = pthread_mutex_unlock(&mutex); check_mutex_error(rc, "pthread_mutex_unlock"); // 非临界区工作 usleep(100000); // 100毫秒 } return NULL; } int main() { pthread_t thread1, thread2; // 初始化带有错误检查的互斥锁 pthread_mutexattr_t mutex_attr; int rc = pthread_mutexattr_init(&mutex_attr); check_mutex_error(rc, "pthread_mutexattr_init"); // 设置互斥锁类型为错误检查 rc = pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ERRORCHECK); check_mutex_error(rc, "pthread_mutexattr_settype"); rc = pthread_mutex_init(&mutex, &mutex_attr); check_mutex_error(rc, "pthread_mutex_init"); // 销毁互斥锁属性 rc = pthread_mutexattr_destroy(&mutex_attr); check_mutex_error(rc, "pthread_mutexattr_destroy"); // 创建两个线程 rc = pthread_create(&thread1, NULL, increment_counter, (void*)1); check_mutex_error(rc, "pthread_create (线程1)"); rc = pthread_create(&thread2, NULL, increment_counter, (void*)2); check_mutex_error(rc, "pthread_create (线程2)"); // 等待线程结束 rc = pthread_join(thread1, NULL); check_mutex_error(rc, "pthread_join (线程1)"); rc = pthread_join(thread2, NULL); check_mutex_error(rc, "pthread_join (线程2)"); // 销毁互斥锁 rc = pthread_mutex_destroy(&mutex); check_mutex_error(rc, "pthread_mutex_destroy"); printf("最终计数器值: %d\n", shared_counter); return EXIT_SUCCESS; }
5. 进程共享互斥锁(Process-Shared Mutex)
- 特点:可以在多个进程之间共享的互斥锁。通常与共享内存结合使用。
- 适用场景:需要跨进程同步的场景。
- 行为:锁对象必须位于共享内存中,且在创建时需要指定
PTHREAD_PROCESS_SHARED属性。
demo:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <sys/mman.h> #include <sys/wait.h> #include <fcntl.h> #include <sys/stat.h> // 共享内存中的共享数据结构 typedef struct { int counter; pthread_mutex_t mutex; } shared_data_t; // 创建或打开共享内存区域 int create_shared_memory(const char* name, size_t size) { int fd = shm_open(name, O_CREAT | O_RDWR, 0666); if (fd == -1) { perror("shm_open"); return -1; } if (ftruncate(fd, size) == -1) { perror("ftruncate"); close(fd); return -1; } return fd; } // 初始化共享互斥锁 void init_shared_mutex(pthread_mutex_t* mutex) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(mutex, &attr); pthread_mutexattr_destroy(&attr); } // 子进程函数 void child_process(shared_data_t* shared) { for (int i = 0; i < 5; i++) { pthread_mutex_lock(&shared->mutex); // 临界区开始 printf("子进程进入临界区\n"); shared->counter++; printf("子进程增加计数器到 %d\n", shared->counter); sleep(1); // 模拟工作 printf("子进程离开临界区\n"); // 临界区结束 pthread_mutex_unlock(&shared->mutex); sleep(1); // 非临界区工作 } } int main() { const char* shm_name = "/my_shared_memory"; const size_t shm_size = sizeof(shared_data_t); // 创建或打开共享内存 int fd = create_shared_memory(shm_name, shm_size); if (fd == -1) { fprintf(stderr, "无法创建共享内存\n"); return EXIT_FAILURE; } // 映射共享内存 shared_data_t* shared = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (shared == MAP_FAILED) { perror("mmap"); close(fd); return EXIT_FAILURE; } // 初始化共享数据 shared->counter = 0; init_shared_mutex(&shared->mutex); pid_t pid = fork(); if (pid == -1) { perror("fork"); munmap(shared, shm_size); close(fd); shm_unlink(shm_name); return EXIT_FAILURE; } if (pid == 0) { // 子进程 child_process(shared); munmap(shared, shm_size); close(fd); _exit(EXIT_SUCCESS); } else { // 父进程 for (int i = 0; i < 5; i++) { pthread_mutex_lock(&shared->mutex); // 临界区开始 printf("父进程进入临界区\n"); shared->counter++; printf("父进程增加计数器到 %d\n", shared->counter); sleep(1); // 模拟工作 printf("父进程离开临界区\n"); // 临界区结束 pthread_mutex_unlock(&shared->mutex); sleep(1); // 非临界区工作 } // 等待子进程结束 wait(NULL); // 清理 munmap(shared, shm_size); close(fd); shm_unlink(shm_name); printf("最终计数器值: %d\n", shared->counter); } return EXIT_SUCCESS; }
- 使用
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED)设置互斥锁为进程间共享 - 互斥锁和计数器都存储在共享内存中
死锁(deallocks): 同一个锁重复加锁或多个锁互相等待造成的互相阻塞。
比较常见的:
(一个锁)一个进程中锁A,函数1加锁-延时-解锁,函数2加锁-延时-解锁。然后函数1里面调用函数2。如果调用的是普通互斥锁,就死锁。如果用递归或检查锁就不会。
(多个锁)demo:
#include <stdio.h> #include <pthread.h> #include <unistd.h> // 定义三个互斥锁(模拟资源) pthread_mutex_t R1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t R2 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t R3 = PTHREAD_MUTEX_INITIALIZER; // 线程 P1 的函数 void *P1(void *arg) { pthread_mutex_lock(&R1); // P1 持有 R1 printf("P1 持有 R1,等待 R2...\n"); sleep(1); // 模拟其他操作 pthread_mutex_lock(&R2); // P1 等待 R2(被 P2 持有) printf("P1 获取到 R2,继续执行...\n"); pthread_mutex_unlock(&R2); pthread_mutex_unlock(&R1); return NULL; } // 线程 P2 的函数 void *P2(void *arg) { pthread_mutex_lock(&R2); // P2 持有 R2 printf("P2 持有 R2,等待 R3...\n"); sleep(1); // 模拟其他操作 pthread_mutex_lock(&R3); // P2 等待 R3(被 P3 持有) printf("P2 获取到 R3,继续执行...\n"); pthread_mutex_unlock(&R3); pthread_mutex_unlock(&R2); return NULL; } // 线程 P3 的函数 void *P3(void *arg) { pthread_mutex_lock(&R3); // P3 持有 R3 printf("P3 持有 R3,等待 R1...\n"); sleep(1); // 模拟其他操作 pthread_mutex_lock(&R1); // P3 等待 R1(被 P1 持有) printf("P3 获取到 R1,继续执行...\n"); pthread_mutex_unlock(&R1); pthread_mutex_unlock(&R3); return NULL; } int main() { pthread_t t1, t2, t3; // 创建线程 pthread_create(&t1, NULL, P1, NULL); pthread_create(&t2, NULL, P2, NULL); pthread_create(&t3, NULL, P3, NULL); // 等待线程结束(实际上会阻塞,因为死锁) pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); // 销毁互斥锁 pthread_mutex_destroy(&R1); pthread_mutex_destroy(&R2); pthread_mutex_destroy(&R3); return 0; }
- 每个线程在持有资源的同时等待另一个线程持有的资源,形成一个循环等待。
- 由于没有其他线程释放资源,所有线程都会阻塞在
pthread_mutex_lock调用处,导致死锁。
以上2个例子都满足以下条件:
- 互斥条件(Mutual Exclusion):资源一次只能由一个进程使用。(指的是普通互斥锁,如果锁已经被持有,重复加锁会阻塞等待)
- 占有并等待(Hold and Wait):进程在持有至少一个资源的同时,等待获取其他进程持有的资源。
- 非抢占条件(No Preemption):已分配给进程的资源不能被强制释放,只能由进程主动释放。
- 循环等待条件(Circular Wait):存在一个进程等待循环,即进程 P1 等待进程 P2 持有的资源,进程 P2 等待进程 P3 持有的资源,...,进程 Pn 等待进程 P1 持有的资源。
当这四个条件同时满足时,系统就可能发生死锁。
如何解决死锁:
解决死锁的方法主要分为死锁预防、死锁避免、死锁检测与恢复、以及死锁忽略四种策略:
1. 死锁预防(Deadlock Prevention)
通过破坏死锁产生的四个必要条件之一或多个来预防死锁的发生:
- 破坏互斥条件:通过允许资源被多个进程同时访问(如使用读写锁),但并非所有资源都适合这种方式。
- 破坏占有并等待条件:要求进程在请求资源时,一次性申请所有需要的资源,若无法全部获得则释放已占有的资源。
- 破坏非抢占条件:允许系统强制从进程手中夺取资源,但实现复杂且可能影响性能。
- 破坏循环等待条件:对资源进行编号,要求进程按顺序申请资源,避免形成循环等待。
一次性申请所需资源demo:
#include <stdio.h> #include <pthread.h> #include <unistd.h> // 定义三个互斥锁(模拟资源) pthread_mutex_t R1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t R2 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t R3 = PTHREAD_MUTEX_INITIALIZER; // 线程函数 void *process(void *arg) { // 尝试一次性申请所有资源 if (pthread_mutex_trylock(&R1) == 0 && pthread_mutex_trylock(&R2) == 0 && pthread_mutex_trylock(&R3) == 0) { printf("进程成功获取所有资源 R1, R2, R3\n"); // 模拟使用资源 sleep(1); // 释放资源 pthread_mutex_unlock(&R1); pthread_mutex_unlock(&R2); pthread_mutex_unlock(&R3); printf("进程释放所有资源 R1, R2, R3\n"); } else { // 如果无法获取所有资源,释放已持有的资源 pthread_mutex_unlock(&R1); pthread_mutex_unlock(&R2); pthread_mutex_unlock(&R3); printf("进程无法获取所有资源,释放已持有的资源\n"); } return NULL; } int main() { pthread_t t1, t2; // 创建线程 pthread_create(&t1, NULL, process, NULL); pthread_create(&t2, NULL, process, NULL); // 等待线程结束 pthread_join(t1, NULL); pthread_join(t2, NULL); // 销毁互斥锁 pthread_mutex_destroy(&R1); pthread_mutex_destroy(&R2); pthread_mutex_destroy(&R3); return 0; }
破坏循环等待条件:
#include <stdio.h> #include <pthread.h> #include <unistd.h> // 定义三个互斥锁(模拟资源),并编号 pthread_mutex_t R1 = PTHREAD_MUTEX_INITIALIZER; // 编号 1 pthread_mutex_t R2 = PTHREAD_MUTEX_INITIALIZER; // 编号 2 pthread_mutex_t R3 = PTHREAD_MUTEX_INITIALIZER; // 编号 3 // 线程 P1 的函数(需要 R1 和 R2) void *P1(void *arg) { pthread_mutex_lock(&R1); // 先申请编号小的资源 R1 printf("P1 持有 R1,等待 R2...\n"); sleep(1); // 模拟其他操作 pthread_mutex_lock(&R2); // 再申请编号大的资源 R2 printf("P1 获取到 R2,继续执行...\n"); pthread_mutex_unlock(&R2); pthread_mutex_unlock(&R1); return NULL; } // 线程 P2 的函数(需要 R2 和 R3) void *P2(void *arg) { pthread_mutex_lock(&R2); // 先申请编号小的资源 R2 printf("P2 持有 R2,等待 R3...\n"); sleep(1); // 模拟其他操作 pthread_mutex_lock(&R3); // 再申请编号大的资源 R3 printf("P2 获取到 R3,继续执行...\n"); pthread_mutex_unlock(&R3); pthread_mutex_unlock(&R2); return NULL; } // 线程 P3 的函数(需要 R3 和 R1) void *P3(void *arg) { pthread_mutex_lock(&R1); // 先申请编号小的资源 R1 printf("P3 持有 R1,等待 R3...\n"); sleep(1); // 模拟其他操作 pthread_mutex_lock(&R3); // 再申请编号大的资源 R3 printf("P3 获取到 R3,继续执行...\n"); pthread_mutex_unlock(&R3); pthread_mutex_unlock(&R1); return NULL; } int main() { pthread_t t1, t2, t3; // 创建线程 pthread_create(&t1, NULL, P1, NULL); pthread_create(&t2, NULL, P2, NULL); pthread_create(&t3, NULL, P3, NULL); // 等待线程结束 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); // 销毁互斥锁 pthread_mutex_destroy(&R1); pthread_mutex_destroy(&R2); pthread_mutex_destroy(&R3); return 0; }
2. 死锁避免(Deadlock Avoidance)
在资源分配前,通过算法(如银行家算法)动态检查资源分配是否会导致系统进入不安全状态,从而避免死锁。
- 银行家算法:在分配资源前,模拟分配过程,检查分配后系统是否仍处于安全状态(即是否存在一个安全序列,使得所有进程都能按顺序完成)。
3. 死锁检测与恢复(Deadlock Detection and Recovery)
允许死锁发生,但系统定期检测死锁,并在检测到死锁后采取措施恢复:
- 死锁检测:使用资源分配图等算法检测系统中是否存在死锁。
- 死锁恢复:
- 进程终止:终止一个或多个进程,释放其持有的资源。
- 资源抢占:从一个或多个进程那里抢占资源,分配给其他进程。
4. 死锁忽略(Deadlock Ignorance)
许多操作系统(包括 Linux)选择忽略死锁问题,认为死锁是小概率事件,且处理死锁的开销可能大于死锁本身带来的影响。当死锁发生时,由管理员手动干预解决。
在 Linux 中,如何调试死锁问题?
- 使用
gdb调试,查看进程的调用栈。 - 使用
strace跟踪系统调用,查看进程在等待什么资源。 - 使用
lsof查看进程打开的文件和锁。 - 使用
pstack查看线程的堆栈信息。
2.读写锁
读锁(共享锁):
- 多个线程可以同时获取读锁。
- 读锁与写锁互斥(即:读锁存在时,不能获取写锁;写锁存在时,不能获取读锁)
写锁(排他锁)
- 只有一个线程可以获取写锁。
- 写锁与读锁和写锁都互斥。
适用场景:对共享资源,读多写少。
demo:
#include <stdio.h> #include <pthread.h> #include <unistd.h> // 共享资源 int shared_data = 0; // 读写锁 pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; // 读线程函数 void *reader_thread(void *arg) { int thread_id = *(int *)arg; for (int i = 0; i < 5; i++) { // 获取读锁 pthread_rwlock_rdlock(&rwlock); printf("Reader %d: shared_data = %d\n", thread_id, shared_data); // 释放读锁 pthread_rwlock_unlock(&rwlock); sleep(1); // 模拟读取操作耗时 } return NULL; } // 写线程函数 void *writer_thread(void *arg) { int thread_id = *(int *)arg; for (int i = 0; i < 3; i++) { // 获取写锁 pthread_rwlock_wrlock(&rwlock); shared_data++; printf("Writer %d: updated shared_data to %d\n", thread_id, shared_data); // 释放写锁 pthread_rwlock_unlock(&rwlock); sleep(2); // 模拟写入操作耗时 } return NULL; } int main() { pthread_t readers[3], writers[2]; int reader_ids[3] = {1, 2, 3}; int writer_ids[2] = {1, 2}; // 创建读线程 for (int i = 0; i < 3; i++) { pthread_create(&readers[i], NULL, reader_thread, &reader_ids[i]); } // 创建写线程 for (int i = 0; i < 2; i++) { pthread_create(&writers[i], NULL, writer_thread, &writer_ids[i]); } // 等待读线程结束 for (int i = 0; i < 3; i++) { pthread_join(readers[i], NULL); } // 等待写线程结束 for (int i = 0; i < 2; i++) { pthread_join(writers[i], NULL); } // 销毁读写锁 pthread_rwlock_destroy(&rwlock); return 0; }
读锁和写锁都通过 pthread_rwlock_unlock(&rwlock) 释放。
3.自旋锁
自旋锁与互斥锁功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁。因此会过多的占用CPU资源。
自旋锁的特点:
- 忙等待(Busy Waiting):
- 自旋锁通过循环不断尝试获取锁,而不是让线程进入睡眠状态。
- 适用于锁的持有时间非常短的场景(通常为几个 CPU 周期)。
- 无上下文切换开销:
- 由于线程不会进入睡眠状态,因此避免了上下文切换的开销。
- 但会占用 CPU 资源。
- 不可阻塞:
- 自旋锁不能用于可能长时间阻塞的场景(如 I/O 操作)。
- 通常用于内核或高性能用户态程序
自旋锁比较少用到,因此在这边就不进行介绍了。
4.条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分 为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局 变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在 互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
条件变量用来阻塞线程等待某个事件的发生,并且当等待的事件发生时,阻塞线程会被通知。互斥锁的缺点是只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
作用
- 线程协调:
- 条件变量允许线程在某个条件不满足时主动放弃 CPU 资源并进入睡眠状态。
- 其他线程可以在条件满足时通知等待的线程,从而避免忙等待(Busy Waiting)。
- 避免竞争条件:
- 条件变量通常与互斥锁结合使用,确保对共享条件的检查是线程安全的。
- 提高效率:
- 避免线程在条件不满足时无意义地占用 CPU 资源。
条件变量的初始化:由pthread_cond_t数据类型表示的条件变量可以用两种方式进行初始化,可以用常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量。也可以动态的使用pthread_cond_init函数对它进行初始化。
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict arrt);
int pthread_cond_destroy(pthread_cond_t *cond);
返回值: 成功:0;错误:错误编号使用默认属性的时候,将arrt设置为NULL。
尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。
pthread_cond_destroy只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict tspir);
返回值: 成功:0; 失败:错误编号
time的时间是绝对时间,不是相对时间
#include <pthread.h> int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); 返回值: 成功:0;失败:错误编号
pthread_cond_signal函数至少能唤醒一个等待该条件的线程,而pthread_cond_broadcast函数则能唤醒等待该条件的所有线程。需要注意的是:一定要在改变条件状态以后再给线程发信号。
条件变量的使用可以分为两部分:
等待线程:
使用pthread_cond_wait前要先加锁;
pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活;
pthread_cond_wait被激活后会再自动加锁;
激活线程:
加锁(和等待线程用同一个锁);
pthread_cond_signal发送信号;
解锁;
demo:
/*借助条件变量模拟 生产者-消费者 问题*/ #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <stdio.h> /*链表作为公享数据,需被互斥量保护*/ struct msg { struct msg *next; int num; }; struct msg *head; /* 静态初始化 一个条件变量 和 一个互斥量*/ pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void *consumer(void *p) { struct msg *mp; for (;;) { pthread_mutex_lock(&lock); while (head == NULL) { //头指针为空,说明没有节点 pthread_cond_wait(&has_product, &lock); } mp = head; head = mp->next; //模拟消费掉一个产品 pthread_mutex_unlock(&lock); printf("-Consume %lu---%d\n", pthread_self(), mp->num); free(mp); sleep(rand() % 4); } } void *producer(void *p) { struct msg *mp; for (;;) { mp = malloc(sizeof(struct msg)); mp->num = rand() % 1000 + 1; //模拟生产一个产品 printf("-Produce -------------%d\n", mp->num); pthread_mutex_lock(&lock); mp->next = head; head = mp; pthread_mutex_unlock(&lock); pthread_cond_signal(&has_product); //将等待在该条件变量上的一个线程唤醒 sleep(rand() % 4); } } int main(int argc, char *argv[]) { pthread_t pid, cid; srand(time(NULL)); pthread_create(&pid, NULL, producer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_join(pid, NULL); pthread_join(cid, NULL); return 0; }
条件变量的优点:
相较于mutex而言,条件变量可以减少竞争。如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。
有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
红字1:
需要在条件变量进行判断时,将变量锁住,让其他线程不能修改此变量,这样就可以保证在判断的时候条件的变量的值是正确的。即互斥锁的作用不是为了保护条件变量,而是为了保护条件判断时共享变量的值不会被修改。
条件变量判断为什么使用while而不是if?
因为唤醒中存在虚假唤醒( spurious wakeup ),换言之,条件尚未满足, pthread_cond_wait 就返回了。在一些实现中,即使没有其他线程向条件变量发送信号,等待此条件变量的线程也有可能会醒来。
为什么还要把互斥锁作为参数传给 pthread_cond_wait 呢?
pthread_mutex_lock(&m) while(condition_is_false) { pthread_mutex_unlock(&m); // 解锁之后,等待之前,可能条件已经满足,信号已经发出,但是该信号可能会被错过 cond_wait(&cv); pthread_mutex_lock(&m); }
在pthread_cond_wait内部,会对传入的互斥锁进行解锁操作,然后再让线程进入等待状态。
1. 防止条件检查和线程休眠之间的时间窗口导致错过条件变化
在调用pthread_cond_wait之前,线程已经持有互斥锁并检查了条件。如果不解锁直接进入等待,其他线程将无法获取互斥锁来修改共享条件,导致条件永远无法满足,等待线程可能永远阻塞。通过解锁,其他线程可以获取互斥锁并修改共享条件,从而可能满足等待线程的条件。
2. 保证条件检查的原子性
在调用pthread_cond_wait之前,线程需要先持有互斥锁来检查条件。如果条件不满足,线程调用pthread_cond_wait,此时会解锁互斥锁并进入等待状态。这样做的目的是确保从条件检查到进入等待状态的过程中,其他线程无法修改共享条件,从而避免条件被错过。
3. 等待线程被唤醒后需要重新获取互斥锁
当等待线程被唤醒时,pthread_cond_wait会重新获取互斥锁。这样做的目的是确保线程在检查条件或操作共享资源时,互斥锁是持有的,从而保证线程安全。
5.信号量
信号量是一个特殊类型的变量,这个信号量可以被增加或者减少,当信号量大于0时,代表资源可以被访问。当访问完后,信号量减1。当信号量为0时,要想访问信号量就要等待(阻塞)。根据信号量的值可以分为:二进制信号量和计数信号量。
信号量的创建/销毁:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t *sem);
- sem:信号量
- pshared:0:线程同步 1:进程同步
- value:信号量初始值
- 返回值:成功返回0,错误返回错误码
信号量的访问/释放:
#include <semaphore.h> int sem_wait(sem_t *sem); int sem_post(sem_t *sem);
wait会对信号量减1,如果信号量大于0,就直接减1。如果信号量等于0,就会等待。
post会对信号量加1。
demo:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <semaphore.h> void *thread_function(void *arg); sem_t g_sem; int main() { int ret; pthread_attr_t thread_attr; pthread_t thread; ret = pthread_attr_init(&thread_attr); if(ret != 0) { perror("ptherad attribute init failed"); exit(EXIT_FAILURE); } ret = sem_init(&g_sem, 0, 0); if(ret != 0) { perror("semaphore init failed"); exit(EXIT_FAILURE); } ret = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); if(ret != 0) { perror("setting thread detached state failure"); exit(EXIT_FAILURE); } ret = pthread_create(&thread, &thread_attr, thread_function, NULL); if(ret != 0) { perror("thread create failed"); exit(EXIT_FAILURE); } ret = pthread_attr_destroy(&thread_attr); if(ret !=0) { perror("thread attr destory failed"); exit(EXIT_FAILURE); } printf("ready to wait sem \n"); sem_wait(&g_sem); sleep(2); printf("the main thread is finish \n"); exit(EXIT_SUCCESS); } void *thread_function(void *arg) { sleep(3); printf("finish thread function \n"); sem_post(&g_sem); pthread_exit(NULL); }
这里需要注意的一点是:与线程有关的函数绝大多数名字都是以“pthread_”打头的 要使用这些函数,除了要引入相应的头文件,链接这些线程函数库时要使用编译器命令的-lpthread选项
浙公网安备 33010602011771号