尽量设置 recursive 属性以初始化 Linux 的互斥变量
在默认情况下,Linux 下的同一线程无法对同一互斥锁进行递归加速,否则将发生死锁。
递归加锁,就是在同一线程中试图对互斥锁进行两次或两次以上的行为。
其场景在 Linux 平台上的代码:
// 通过默认条件建锁
pthread_mutex_t *theMutex = new pthread_mutex_t;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutex_init(theMutex,&attr);
pthread_mutexattr_destroy(&attr);
// 递归加锁
pthread_mutex_lock (theMutex);
pthread_mutex_lock (theMutex);
pthread_mutex_unlock (theMutex);
pthread_mutex_unlock (theMutex);
在以上代码场景中,问题将出现在第二次加锁操作。由于在默认情况下,Linux 不允许同一线程递归加锁,因此在第二次加锁操作时线程将出现死锁。
Linux 互斥变量这种奇怪的行为或许对于特定的某些场景会所有用处,但是对于大多数情况下看起来更像是程序的一个 bug 。毕竟,在同一线程中对同一互斥锁进行递归加锁在尤其是二次开发中经常会需要。
这个问题与互斥锁的中的默认 recursive 属性有关。解决问题的方法就是显式地在互斥变量初始化时将设置起 recursive 属性。基于此,以上代码其实稍作修改就可以很好的运行,只需要在初始化锁的时候加设置一个属性。
pthread_mutexattr_init(&attr);
// 设置 recursive 属性
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(theMutex,&attr);
因此,建议尽量设置 recursive 属性以初始化 Linux 的互斥锁,这样既可以解决同一线程递归加锁的问题,又可以避免很多情况下死锁的发生。这样做还有一个额外的好处,就是可以让 Windows 和 Linux 下让锁的表现统一。
注意 Linux 平台上触发条件变量的自动复位问题
条件变量的置位和复位有两种常用模型:
第一种模型是当条件变量置位(signaled)以后,如果当前没有线程在等待,其状态会保持为置位(signaled),直到有等待的线程进入被触发,其状态才会变为复位(unsignaled),这种模型的采用以 Windows 平台上的 Auto-set Event 为代表。
第二种模型则是 Linux 平台的 Pthread 所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。
具体来说,Linux 平台上 Pthread 下的条件变量状态变化模型是这样工作的:调用 pthread_cond_signal() 释放被条件阻塞的线程时,无论存不存在被阻塞的线程,条件都将被重新复位,下一个被条件阻塞的线程将不受影响。
// 提示出租车到达的条件变量
pthread_cond_t taxiCond;
// 同步锁
pthread_mutex_t taxiMutex;
// 旅客到达等待出租车
void * traveler_arrive(void * name) {
cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
pthread_mutex_lock(&taxiMutex);
pthread_cond_wait (&taxiCond, &taxtMutex);
pthread_mutex_unlock (&taxtMutex);
cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
pthread_exit( (void *)0 );
}
// 出租车到达
void * taxi_arrive(void *name) {
cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;
pthread_cond_signal(&taxtCond);
pthread_exit( (void *)0 );
}
void main() {
// 初始化
taxtCond= PTHREAD_COND_INITIALIZER;
taxtMutex= PTHREAD_MUTEX_INITIALIZER;
pthread_t thread;
pthread_attr_t threadAttr;
pthread_attr_init(&threadAttr);
pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));
sleep(1);
pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));
sleep(1);
pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));
sleep(1);
return 0;
}
程序结果输出
Taxi Jack arrives.
Traveler Susan needs a taxi now!
Taxi Mike arrives.
Traveler Susan now got a taxi.
采用 Linux 条件变量模型的出租车实例流程
Linux 平台,问题就来了,Jack 到了站台一看没人,触发的条件变量被直接复位,于是 Jack 排在等待队列里面。来迟一秒的 Susan 小姐到了站台却看不到在那里等待的 Jack,只能等待,直到 Mike 开车赶到,重新触发条件变量,Susan 才上了 Mike 的车。这对于在排队系统前面的 Jack 是不公平的,而问题症结是在于 Linux 平台上条件变量触发的自动复位引起的一个 Bug 。
条件变量在 Linux 平台上的这种模型很难说好坏。但是在实际开发中,我们可以对代码稍加改进就可以避免这种差异的发生。由于这种差异只发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查下该变量即可。改进后 Linux 函数:
// 提示出租车到达的条件变量
pthread_cond_t taxiCond;
// 同步锁
pthread_mutex_t taxiMutex;
// 旅客人数,初始为 0
int travelerCount=0;
// 旅客到达等待出租车
void * traveler_arrive(void * name) {
cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
pthread_mutex_lock(&taxiMutex);
// 提示旅客人数增加
travelerCount++;
pthread_cond_wait (&taxiCond, &taxiMutex);
pthread_mutex_unlock (&taxiMutex);
cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
pthread_exit( (void *)0 );
}
// 出租车到达
void * taxi_arrive(void *name)
{
cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;
while(true)
{
pthread_mutex_lock(&taxiMutex);
// 当发现已经有旅客在等待时,才触发条件变量
if(travelerCount>0)
{
pthread_cond_signal(&taxtCond);
pthread_mutex_unlock (&taxiMutex);
break;
}
pthread_mutex_unlock (&taxiMutex);
}
pthread_exit( (void *)0 );
}
因此我们建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。
注意条件返回时互斥锁的解锁问题
Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。
void * traveler_arrive(void * name) {
cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
pthread_mutex_lock(&taxiMutex);
pthread_cond_wait (&taxiCond, &taxtMutex);
pthread_mutex_unlock (&taxtMutex);
cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
pthread_exit( (void *)0 );
}
等待的绝对时间问题
超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。但是如果你是第一次使用这个 API 时,首先你需要了解的就是这个 API 当中超时参数的特殊性(就如本节标题所提示的那样)。我们首先来看一下这个 API 的定义。
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。
假设我们指定相对的超时时间参数如 dwMilliseconds (单位毫秒)来调用和超时相关的函数,这样就需要将 dwMilliseconds 转化为 Linux 下的绝对时间参数 abstime 使用。
/* get the current time */
struct timeval now;
gettimeofday(&now, NULL);
/* add the offset to get timeout value */
abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000;
abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;
Linux 的绝对时间看似简单明了,却是开发中一个非常隐晦的陷阱。而且一旦你忘了时间转换,可以想象,等待你的错误将是多么的令人头疼:如果忘了把相对时间转换成绝对时间,相当于你告诉系统你所等待的超时时间是过去式的 1970 年 1 月 1 号某个时间段,于是操作系统毫不犹豫马上送给你一个 timeout 的返回值,然后你会举着拳头抱怨为什么另外一个同步线程耗时居然如此之久,并一头扎进寻找耗时原因的深渊里。
正确处理 Linux 平台下的线程结束问题
在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。
int pthread_join(pthread_t th, void **thread_return);
另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, THREAD_FUNCTION, arg);
总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。
参照文档:
http://www.ibm.com/developerworks/cn/linux/l-cn-mthreadps/index.html