Linux多线程编程讲解之系列二

多线程系列文章源码头文件内容:

#include<pthread.h>

#include<stdio.h>

#include<unistd.h>

作为程序员,就是要减少重复劳动,拒绝一切无目的的DIY。哪怕只有三行代码,哈哈😄

在上一篇文章中,我们谈到了会导致异常结果的线程代码。两个线程分别对同一个全局变量进行了二十次加一。变量的值最后应该是 40,但最终值却是 21。这是怎么回事呢?因为一个线程不停地“取消”了另一个线程执行的加一操作,所以产生这个问题。现在让我们来查看改正后的代码,它使用 互斥对象(mutex)来解决该问题:

int myglobal;  

pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;  

void *thread_function(void *arg) {  

int i,j;  

for ( i=0; i<20; 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; i<20; 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("\nmyglobal equals %d\n",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_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

现在来看另一个假想的例子。假设有许多线程,他们都正在执行下列代码:

myglobal=myglobal+1;

那么,是否需要在加一操作语句前后分别锁定和解锁互斥对象呢?也许有人会说“不”。编译器极有可能把上述赋值语句编译成一条机器指令。大家都知道,不可能"半途"中断一条机器指令。即使是硬件中断也不会破坏机器指令的完整性。基于以上考虑,很可能倾向于完全省略pthread_mutex_lock() 和 pthread_mutex_unlock() 调用。

不要这样做。我在说废话吗?不完全是这样。首先,不应该假定上述赋值语句一定会被编译成一条机器指令,除非亲自验证了机器代码。即使插入某些内嵌汇编语句以确保加一操作的完整执行――甚至,即使是自己动手写编译器!-- 仍然可能有问题。答案在这里。使用单条内嵌汇编操作码在单处理器系统上可能不会有什么问题。

每个加一操作都将完整地进行,并且多半会得到期望的结果。但是多处理器系统则截然不同。在多 CPU 机器上,两个单独的处理器可能会在几乎同一时刻(或者,就在同一时刻)执行上述赋值语句。不要忘了,这时对内存的修改需要先从 L1 写入 L2高速缓存、然后才写入主存。(SMP 机器并不只是增加了处理器而已;它还有用来仲裁对 RAM 存取的特殊硬件。)最终,根本无法搞清在写入主存的竞争中,哪个 CPU 将会"胜出"。要产生可预测的代码,应使用互斥对象。互斥对象将插入一道"内存关卡",由它来确保对主存的写入按照线程锁定互斥对象的顺序进行。

考虑一种以 32 位块为单位更新主存的 SMP 体系结构。如果未使用互斥对象就对一个64 位整数进行加一操作,整数的最高 4 位字节可能来自一个 CPU,而其它 4 个字节却来自另一 CPU。糟糕吧!最糟糕的是,使用差劲的技术,您的程序在重要客户的系统上有可能不是很长时间才崩溃一次,就是早上三点钟就崩溃。

互斥对象

如果放置了过多的互斥对象,代码就没有什么并发性可言,运行起来也比单线程解决方案慢。如果放置了过少的互斥对象,代码将出现奇怪和令人尴尬的错误。幸运的是,有一个中间立场。

首先,互斥对象是用于串行化存取*共享数据*。不要对非共享数据使用互斥对象,并且,如果程序逻辑确保任何时候都只有一个线程能存取特定数据结构,那么也不要使用互斥对象。

其次,如果要使用共享数据,那么在读、写共享数据时都应使用互斥对象。用pthread_mutex_lock() 和 pthread_mutex_unlock() 把读写部分保护起来,或者在程序中不固定的地方随机使用它们。学会从一个线程的角度来审视代码,并确保程序中每一个线程对内存的观点都是一致和合适的。为了熟悉互斥对象的用法,最初可能要花好几个小时来编写代码,但是很快就会习惯并且*也*不必多想就能够正确使用它们。

使用调用:初始化

现在该来看看使用互斥对象的各种不同方法了。让我们从初始化开始。我们使用了静态初始化方法。这需要声明一个 pthread_mutex_t 变量,并赋给它常数PTHREAD_MUTEX_INITIALIZER:

pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;

很简单吧。但是还可以动态地创建互斥对象。当代码使用 malloc() 分配一个新的互斥对象时,使用这种动态方法。此时,静态初始化方法是行不通的,并且应当使用例程pthread_mutex_init():

int pthread_mutex_init( pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr)


正如所示,pthread_mutex_init 接受一个指针作为参数以初始化为互斥对象,该指针指向一块已分配好的内存区。第二个参数,可以接受一个可选的 pthread_mutexattr_t 指针。这个结构可用来设置各种互斥对象属性。但是通常并不需要这些属性,所以正常做法是指定 NULL。

一旦使用 pthread_mutex_init() 初始化了互斥对象,就应使用 pthread_mutex_destroy() 消除它。pthread_mutex_destroy() 接受一个指向 pthread_mutext_t 的指针作为参数,并释放创建互斥对象时分配给它的任何资源。请注意, pthread_mutex_destroy()不会释放用来存储 pthread_mutex_t 的内存。释放自己的内存完全取决于您。还必须注意一点,pthread_mutex_init() 和 pthread_mutex_destroy() 成功时都返回零。

使用调用:锁定

pthread_mutex_lock(pthread_mutex_t *mutex)

pthread_mutex_unlock(pthread_mutex_t *mutex)

pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_lock() 接受一个指向互斥对象的指针作为参数以将其锁定。如果碰巧已经锁定了互斥对象,调用者将进入睡眠状态。函数返回时,将唤醒调用者(显然)并且调用者还将保留该锁。函数调用成功时返回零,失败时返回非零的错误代码。

pthread_mutex_unlock() 与 pthread_mutex_lock() 相配合,它把线程已经加锁的互斥对象解锁。始终应该尽快对已加锁的互斥对象进行解锁(以提高性能)。并且绝对不要对您未保持锁的互斥对象进行解锁操作(否则,pthread_mutex_unlock() 调用将失败并带一个非零的 EPERM 返回值)。

当线程正在做其它事情的时候(由于互斥对象当前是锁定的),如果希望锁定互斥对象,这个调用就相当方便。调用 pthread_mutex_trylock() 时将尝试锁定互斥对象。如果互斥对象当前处于解锁状态,那么您将获得该锁并且函数将返回零。然而,如果互斥对象已锁定,这个调用也不会阻塞。当然,它会返回非零的 EBUSY 错误值。然后可以继续做其它事情,稍后再尝试锁定。

等待条件发生

互斥对象是线程程序必需的工具,但它们并非万能的。例如,如果线程正在等待共享数据内某个条件出现,那会发生什么呢?代码可以反复对互斥对象锁定和解锁,以检查值的任何变化。同时,还要快速将互斥对象解锁,以便其它线程能够进行任何必需的更改。这是一种非常可怕的方法,因为线程需要在合理的时间范围内频繁地循环检测变化。

在每次检查之间,可以让调用线程短暂地进入睡眠,比如睡眠三秒钟,但是因此线程代码就无法最快作出响应。真正需要的是这样一种方法,当线程在等待满足某些条件时使线程进入睡眠状态。一旦条件满足,还需要一种方法以唤醒因等待满足特定条件而睡眠的线程。如果能够做到这一点,线程代码将是非常高效的,并且不会占用宝贵的互斥对象锁。这正是 POSIX 条件变量能做的事!

而 POSIX 条件变量将是我下一篇文章的主题,其中将说明如何正确使用条件变量。到那时,您已经越来越熟悉线程,我将在下一篇文章中加快进度。这样,在下一篇文章的结尾就能放上一个相对复杂的线程程序。

说到等到条件产生,下次再见!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容