⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 posix 线程详解 - 菜鸟成长轨迹 - csdnblog.htm

📁 pthread soure code
💻 HTM
📖 第 1 页 / 共 5 页
字号:
name=N100DD><SPAN class=atitle>理解 thread2.c</SPAN></A>
<P>如同第一个程序,这个程序创建一个新线程。主线程和新线程都将全局变量 myglobal 加<BR>一20 
次。但是程序本身产生了某些意想不到的结果。编译代码请输入:</P><PRE class=displaycode>$ gcc thread2.c -o thread2 -lpthread</PRE>
<P>运行请输入:</P><PRE class=displaycode>$ ./thread2</PRE>
<P>输出:</P><PRE class=displaycode>$ ./thread2<BR>..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o<BR>myglobal equals 21</PRE>
<P>非常意外吧!因为myglobal从零开始,主线程和新线程各自对其进行了20 次加一, 
程序结束时myglobal<BR>值应当等于40。由于myglobal 输出结果为 21,这其中肯定有问题。但是究竟是什么呢?</P>
<P>放弃吗?好,让我来解释是怎么一回事。首先查看函数 thread_function()。注意如何将 myglobal <BR>复制到局部变量 "j" 了吗? 
接着将 j 加一, 再睡眠一秒,然后到这时才将新的 j 值复制到 myglobal?<BR>这就是关键所在。设想一下,如果主线程就在新线程将 myglobal 
值复制给 j <STRONG>后</STRONG> 立即将 myglobal <BR>加一,会发生什么?当 thread_function() 将 j 的值写回 
myglobal 时,就覆盖了主线程所做的修改。 </P>
<P>当编写线程程序时,应避免产生这种无用的副作用,否则只会浪费时间(当然,除了编写关于<BR>POSIX 
线程的文章时有用)。那么,如何才能排除这种问题呢?</P>
<P>由于是将 myglobal 复制给 j 并且等了一秒之后才写回时产生问题,可以尝试避免使用临时局部变<BR>量并直接将 myglobal 
加一。虽然这种解决方案对这个特定例子适用,但它还是不正确。如果我们<BR>对 myglobal 
进行相对复杂的数学运算,而不是简单的加一,这种方法就会失效。但是为什么呢?</P>
<P>要理解这个问题,必须记住线程是并发运行的。即使在单处理器系统上运行(内核利用时间分片模<BR>拟多任务)也是可以的,从程序员的角度,想像两个线程是同时执行的。thread2.c 
出现问题是因为<BR>thread_function() 依赖以下论据:在 myglobal 加一之前的大约一秒钟期间不会修改 
myglobal。<BR>需要有些途径让一个线程在对 myglobal 
做更改时通知其它线程“不要靠近”。我将在下一篇文章<BR>中讲解如何做到这一点。到时候见。</P>
<H1>第二部分:称作互斥对象的小玩意</H1>
<BLOCKQUOTE>POSIX 线程是提高代码响应和性能的有力手段。在此三部分系列文章的第二篇中,Daniel <BR>Robbins 
  将说明,如何使用被称为互斥对象的灵巧小玩意,来保护线程代码中共享数据结<BR>构的完整性。</BLOCKQUOTE><!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
