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

2016-02-19 15:55 1 1 收藏

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

【 tulaoshi.com - 编程语言 】


  第 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
  现在来看另一个假想的例子。假设有许多线程,他们都正在执行下列代码

来源:https://www.tulaoshi.com/n/20160219/1610694.html

延伸阅读
本文包括以下内容: 单线程规则:Swing线程在同一时刻仅能被一个线程所访问。一般来说,这个线程是事件派发线程(event-dispatching thread)。 规则的例外:有些操作保证是线程安全的。 事件分发:假如你需要从事件处理(event-handling)或绘制代码以外的地方访问UI,那么你可以使用SwingUtilities类的invokeLate...
虽然集成开发环境(IDE)可以为图形化应用程序提供很好的调试设置,但是它不允许你调试多线程的Java服务器程序。 幸运的是,有几个工具可以做到,例如日志应用程序接口(API)和Java调试器。开发人员也可以获得系统的线程转储,它可以在任何时间显示出系统状态。 为了得到系统线程转储,运行服务器程序并键入[Ctrl] []。这会输出所有...
实现Runnable接口的类必须使用Thread类的实例才能创建线程。通过Runnable接口创建线程分为两步:     1. 将实现Runnable接口的类实例化。     2. 建立一个Thread对象,并将第一步实例化后的对象作为参数传入Thread类的构造方法。     最后通过Thread类的start方法建立线程。   ...
在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。在使用Runnable接口时需要建立一个Thread实例。因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例。Thread类的构造方法被重载了八次,构造方法如下: 代码如下: public Thread( );  public Thread(Runnable target);  public T...
与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。下面给出了Thread类中和这四种状态相关的方法。 代码如下: // 开始线程      public void start( );      public void run( );      // ...

经验教程

688

收藏

26
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部