在进行程序开发过程中,错误使用了pthread_mutex_lock导致程序概率性的奔溃,奔溃时报如下错误:
问题分析:
本文分析在Linux应用程序中错误使用pthread_mutex锁时会概率性触发SIG_ABRT信号而导致程序崩溃(库打印输出 :Assertion `mutex->__data.__owner == 0' failed)的原因。
首先给出出错的示例程序:
#include <stdio.h>
#include <unistd.h>
#include "pthread.h"
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void * process(void * arg)
{
fprintf(stderr, "Starting process %s\n", (char *) arg);
while (1) {
/* 加锁等待某些资源 */
pthread_mutex_lock(&lock);
fprintf(stderr, "Process %s lock mutex\n", (char *) arg);
/* 加锁成功表示资源就绪 */
usleep(1000);
/* do something */
}
return NULL;
}
int main(void)
{
pthread_t th_a, th_b;
int ret = 0;
ret = pthread_create(&th_a, NULL, process, "a");
if (ret != 0) fprintf(stderr, "create a failed %d\n", ret);
ret = pthread_create(&th_b, NULL, process, "b");
if (ret != 0) fprintf(stderr, "create b failed %d\n", ret);
while (1) {
/* 等待并检测某些资源就绪 */
/* something */
/* 解锁告知线程资源就绪 */
pthread_mutex_unlock(&lock);
fprintf(stderr, "Main Process unlock mutex\n");
}
return 0;
}
本示例程序中,main函数首先创建两个线程,然后主线程等待某些资源就绪(伪代码,程序中未体现),待就绪后解锁mutex lock以告知子线程可以执行相应的处理(在解锁后打印输出解锁成功),不断循环;创建出的两个线程均调用process函数,该函数会尝试加锁mutex lock,加锁成功则表示资源就绪可以处理(打印输出加锁成功),否则在锁上等待,亦往复循环。本程序中对mutex锁的用法特殊,并不对临界资源进行保护,而是作为线程间”生产---消费“同步功能的一个简化示例,加锁以等待资源就绪,解锁以通知资源就绪,加锁和解锁的操作分别在不同的线程中执行。
运行该程序后不到10s时间程序就会出错退出,并且触发SIG_ABRT信号,终端打印输出如下:
......
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Process b lock mutex
Process a lock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Process b lock mutex
pthread_test: pthread_mutex_lock.c:62: __pthread_mutex_lock: Assertion `mutex->__data.__owner == 0' failed.
Aborted
程序在Glibc库中的pthread_mutex_lock.c的第62行__pthread_mutex_unlock()函数中出错,程序ABRT退出。
下面先来分析对应的源码,首先是加锁流程:
加锁函数源码:
int
__pthread_mutex_lock (mutex)
pthread_mutex_t *mutex;
{
assert (sizeof (mutex->__size) >= sizeof (mutex->__data));
unsigned int type = PTHREAD_MUTEX_TYPE (mutex);
if (__builtin_expect (type & ~PTHREAD_MUTEX_KIND_MASK_NP, 0))
return __pthread_mutex_lock_full (mutex);
pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP)
== PTHREAD_MUTEX_TIMED_NP) //1---判断锁类型
{
simple:
/* Normal mutex. */
LLL_MUTEX_LOCK (mutex); //2---加锁(原子操作)
assert (mutex->__data.__owner == 0); //3---Owner判断
}
...
/* Record the ownership. */
mutex->__data.__owner = id; //4---Owner赋值
#ifndef NO_INCR
++mutex->__data.__nusers;
#endif
return 0;
}
加锁函数的主要4步操作已经列出,首先会判断锁的类型,这里仅对PTHREAD_MUTEX_TIMED_NP类型的锁做出分析,该该类型的锁为默认的锁类型,当一个线程加锁后其余请求锁的线程会排入一个等待队列,并在锁解锁后按优先级获得锁。然后程序调用LLT_MUTEX_LOCK()宏执行底层加锁动作,这个加锁流程是原子的且不同的架构实现并不相同,然后会判断是否已经有线程获取了该锁(因为PTHREAD_MUTEX_TIMED_NP类型的锁是不允许嵌套加锁的),若已经有线程获取了锁则出错退出(示例程序中就是在此出错的),在函数的最后会把当前获得锁的线程号赋给__owner字段(线程与锁绑定)就结束了,此时当前线程进入临界区,其他对锁请求的线程将阻塞。下面来看一下解锁流程:
解锁函数源码:
int
internal_function attribute_hidden
__pthread_mutex_unlock_usercnt (mutex, decr)
pthread_mutex_t *mutex;
int decr;
{
int type = PTHREAD_MUTEX_TYPE (mutex);
if (__builtin_expect (type & ~PTHREAD_MUTEX_KIND_MASK_NP, 0))
return __pthread_mutex_unlock_full (mutex, decr);
if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP) //1---判断锁类型
== PTHREAD_MUTEX_TIMED_NP)
{
/* Always reset the owner field. */
normal:
mutex->__data.__owner = 0; //2---Owner解除
if (decr)
/* One less user. */
--mutex->__data.__nusers;
/* Unlock. */
lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex)); //3---原子解锁
return 0;
}
...
}
解锁函数的3步主要操作如上,首先依旧是判断锁类型,然后解除锁和线程的绑定关系,最后就调用lll_unlock()函数原子的解锁,此时若有加锁线程需要获取锁,相应线程会从
LLT_MUTEX_LOCK()函数返回继续执行。
以上就是调用pthread mutex函数加解锁函数的主要流程,其中需要关注的一点就是这两个函数的执行并不是原子的,是可能存在上下文切换动作的。在通常的用法中,我们加锁操作一般都是为了保护临界资源不被重入改写,一半都是严格按照“加锁-->写入/读取临界资源-->解锁”的流程执行(由加锁的线程负责解锁),而从前文中分析的__pthread_mutex_lock()和__pthread_mutex_unlock_usercnt()函数中也可以看到,只有在原子加锁期间才会改变这__owner值(该值也可认为是临界资源的一部分而被保护起来了),因此是不可能出现加锁已经加锁的线程的,所以也不会调用assert()函数而退出程序的。
但是本程序中对锁的用法显然并不这么“一般”,而是作为一种线程间的同步功能使用。其中主进程中不停的解锁,即是线程A和B没有加锁也同样如此,而线程A和B会竞争的每隔一定时间去加锁,那么就有可能出现如下图中所示的一种情况:1、
该图中主进程待资源就绪后正在解锁一个未被加锁的mutex_lock时发成了线程切换,线程A打断解锁流程完成了一整个加锁的流程,随后线程又且换回了主进程继续执行真正的解锁操作,这样线程A所加的锁就被莫名其妙的解掉了(关键的一点),此时若线程B在等待该锁,则会进入到加锁流程,从而在加锁成功后崩溃在这个__owner判断上。其实该程序出错的主要原因即是解了并未加锁的mutex_lock,如若主进程解得锁是已经上了锁的,则线程A是没有机会加锁的,主进程会原子的完成整个mutex_unlock动作。
另外,其实可以适当的调整程序再来看一下另外一种可能的情形(两个执行流),同样是“线程间同步”用法:2、
这种情况就是在资源就绪较慢且资源处理较快的情况容易出现崩溃,同样是概率性出现的。最后来看第三种可能的情况:3、
这种情况崩溃出现在线程A加锁的过程中被主进程解锁,然后线程A或其他线程又一次加锁的时候。其实不论上述哪一种同步的情况,其出错的原因有两点:(1)解了未被上锁的锁;(2)A线程加的锁由其他线程去解,进一步分析就是没有严格按照“加锁-->解锁”的流程使用mutex锁。
最后对于以上这种“线程间同步”的使用方法可以使用条件变量或者是信号量实现而不要使用mutex锁,mutex锁一般被用在保护线程间临界资源的情况下。
总结:
1、不要去解锁一个未被加锁的mutex锁;
2、不要一个线程中加锁而在另一个线程中解锁;
3、使用mutex锁用于保护临界资源,严格按照“加锁-->写入/读取临界资源-->解锁”的流程执行,对于线程间同步的需求使用条件变量或信号量实现。