`

Linux 的多线程编程

阅读更多
本文们针 Linux 线程编程主特性总结出 5 条经验,以改善 Linux 线程编程习惯避免其陷阱。本文,们穿插些 Windows 编程例以比 Linux 特性,以加深读印象。

   背景

   Linux 线程程序相应其(比如 Windows)线程 API 有些细微隐晦差别。不注意这些 Linux 些陷阱,常常致程序问题不穷,死锁不断。本文们从 5 个方面总结出 Linux 线程编程问题,并分别引出相关改善经验,以避免这些陷阱。们希望这些经验以帮助读们能更好更快熟悉 Linux 线程编程。

   们假设读都已经熟悉 Linux 基本线程编程 Pthread 库 API 。其第三方以线程编程库,如 boost,不本文提及。本文主涉及题材包括线程线程管理,互斥变量,条变量等。进程概念不本文涉及。

   Linux 线程 API 概介绍

   线程 Linux 已经有成熟 Pthread 库支持。其涉及线程最基本概念主包含三点:线程,互斥锁,条。其,线程操作又分线程创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别创建,销毁,加锁解锁。条操作有 5 种操作:创建,销毁,触发,广播等待。其些线程扩展概念,如信号灯等,都以通过面三个基本元素基本操作封装出。

   线程,互斥锁,条 Linux 应 API 以表 1 归纳。方便熟悉 Windows 线程编程读熟悉 Linux 线程 API,们表同时列出 Windows SDK 库所应 API 名称。

   表 1. 线程函数列表

象 操作 Linux Pthread API Windows SDK 库应 API
线程 创建 pthread_create CreateThread
退出 pthread_exit ThreadExit
等待 pthread_join WaitForSingleObject
互斥锁 创建 pthread_mutex_init CreateMutex
销毁 pthread_mutex_destroy CloseHandle
加锁 pthread_mutex_lock WaitForSingleObject
解锁 pthread_mutex_unlock ReleaseMutex
条 创建 pthread_cond_init CreateEvent
销毁 pthread_cond_destroy CloseHandle
触发 pthread_cond_signal SetEvent
广播 pthread_cond_broadcast SetEvent / ResetEvent
等待 pthread_cond_wait / pthread_cond_timedwait SingleObjectAndWait


   线程 Linux 已经有成熟 Pthread 库支持。其涉及线程最基本概念主包含三点:线程,互斥锁,条。其,线程操作又分线程创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别创建,销毁,加锁解锁。条操作有 5 种操作:创建,销毁,触发,广播等待。其些线程扩展概念,如信号灯等,都以通过面三个基本元素基本操作封装出。

   Linux 线程编程 5 条经验

   尽量设置 recursive 属性以始化 Linux 互斥变量

   互斥锁线程编程基本概念,被广泛使。其调次序层次清晰简单:建锁,加锁,解锁,销毁锁。但需注意,诸如 Windows 互斥变量不同,默认,Linux 同线程无法同互斥锁进行递归加速,否则发生死锁。

   所谓递归加锁,就同线程试图互斥锁进行两次或两次以行。其场景 Linux 代码由清单 1 所示。

   清单 1. 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 属性。基于此,以代码其实稍作修改就以好运行,只需始化锁时候加设置个属性。看清单 2 。

   清单 2. 设置互斥锁 recursive 属性实例

   pthread_mutexattr_init(&attr); 
  // 设置 recursive 属性
  pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP); 
  pthread_mutex_init(theMutex,&attr);

   因此,建议尽量设置 recursive 属性以始化 Linux 互斥锁,这样既以解决同线程递归加锁问题,又以避免死锁发生。这样做还有个额好处,就以让 Windows  Linux 让锁表现统。

  注意 Linux 触发条变量自动复位问题

   条变量置位复位有两种常模型:第种模型当条变量置位(signaled)以,如果当没有线程等待,其状态保持置位(signaled),直有等待线程进入被触发,其状态才变复位(unsignaled),这种模型采以 Windows  Auto-set Event 代表。其状态如图 1 所示:

   图 1. Windows 条变量状态流程





   通过比结果,同样逻辑, Linux 运行结果却完全两样。于 Windows 模型, Jack 开着出租车站台,触发条变量。如果没顾客,条变量维持触发状态,就说 Jack 停车那里等着。直 Susan 姐站台,执行等待条找出租车。 Susan 搭 Jack 出租车离开,同时条变量被自动复位。

   但 Linux ,问题就,Jack 站台看没人,触发条变量被直接复位,于 Jack 排等待队列里面。迟秒 Susan 姐站台却看不那里等待 Jack,只能等待,直 Mike 开车赶,重新触发条变量,Susan 才 Mike 车。这于排队系统面 Jack 不公平,而问题症结于 Linux 条变量触发自动复位引起个 Bug 。

   条变量 Linux 这种模型难说好坏。但实际,们以代码稍加改进就以避免这种差异发生。由于这种差异只发生触发没有被线程等待条变量时刻,因此们只需掌握好触发时机即。最简单做法增加个计数器记录等待线程个数,决定触发条变量检查该变量即。改进 Linux 函数如清单 5 所示。

   清单 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 唯问题如果仅仅于线程编程,其似显得有些过于重量级。而且其较复杂配置让其部署学而言并非易事。

  • 大小: 4.6 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics