互斥锁在默认属性的情况下使用,一般需要关注死锁的情况。所谓死锁,即互斥锁无法解除同时也无法加持,导致程序可能会无限阻塞的情况。有时,一个线程可能会同时访问多个不同的共享资源,而每个共享资源都需要有不同互斥锁管理。那么在不经意间程序编写极容易造成死锁的情况。造成死锁的原因有很多:
- 在互斥锁默认属性的情况下,在同一个线程中不允许对同一互斥锁连续进行加锁操作。因为之前锁处于未解除状态,如果再次对同一个互斥锁进行加锁,那么必然会导致程序无限阻塞等待。
- 多个线程对多个互斥锁交叉使用,每一个线程都试图对其他线程所持有的互斥锁进行加锁。如下图所示情况,线程分别持有了对方需要的锁资源,并相互影响,可能会导致程序无限阻塞,就会造成死锁。
同时,还需要注意的是在一个线程中操作多个互斥锁时,加锁与解锁的顺序一定是相反的,否则也会导致错误。如上图所示,如果线程先加锁 1
,后加锁 2
,之后一定要先解锁 2
,再解锁 1
。
- 一个持有互斥锁的线程被其他线程取消,其他线程将无法获得该锁,则会造成死锁。具体如下所示:
#include <pthread.h> #include <stdio.h> #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) int value1 = 0; int value2 = 0; int count = 0; pthread_mutex_t lock; void *thread1_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); value1 = count; value2 = count; count++; sleep(3); pthread_mutex_unlock(&lock); } pthread_exit(0); } void *thread2_handler(void *arg) { while (1) { sleep(1); pthread_mutex_lock(&lock); if (value1 != value2) { sleep(1); printf("value1 = %d value2 = %d\n", value1, value2); } pthread_mutex_unlock(&lock); } pthread_exit(0); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2; if(pthread_mutex_init(&lock,NULL)!=0){ errlog("pthread_mutex_init error"); } if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } sleep(2); pthread_cancel(thread1); pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_mutex_destroy(&lock); return 0; }点击复制复制失败已复制
上述代码对线程进行延迟处理,确保线程 1
先获取互斥锁,并在线程 1
持有互斥锁期间被取消。此时线程 1
使用的互斥锁将无法被获取,造成死锁。
为了规避这类问题,线程可以设置一个或多个清理函数,当线程遭取消时会自动运行这些函数。在线程终止前可执行诸如修改全局变量、解锁等操作。每一个线程都有一个清理函数栈。当线程遭取消时,会沿该栈顶自顶向下依次执行清理函数。当执行完所有的清理函数后,线程终止。 pthread_cleanup_push()
函数和 pthread_cleanup_pop()
函数分别负责向调用线程的清理函数栈添加和移除清理函数。
#include <pthread.h> void pthread_cleanup_push(void (*routine)(void *), void *arg); void pthread_cleanup_pop(int execute);点击复制复制失败已复制
执行 pthread_cleanup_push()
函数会将参数 routine
所含的函数地址添加到调用线程的清理函数栈顶。 arg
作为调用函数的参数,传递给 routine
。
pthread_cleanup_pop()
函数且参数 execute
为非零。本笔记讨论的正是线程被取消的情况,将清除函数的功能设置为解除互斥锁,这样其他线程就不会陷入无限期等待状态。具体代码如下所示:
#include <pthread.h> #include <stdio.h> #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) int value1 = 0; int value2 = 0; int count = 0; pthread_mutex_t lock; void cleanup_handler(void *arg) { pthread_mutex_unlock(&lock); } void *thread1_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); pthread_cleanup_push(cleanup_handler, NULL); value1 = count; value2 = count; count++; sleep(3); pthread_cleanup_pop(0); pthread_mutex_unlock(&lock); } pthread_exit(0); } void *thread2_handler(void *arg) { while (1) { sleep(1); pthread_mutex_lock(&lock); if (value1 == value2) { sleep(1); printf("value1 = %d value2 = %d\n", value1, value2); } pthread_mutex_unlock(&lock); } pthread_exit(0); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2; if (pthread_mutex_init(&lock, NULL) != 0) { errlog("pthread_mutex_init error"); } if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } sleep(2); pthread_cancel(thread1); pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_mutex_destroy(&lock); return 0; }点击复制复制失败已复制
上述代码当线程 1
被取消自动解除互斥锁之后,线程 2
获得该锁,此时 value1
与 value2
的值一定相等,且为 0
。通过运行结果也可验证这一点:
$ gcc main.c && ./a.out value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0 value1 = 0 value2 = 0