pthread 学习系列 case2-- 使用互斥锁
ref http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/index.html
1 #include <pthread.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <stdio.h> 5 int myglobal; 6 void *thread_function(void *arg) { 7 int i,j; 8 for ( i=0; i<20; i++) { 9 j=myglobal; 10 j=j+1; 11 printf("."); 12 fflush(stdout); 13 sleep(1); 14 myglobal=j; 15 } 16 return NULL; 17 } 18 int main(void) { 19 pthread_t mythread; 20 int i; 21 if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { 22 printf("error creating thread."); 23 abort(); 24 } 25 for ( i=0; i<20; i++) { 26 myglobal=myglobal+1; 27 printf("o"); 28 fflush(stdout); 29 sleep(1); 30 } 31 if ( pthread_join ( mythread, NULL ) ) { 32 printf("error joining thread."); 33 abort(); 34 } 35 printf("\nmyglobal equals %d\n",myglobal); 36 exit(0); 37 }
打印输出:
$ ./thread2 ..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o myglobal equals 21
非常意外吧!因为 myglobal 从零开始,主线程和新线程各自对其进行了 20 次加一, 程序结束时 myglobal 值应当等于 40。由于 myglobal 输出结果为 21,这其中肯定有问题。但是究竟是什么呢?
放弃吗?好,让我来解释是怎么一回事。首先查看函数 thread_function()。注意如何将 myglobal 复制到局部变量 "j" 了吗? 接着将 j 加一, 再睡眠一秒,然后到这时才将新的 j 值复制到 myglobal?这就是关键所在。设想一下,如果主线程就在新线程将 myglobal 值复制给 j 后 立即将 myglobal 加一,会发生什么?当 thread_function() 将 j 的值写回 myglobal 时,就覆盖了主线程所做的修改。
当编写线程程序时,应避免产生这种无用的副作用,否则只会浪费时间(当然,除了编写关于 POSIX 线程的文章时有用)。那么,如何才能排除这种问题呢?
由于是将 myglobal 复制给 j 并且等了一秒之后才写回时产生问题,可以尝试避免使用临时局部变量并直接将 myglobal 加一。虽然这种解决方案对这个特定例子适用,但它还是不正确。如果我们对 myglobal 进行相对复杂的数学运算,而不是简单的加一,这种方法就会失效。但是为什么呢?
要理解这个问题,必须记住线程是并发运行的。即使在单处理器系统上运行(内核利用时间分片模拟多任务)也是可以的,从程序员的角度,想像两个线程是同时执行的。thread2.c 出现问题是因为 thread_function() 依赖以下论据:在 myglobal 加一之前的大约一秒钟期间不会修改 myglobal。需要有些途径让一个线程在对 myglobal 做更改时通知其它线程“不要靠近”。
个人分析:
线程 thread_function 先将global的值保存在j中,然后休眠1秒,在这一秒钟主线程做了global++的操作,然后休眠,当到thread_function 调度执行的时候i仍然保存的是1秒钱的值,然后再加加操作,覆盖了global的值。
所以要加锁
1 #include <pthread.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <stdio.h> 5 int myglobal; 6 pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER; 7 void *thread_function(void *arg) { 8 int i,j; 9 for ( i=0; i<20; i++) { 10 pthread_mutex_lock(&mymutex); 11 j=myglobal; 12 j=j+1; 13 printf("."); 14 fflush(stdout); 15 sleep(1); 16 myglobal=j; 17 pthread_mutex_unlock(&mymutex); 18 } 19 return NULL; 20 } 21 int main(void) { 22 pthread_t mythread; 23 int i; 24 if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { 25 printf("error creating thread."); 26 abort(); 27 } 28 for ( i=0; i<20; i++) { 29 pthread_mutex_lock(&mymutex); 30 myglobal=myglobal+1; 31 pthread_mutex_unlock(&mymutex); 32 printf("o"); 33 fflush(stdout); 34 sleep(1); 35 } 36 if ( pthread_join ( mythread, NULL ) ) { 37 printf("error joining thread."); 38 abort(); 39 } 40 printf("\nmyglobal equals %d\n",myglobal); 41 exit(0); 42 }
打印输出:
o....................ooooooooooooooooooo myglobal equals 40
互斥对象是这样工作的。如果线程 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_unlock() 把读写部分保护起来,或者在程序中不固定的地方随机使用它们。学会从一个线程的角度来审视代码,并确保程序中每一个线程对内存的观点都是一致和合适的。为了熟悉互斥对象的用法,最初可能要花好几个小时来编写代码,但是很快就会习惯并且*也*不必多想就能够正确使用它们。