通用线程:POSIX 线程详解(2)

铁血汉子a

铁血汉子a

2016-02-19 15:55

只要你有一台电脑或者手机,都能关注图老师为大家精心推荐的通用线程:POSIX 线程详解(2),手机电脑控们准备好了吗?一起看过来吧!

  第 2 部分
  称作互斥对象的小玩意
  
  作者:Daniel Robbins
  
  内容:
  
  
  互斥我吧!
  解读一下
  为什么要用互斥对象?
  线程内幕 1
  线程内幕 2
  许多互斥对象
  使用调用:初始化
  使用调用:锁定
  等待条件发生
  参考资料
  关于作者
  
  
  
  POSIX 线程是提高代码响应和性能的有力手段。在此三部分系列文章的第二篇中,Daniel Robbins 将说明,如何使用被称为互斥对象的灵巧小玩意,来保护线程代码中共享数据结构的完整性。
  
  互斥我吧!
  在前一篇文章中,谈到了会导致异常结果的线程代码。两个线程分别对同一个全局变量进行了二十次加一。变量的值最后应该是 40,但最终值却是 21。这是怎么回事呢?因为一个线程不停地“取消”了另一个线程执行的加一操作,所以产生这个问题。现在让我们来查看改正后的代码,它使用互斥对象(mutex)来解决该问题:
  
  thread3.c
  #include
  #include
  #include
  #include
  
  int myglobal;
  pthread_mutex_t mymutex=PTHREAD_MUTEX_IN99vIALIZER;
  
  void *thread_function(void *arg) {
  int i,j;
  for ( i=0; i20; i++) {
  pthread_mutex_lock(&mymutex);
  j=myglobal;
  j=j+1;
  printf(".");
  fflush(stdout);
  sleep(1);
  myglobal=j;
  pthread_mutex_unlock(&mymutex);
  }
  return NULL;
  }
  
  int main(void) {
  
  pthread_t mythread;
  int i;
  
  if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
  printf("error creating thread.");
  abort();
  }
  
  for ( i=0; i20; i++) {
  pthread_mutex_lock(&mymutex);
  myglobal=myglobal+1;
  pthread_mutex_unlock(&mymutex);
  printf("o");
  fflush(stdout);
  sleep(1);
  }
  
  if ( pthread_join ( mythread, NULL ) ) {
  printf("error joining thread.");
  abort();
  }
  
  printf("myglobal equals %d",myglobal);
  
  exit(0);
  
  }
  
  
  
  解读一下
  假如将这段代码与前一篇文章中给出的版本作一个比较,就会注重到增加了 pthread_mutex_lock() 和 pthread_mutex_unlock() 函数调用。在线程程序中这些调用执行了不可或缺的功能。他们提供了一种相互排斥的方法(互斥对象即由此得名)。两个线程不能同时对同一个互斥对象加锁。
  
  互斥对象是这样工作的。假如线程 a 试图锁定一个互斥对象,而此时线程 b 已锁定了同一个互斥对象时,线程 a 就将进入睡眠状态。一旦线程 b 释放了互斥对象(通过 pthread_mutex_unlock() 调用),线程 a 就能够锁定这个互斥对象(换句话说,线程 a 就将从 pthread_mutex_lock() 函数调用中返回,同时互斥对象被锁定)。同样地,当线程 a 正锁定互斥对象时,假如线程 c 试图锁定互斥对象的话,线程 c 也将临时进入睡眠状态。对已锁定的互斥对象上调用 pthread_mutex_lock() 的所有线程都将进入睡眠状态,这些睡眠的线程将“排队”访问这个互斥对象。
  
  通常使用 pthread_mutex_lock() 和 pthread_mutex_unlock() 来保护数据结构。这就是说,通过线程的锁定和解锁,对于某一数据结构,确保某一时刻只能有一个线程能够访问它。可以推测到,当线程试图锁定一个未加锁的互斥对象时,POSIX 线程库将同意锁定,而不会使线程进入睡眠状态。
  
  
  请看这幅轻松的漫画,四个小精灵重现了最近一次 pthread_mutex_lock() 调用的一个场面。
  
  
  
  图中,锁定了互斥对象的线程能够存取复杂的数据结构,而不必担心同时会有其它线程干扰。那个数据结构实际上是“冻结”了,直到互斥对象被解锁为止。pthread_mutex_lock() 和 pthread_mutex_unlock() 函数调用,如同“在施工中”标志一样,将正在修改和读取的某一特定共享数据包围起来。这两个函数调用的作用就是警告其它线程,要它们继续睡眠并等待轮到它们对互斥对象加锁。当然,除非在每个对特定数据结构进行读写操作的语句前后,都分别放上 pthread_mutex_lock() 和 pthread_mutext_unlock() 调用,才会出现这种情况。
  
  为什么要用互斥对象?
  听上去很有趣,但究竟为什么要让线程睡眠呢?要知道,线程的主要优点不就是其具有独立工作、更多的时候是同时工作的能力吗?是的,确实是这样。然而,每个重要的线程程序都需要使用某些互斥对象。让我们再看一下示例程序以便理解原因所在。
  
  请看 thread_function(),循环中一开始就锁定了互斥对象,最后才将它解锁。在这个示例程序中,mymutex 用来保护 myglobal 的值。仔细查看 thread_function(),加一代码把 myglobal 复制到一个局部变量,对局部变量加一,睡眠一秒钟,在这之后才把局部变量的值传回给 myglobal。不使用互斥对象时,即使主线程在 thread_function() 线程睡眠一秒钟期间内对 myglobal 加一,thread_function() 清醒后也会覆盖主线程所加的值。使用互斥对象能够保证这种情形不会发生。(您也许会想到,我增加了一秒钟延迟以触发不正确的结果。把局部变量的值赋给 myglobal 之前,实际上没有什么真正理由要求 thread_function() 睡眠一秒钟。)使用互斥对象的新程序产生了期望的结果:
  
  
  $ ./thread3
  o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooo
  myglobal equals 40
  
  
  
  
  为了进一步探索这个极为重要的概念,让我们看一看程序中进行加一操作的代码:
  
  
  thread_function() 加一代码:
  j=myglobal;
  j=j+1;
  printf(".");
  fflush(stdout);
  sleep(1);
  myglobal=j;
  
  主线程加一代码:
  myglobal=myglobal+1;
  
  
  
  
  假如代码是位于单线程程序中,可以预期 thread_function() 代码将完整执行。接下来才会执行主线程代码(或者是以相反的顺序执行)。在不使用互斥对象的线程程序中,代码可能(几乎是,由于调用了 sleep() 的缘故)以如下的顺序执行:
  
  
  thread_function() 线程 主线程
  
  j=myglobal;
  j=j+1;
  printf(".");
  fflush(stdout);
  sleep(1); myglobal=myglobal+1;
  myglobal=j;
  
  
  
  
  当代码以此特定顺序执行时,将覆盖主线程对 myglobal 的修改。程序结束后,就将得到不正确的值。假如是在操纵指针的话,就可能产生段错误。注重到 thread_function() 线程按顺序执行了它的所有指令。看来不象是 thread_function() 有什么次序颠倒。问题是,同一时间内,另一个线程对同一数据结构进行了另一个修改。
  
  线程内幕 1
  在解释如何确定在何处使用互斥对象之前,先来深入了解一下线程的内部工作机制。请看第一个例子:
  
  假设主线程将创建三个新线程:线程 a、线程 b 和线程 c。假定首先创建线程 a,然后是线程 b,最后创建线程 c。
  
  
  pthread_create( &thread_a, NULL, thread_function, NULL);
  pthread_create( &thread_b, NULL, thread_function, NULL);
  pthread_create( &thread_c, NULL, thread_function, NULL);
  
  
  
  
  在第一个 pthread_create() 调用完成后,可以假定线程 a 不是已存在就是已结束并停止。第二个 pthread_create() 调用后,主线程和线程 b 都可以假定线程 a 存在(或已停止)。
  
  然而,就在第二个 create() 调用返回后,主线程无法假定是哪一个线程(a 或 b)会首先开始运行。虽然两个线程都已存在,线程 CPU 时间片的分配取决于内核和线程库。至于谁将首先运行,并没有严格的规则。尽管线程 a 更有可能在线程 b 之前开始执行,但这并无保证。对于多处理器系统,情况更是如此。假如编写的代码假定在线程 b 开始执行之前实际上执行线程 a 的代码,那么,程序最终正确运行的概率是 99%。或者更糟糕,程序在您的机器上 100% 地正确运行,而在您客户的四处理器服务器上正确运行的概率却是零。
  
  从这个例子还可以得知,线程库保留了每个单独线程的代码执行顺序。换句话说,实际上那三个 pthread_create() 调用将按它们出现的顺序执行。从主线程上来看,所有代码都是依次执行的。有时,可以利用这一点来优化部分线程程序。例如,在上例中,线程 c 就可以假定线程 a 和线程 b 不是正在运行就是已经终止。它不必担心存在还没有创建线程 a 和线程 b 的可能性。可以使用这一逻辑来优化线程程序。
  
  
  线程内幕 2
  现在来看另一个假想的例子。假设有许多线程,他们都正在执行下列代码
展开更多 50%)
分享

猜你喜欢

通用线程:POSIX 线程详解(2)

编程语言 网络编程
通用线程:POSIX 线程详解(2)

通用线程:POSIX 线程详解(3)

编程语言 网络编程
通用线程:POSIX 线程详解(3)

s8lol主宰符文怎么配

英雄联盟 网络游戏
s8lol主宰符文怎么配

POSIX 线程详解(1)

编程语言 网络编程
POSIX 线程详解(1)

Java多线程的用法详解

编程语言 网络编程
Java多线程的用法详解

lol偷钱流符文搭配推荐

英雄联盟 网络游戏
lol偷钱流符文搭配推荐

基于Java回顾之多线程详解

编程语言 网络编程
基于Java回顾之多线程详解

深入java线程池的使用详解

编程语言 网络编程
深入java线程池的使用详解

lolAD刺客新符文搭配推荐

英雄联盟
lolAD刺客新符文搭配推荐

我只会用命去珍惜你 - QQ图案分组

我只会用命去珍惜你 - QQ图案分组

通过覆盖__atexit进行缓冲区溢出攻击

通过覆盖__atexit进行缓冲区溢出攻击
下拉加载更多内容 ↓