<H2><A name=N10046><SPAN class=atitle>互斥我吧!</SPAN></A></H2><BR>
<P>在 <A 
href="http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/index.html"><FONT 
color=#5c81a7>前一篇文章中</FONT></A> 
,谈到了会导致异常结果的线程代码。两个线程分别对同一个全局变量进行了<BR>二十次加一。变量的值最后应该是 40,但最终值却是 
21。这是怎么回事呢?因为一个线程不停<BR>地“取消”了另一个线程执行的加一操作,所以产生这个问题。现在让我们来查看改正后的代码,<BR>它使用 
<STRONG>互斥对象</STRONG>(mutex)来解决该问题: <BR><BR><A 
name=thread3.c><STRONG>thread3.c</STRONG></A></P><PRE class=displaycode>#include &lt;pthread.h&gt;<BR>#include &lt;stdlib.h&gt;<BR>#include &lt;unistd.h&gt;<BR>#include &lt;stdio.h&gt;<BR>int myglobal;<BR>pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;<BR> void *thread_function(void *arg) {<BR>  int i,j;<BR>  for ( i=0; i&lt;20; i++) {<BR>    pthread_mutex_lock(&amp;mymutex);<BR>    j=myglobal;<BR>    j=j+1;<BR>    printf(".");<BR>    fflush(stdout);<BR>    sleep(1);<BR>    myglobal=j;<BR>    pthread_mutex_unlock(&amp;mymutex);<BR>  }<BR>  return NULL;<BR>}<BR>int main(void) {<BR>  pthread_t mythread;<BR>  int i;<BR>  if ( pthread_create( &amp;mythread, NULL, thread_function, NULL) ) {<BR>    printf("error creating thread.");<BR>    abort();<BR>  }<BR>  for ( i=0; i&lt;20; i++) {<BR>    pthread_mutex_lock(&amp;mymutex);<BR>    myglobal=myglobal+1;<BR>    pthread_mutex_unlock(&amp;mymutex);<BR>    printf("o");<BR>    fflush(stdout);<BR>    sleep(1);<BR>  }<BR>  if ( pthread_join ( mythread, NULL ) ) {<BR>    printf("error joining thread.");<BR>    abort();<BR>  }<BR>  printf("\nmyglobal equals %d\n",myglobal);<BR>  exit(0);<BR>}</PRE>
<H2><A name=N10060><SPAN class=atitle>解读一下</SPAN></A></H2>
<P>如果将这段代码与<A 
href="http://www.ibm.com/software/developerworks/library/posix1.html"><FONT 
color=#5c81a7>前一篇文章</FONT></A>中给出的版本作一个比较,就会注意到增加了pthread_mutex_lock() 
和<BR>pthread_mutex_unlock() 
函数调用。在线程程序中这些调用执行了不可或缺的功能。他们提供了一种<BR><EM>相互排斥</EM>的方法(互斥对象即由此得名)。两个线程不能同时对同一个互斥对象加锁。 
</P>
<P>互斥对象是这样工作的。如果线程 a 试图锁定一个互斥对象,而此时线程 b 已锁定了同一个互斥对<BR>象时,线程 a 就将进入睡眠状态。一旦线程 b 
释放了互斥对象(通过 pthread_mutex_unlock() 调<BR>用),线程 a 就能够锁定这个互斥对象(换句话说,线程 a 就将从 
pthread_mutex_lock() 函数调<BR>用中返回,同时互斥对象被锁定)。同样地,当线程 a 正锁定互斥对象时,如果线程 c 
试图锁定互<BR>斥对象的话,线程 c 也将临时进入睡眠状态。对已锁定的互斥对象上调用 pthread_mutex_lock() 
的<BR>所有线程都将进入睡眠状态,这些睡眠的线程将“排队”访问这个互斥对象。</P>
<P>通常使用 pthread_mutex_lock() 和 pthread_mutex_unlock() 
来保护数据结构。这就是说,通过线<BR>程的锁定和解锁,对于某一数据结构,确保某一时刻只能有一个线程能够访问它。可以推测到,当线<BR>程试图锁定一个未加锁的互斥对象时,POSIX 
线程库将同意锁定,而不会使线程进入睡眠状态。<BR><BR><A name=figure1><STRONG>请看这幅轻松的漫画,四个小精灵重现了最近一次 
pthread_mutex_lock() 调用的一个场面。</STRONG></A><BR><IMG height=280 alt="" 
src="POSIX 线程详解 - 菜鸟成长轨迹 - CSDNBlog.files/mutex.gif" width=600 border=0 
valign="top"> <BR></P>
<P>图中,锁定了互斥对象的线程能够存取复杂的数据结构,而不必担心同时会有其它线程干扰。那个<BR>数据结构实际上是“冻结”了,直到互斥对象被解锁为止。pthread_mutex_lock() 
和<BR>pthread_mutex_unlock() 
函数调用,如同“在施工中”标志一样,将正在修改和读取的某一特定<BR>共享数据包围起来。这两个函数调用的作用就是警告其它线程,要它们继续睡眠并等待轮到它们<BR>对互斥对象加锁。当然,除非在 
<EM>每个</EM> 对特定数据结构进行读写操作的语句前后,都分别放上<BR>pthread_mutex_lock() 和 
pthread_mutext_unlock() 调用,才会出现这种情况。</P>
<H2><A name=N1008F><SPAN class=atitle>为什么要用互斥对象?</SPAN></A></H2>
<P>听上去很有趣,但究竟为什么要让线程睡眠呢?要知道,线程的主要优点不就是其具有独立工作、<BR>更多的时候是同时工作的能力吗?是的,确实是这样。然而,每个重要的线程程序都需要使用某些<BR>互斥对象。让我们再看一下示例程序以便理解原因所在。</P>
<P>请看 thread_function(),循环中一开始就锁定了互斥对象,最后才将它解锁。在这个示例程序中,<BR>mymutex 用来保护 
myglobal 的值。仔细查看 thread_function(),加一代码把 myglobal 
复制到一<BR>个局部变量,对局部变量加一,睡眠一秒钟,在这之后才把局部变量的值传回给 myglobal。不使用<BR>互斥对象时,即使主线程在 
thread_function() 线程睡眠一秒钟期间内对 myglobal 
加一,thread_function()<BR>苏醒后也会覆盖主线程所加的值。使用互斥对象能够保证这种情形不会发生。(您也许会想到,我<BR>增加了一秒钟延迟以触发不正确的结果。把局部变量的值赋给 
myglobal 之前,实际上没有什么真<BR>正理由要求 thread_function() 睡眠一秒钟。)使用互斥对象的新程序产生了期望的结果:</P><PRE class=displaycode>$ ./thread3<BR>o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooo<BR>myglobal equals 40</PRE>
<P>为了进一步探索这个极为重要的概念,让我们看一看程序中进行加一操作的代码:</P><PRE class=displaycode>thread_function() 加一代码:<BR>    j=myglobal;<BR>    j=j+1;<BR>    printf(".");<BR>    fflush(stdout);<BR>    sleep(1);<BR>    myglobal=j;<BR><BR>主线程加一代码:<BR><BR>    myglobal=myglobal+1;</PRE>
<P>如果代码是位于单线程程序中,可以预期 thread_function() 
代码将完整执行。接下来才会执<BR>行主线程代码(或者是以相反的顺序执行)。在不使用互斥对象的线程程序中,代码可能(几<BR>乎是,由于调用了 sleep() 
的缘故)以如下的顺序执行:</P><PRE class=displaycode>    thread_function() 线程        主线程<BR>    j=myglobal;<BR>    j=j+1;<BR>    printf(".");<BR>    fflush(stdout);<BR>    sleep(1);                     myglobal=myglobal+1;<BR>    myglobal=j;</PRE>
<P>当代码以此特定顺序执行时,将覆盖主线程对 myglobal 的修改。程序结束后,就将得到不正<BR>确的值。如果是在操纵指针的话,就可能产生段错误。注意到 
thread_function() 线程按顺序<BR>执行了它的所有指令。看来不象是 thread_function() 
有什么次序颠倒。问题是,同一时间内,<BR>另一个线程对同一数据结构进行了另一个修改。</P>
<H2><A name=N100B0><SPAN class=atitle>线程内幕1</SPAN></A></H2>
<P>在解释如何确定在何处使用互斥对象之前,先来深入了解一下线程的内部工作机制。请看第一<BR>个例子:</P>
<P>假设主线程将创建三个新线程:线程 a、线程 b 和线程 c。假定首先创建线程 a,然后是线<BR>程b,最后创建线程 c。</P><PRE class=displaycode>    pthread_create( &amp;thread_a, NULL, thread_function, NULL);<BR>    pthread_create( &amp;thread_b, NULL, thread_function, NULL);<BR>    pthread_create( &amp;thread_c, NULL, thread_function, NULL);</PRE>
<P>在第一个 pthread_create() 调用完成后,可以假定线程 a 不是已存在就是已结束并停止。第<BR>二个 pthread_create() 
调用后,主线程和线程 b 都可以假定线程 a 存在(或已停止)。</P>
<P>然而,就在第二个 create() 调用返回后,主线程无法假定是哪一个线程(a 或 b)会首先开<BR>始运行。虽然两个线程都已存在,线程 CPU 
时间片的分配取决于内核和线程库。至于谁将首<BR>先运行,并没有严格的规则。尽管线程 a 更有可能在线程 b 
之前开始执行,但这并无保证。<BR>对于多处理器系统,情况更是如此。如果编写的代码假定在线程 b 开始执行之前实际上执行线<BR>程 a 
的代码,那么,程序最终正确运行的概率是 
99%。或者更糟糕,程序在您的机器上100%<BR>&nbsp;地正确运行,而在您客户的四处理器服务器上正确运行的概率却是零。</P>
<P>从这个例子还可以得知,线程库保留了每个单独线程的代码执行顺序。换句话说,实际上那三<BR>个 pthread_create() 
调用将按它们出现的顺序执行。从主线程上来看,所有代码都是依次执<BR>行的。有时,可以利用这一点来优化部分线程程序。例如,在上例中,线程 c 
就可以假定线<BR>程 a 和线程 b 不是正在运行就是已经终止。它不必担心存在还没有创建线程a 和线程b 
的可<BR>能性。可以使用这一逻辑来优化线程程序。</P>
<H2><A name=N100C9><SPAN class=atitle>线程内幕 2</SPAN></A></H2>
<P>现在来看另一个假想的例子。假设有许多线程,他们都正在执行下列代码:</P><PRE class=displaycode>    myglobal=myglobal+1;</PRE>
<P>那么,是否需要在加一操作语句前后分别锁定和解锁互斥对象呢?也许有人会说“不”。编<BR>译器极有可能把上述赋值语句编译成一条机器指令。大家都知道,不可能"半途"中断一条机<BR>器指令。即使是硬件中断也不会破坏机器指令的完整性。基于以上考虑,很可能倾向于完全<BR>省略 
pthread_mutex_lock() 和 pthread_mutex_unlock() 调用。不要这样做。</P>
<P>我在说废话吗?不完全是这样。首先,不应该假定上述赋值语句一定会被编译成一条机器指<BR>令,除非亲自验证了机器代码。即使插入某些内嵌汇编语句以确保加一操作的完整执行――<BR>甚至,即使是自己动手写编译器!-- 
仍然可能有问题。</P>
<P>答案在这里。使用单条内嵌汇编操作码在单处理器系统上可能不会有什么问题。每个加一操<BR>作都将完整地进行,并且多半会得到期望的结果。但是多处理器系统则截然不同。在多CPU<BR>机器上,两个单独的处理器可能会在几乎同一时刻(或者,就在同一时刻)执行上述赋值语<BR>句。不要忘了,这时对内存的修改需要先从 
L1 写入 L2 高速缓存、然后才写入主存。(SMP <BR>机器并不只是增加了处理器而已;它还有用来仲裁对 RAM 
存取的特殊硬件。)最终,根本<BR>无法搞清在写入主存的竞争中,哪个 CPU 
将会"胜出"。要产生可预测的代码,应使用互斥<BR>对象。互斥对象将插入一道"内存关卡",由它来确保对主存的写入按照线程锁定互斥对象的<BR>顺序进行。</P>
<P>考虑一种以 32 位块为单位更新主存的 SMP 体系结构。如果未使用互斥对象就对一个64 <BR>位整数进行加一操作,整数的最高 4 位字节可能来自一个 
CPU,而其它 4 个字节却来自<BR>另一 
CPU。糟糕吧!最糟糕的是,使用差劲的技术,您的程序在重要客户的系统上有可能<BR>不是很长时间才崩溃一次,就是早上三点钟就崩溃。David R. 
Butenhof 在他的《POSIX 线<BR>程编程》(请参阅本文末尾的 <A 
href="http://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/index.html#resources"><FONT 
color=#996699>参考资料</FONT></A>部分)一书中,讨论了由于未使用互斥对象而将产<BR>生的种种情况。</P>
<H2><A name=N100E6><SPAN class=atitle>许多互斥对象</SPAN></A></H2>
<P>如果放置了过多的互斥对象,代码就没有什么并发性可言,运行起来也比单线程解决方案<BR>慢。如果放置了过少的互斥对象,代码将出现奇怪和令人尴尬的错误。幸运的是,有一个<BR>中间立场。首先,互斥对象是用于串行化存取*共享数据*。不要对非共享数据使用互斥对<BR>象,并且,如果程序逻辑确保任何时候都只有一个线程能存取特定数据结构,那么也不要<BR>使用互斥对象。</P>
<P>其次,如果要使用共享数据,那么在读、写共享数据时都应使用互斥对象。用<BR>pthread_mutex_lock() 和 
pthread_mutex_unlock() 
把读写部分保护起来,或者在程序中<BR>不固定的地方随机使用它们。学会从一个线程的角度来审视代码,并确保程序中每一个线<BR>程对内存的观点都是一致和合适的。为了熟悉互斥对象的用法,最初可能要花好几个小时<BR>来编写代码,但是很快就会习惯并且*也*不必多想就能够正确使用它们。</P>
<H2><A name=N100F2><SPAN class=atitle>使用调用:初始化</SPAN></A></H2>
<P>现在该来看看使用互斥对象的各种不同方法了。让我们从初始化开始。在 <A 
href="http://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/index.html#thread3.c"><FONT 
color=#996699>thread3.c 示例</FONT></A> <BR>中,我们使用了静态初始化方法。这需要声明一个 pthread_mutex_t 
变量,并赋给它常数<BR>PTHREAD_MUTEX_INITIALIZER:</P><PRE class=displaycode>pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;</PRE>
<P></P>
<P>很简单吧。但是还可以动态地创建互斥对象。当代码使用 malloc() 
分配一个新的互斥对<BR>象时,使用这种动态方法。此时,静态初始化方法是行不通的,并且应当使用例程<BR>pthread_mutex_init():</P><PRE class=displaycode>int pthread_mutex_init( pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr)<BR></PRE>
<P>正如所示,pthread_mutex_init 
接受一个指针作为参数以初始化为互斥对象,该指针指<BR>向一块已分配好的内存区。第二个参数,可以接受一个可选的 pthread_mutexattr_t 
指<BR>针。这个结构可用来设置各种互斥对象属性。但是通常并不需要这些属性,所以正常做<BR>法是指定 NULL。</P>
<P>一旦使用 pthread_mutex_init() 初始化了互斥对象,就应使用 pthread_mutex_destroy() 
<BR>消除它。pthread_mutex_destroy() 接受一个指向 pthread_mutext_t 
的指针作为参数,<BR>并释放创建互斥对象时分配给它的任何资源。请注意, pthread_mutex_destroy() 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -