清单 5. 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 进行条件变量等待操作时,我们增加一个互斥变量参数是必要的,这是为了避免线程间的竞争和饥饿情况。但是当条件等待返回时候,需要注意的是一定不要遗漏对互斥变量进行解锁。 Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。正如前文中乘客等待出租车的 Linux 代码如清单 6 所示: 清单 6. 条件变量返回后的解锁实例 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 ); } 这一点对于熟悉 Windows 平台多线程开发的开发者来说尤为重要。 Windows 上的 SignalObjectAndWait() 函数是常与 Linux 平台上的 pthread_cond_wait() 函数被看作是跨平台编程时的一对等价函数。但是需要注意的是,两个函数退出时的状态是不一样的。在 Windows 平台上,SignalObjectAndWait(HANDLE a, HANDLE b, …… ) 方法在调用结束返回时的状态是 a 和 b 都是置位(signaled)状态,在普遍的使用方法中,a 经常是一个 Mutex 变量,在这种情况下,当返回时,Mutex a 处于解锁状态(signaled),Event b 处于置位状态(signaled), 因此,对于 Mutex a 而言,我们不需要考虑解锁的问题。而且,在 SignalObjectAndWait() 之后,如果需要对临界区数据进行重新访问,都需要调用 WaitForSingleObject() 重新加锁。这一点刚好与 Linux 下的 pthread_cond_wait() 完全相反。 Linux 对于 Windows 的这一点额外解锁的操作区别很重要,一定得牢记。否则从 Windows 移植到 Linux 上的条件等待操作一旦忘了结束后的解锁操作,程序将肯定会发生死锁。 等待的绝对时间问题 超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。但是如果你是第一次使用这个 API 时,首先你需要了解的就是这个 API 当中超时参数的特殊性(就如本节标题所提示的那样)。我们首先来看一下这个 API 的定义。 pthread_cond_timedwait() 定义请看清单 7 。 清单 7. pthread_cond_timedwait() 函数定义 int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); 参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。这对于拥有 Windows 平台线程开发经验的人来说可能尤为困惑。因为 Windows 平台下所有的 API 等待参数(如 SignalObjectAndWait,等)都是相对时间, 假设我们指定相对的超时时间参数如 dwMilliseconds (单位毫秒)来调用和超时相关的函数,这样就需要将 dwMilliseconds 转化为 Linux 下的绝对时间参数 abstime 使用。常用的转换方法如清单 8 所示: 清单 8. 相对时间到绝对时间转换实例 /* 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() 来获得另一个线程的终止状态并且释放该线程所占的资源。 Pthread_join() 函数的定义如清单 9 。 清单 9. pthread_join 函数定义 int pthread_join(pthread_t th, void **thread_return); 调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。 如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。其申明如清单 10 。 清单 10. pthread_detach 函数定义 int pthread_detach(pthread_t th); 另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如清单 11 。 清单 11. 创建 detach 线程代码实例 ………………………………… .. 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() 函数来对其进行资源回收。 总结与补充 本文以上部分详细介绍了 Linux 的多线程编程的 5 条高效开发经验。另外你也可以考虑尝试其他一些开源类库来进行线程开发。 1. Boost 库 Boost 库来自于由 C++ 标准委员会类库工作组成员发起,致力于为 C++ 开发新的类库的 Boost 组织。虽然该库本身并不是针对多线程而产生,但是发展至今,其已提供了比较全面的多线程编程的 API 支持。 Boost 库对于多线程支持的 API 风格上更类似于 Linux 的 Pthread 库,差别在于其将线程,互斥锁,条件等线程开发概念都封装成了 C++ 类,以方便开发调用。 Boost 库目前对跨平台支持的很不错,不仅支持 Windows 和 Linux ,还支持各种商用的 Unix 版本。如果开发者想使用高稳定性的统一线程编程接口减轻跨平台开发的难度, Boost 库将是首选。 2. ACE ACE 全称是 ADAPTIVE Communication Environment,它是一个免费的,开源的,面向对象的工具框架,用以开发并发访问的软件。由于 ACE 最初是面向网络服务端的编程开发,因此对于线程开发的工具库它也能提供很全面的支持。其支持的平台也很全面,包括 Windows,Linux 和各种版本 Unix 。 ACE 的唯一问题是如果仅仅是用于线程编程,其似乎显得有些过于重量级。而且其较复杂的配置也让其部署对初学者而言并非易事。