跟我学Linux编程-11-多线程编程-竞争

跟我学Linux编程-11-多线程编程-竞争
跟我学Linux编程-11-多线程编程-竞争

多线程编程-变量

在上几个章节关于Linux多线程示例程序的运行结果描述过程中,细心的同志可能已经发现,我几乎每次的措辞都是“运行结果大致为”。在单线程的编程环境上,如果输入条件一样,程序中又没有随机处理逻辑,那么程序的运行结果是非常精确的,每次都会一样。在多线程中,“程序每次运行,结果精确相同”的结论不再成立,因为各个线程中存在竞争,导致程序进入不可完全预测的分支或执行顺序,从而每次运行的结果可能(注意不是一定)会有变化。今天,我将通过一个简单的示例,要向大家展示,“竞争”是怎么一回事。

示例程序还是和上前边的章节相同,只做了一些简单的改动。代码如下:

#include

#include

#include

int gcnt = 0;

void *thread_task(void *arg)

{

int id = (int)arg;

while (1)

{

gcnt++;

if (gcnt % 2)

{

if (!(gcnt % 2)) printf("[%d] : %d\n", id, gcnt);

}

usleep(1);

}

return NULL;

}

int main(int argc, char *argv[])

{

pthread_t thr;

pthread_create(&thr, NULL, thread_task, (void *)1);

pthread_create(&thr, NULL, thread_task, (void *)2);

thread_task((void *)0);

return 0;

}

在线程执行函数thread_task中,我们每次循环对全局变量gcnt的值进行递增,紧接着做了两个条件判断,如果两次条件都成立,则将gcnt的值打印出来,我们来分析这两个判断条件:

if (gcnt % 2)

{

if (!(gcnt % 2)) printf("[%d] : %d\n", id, gcnt);

}

if (gcnt % 2)在gcnt为奇数是为真,而if (!(gcnt % 2))在gcnt为偶数时为真,两个条件做双重判断,就是说当gcnt即为奇数又为偶数的时候条件才成立,显然,这是一个不可能的事情,那么,gcnt的值永远不会被打印。

结果是这样的吗?我们先不去猜测,编译程序运行一下结果就知道了:

gcc thread2.c -o thread2 –lpthread

./thread2

程序运行后,可能要等很久,也可能没过多久,屏幕上会突然输出如下的信息:

[1] : 775816

[1] : 1558050

[0] : 1617036

[1] : 1655538

[0] : 1667332

[1] : 1680268

[0] : 1681388

如果发现程序有输出,我们可以按Ctrl+c终止程序。

不可能发生的事情居然发生了!!!!!难道是因为我们看电脑太久,眼花了吗????如果不相信,可以再执行一次,你可能又会得到下面这样的结果:

[0] : 33876

[2] : 346190

[0] : 545698

[1] : 641654

结果还是出人意料,如果还是不信,你可以Ctrl+c杀掉程序,接着再试。我以人格向同志们保证:每次程序多少会输出一些内容,而且每次输出的内容几乎都不相同(如果有某两甚至多次结果相同,也不必惊讶,因这这也是有可能的)。

“不可能”发生的事情多次发生,那么也就成了事实。事实的背后,总是会有真理,接下来就向大家揭晓真理:

在多线程环境中,程序中的每个线程是以各自的进度“同时”在执行的,线程1在做事情A 的时候,线程2可能在做事情B,如果事情A和事情B存在某种关联,则线程1与线程2就会构成竞争,如果这种竞争构成冲突,则会引发不可预测的结果。

此外,各个线程“同时运行”的特性,是从宏观上来度量的,在微观角度上考查,则某一时刻,线程1与线程2未必真的同时在运行。现代的CPU,某个时刻,只能运行一个线程的代码,当线程数大于CPU数量的时候(几乎总是如此),CPU通过分时来为不同的线程服务,因此某个时刻,总是有一些线程(不会超过电脑中CPU核心个数)在运行,而另外一些线程没有在运行,而在下一刻,之前正在运行的线程停止执行,没有运行的线程获得CPU,投入运行。这个过程被称之为线程调度或者说线程切换。运行的线程被调度为不运行,我们称之为切出,没运行的线程被运行,我们称之为切入。线程的切入切出,遵循一定的时间间隔,这个时间间隔比较短,通常在毫秒这个量级,同时由于各种原因,这种时间间隔又不是很精确,差几个微秒是非常正常的事情。但对于CPU而言,几微秒的差异就是数百万条的机器指令,因此两个线程,就是做一样的事情,经过一断时间后,其各其的进度将会出现不小的差异(除非我们使用了某种方法来阻碍了这种差异的发生与扩大,如之前的示例程序中,我们让程序每执行一条程序就sleep 1秒),从而导致程序运行结果的差异性。

理论知识基本讲解完毕,我们接下来回到示例代码本身。我们将代码:

gcnt++;

if (gcnt % 2)

{

if (!(gcnt % 2)) printf("[%d] : %d\n", id, gcnt);

}

分解为4步:

a 将gcnt的值累加1

b 判断gcnt是否为奇数

c 判断gcnt是否为偶数

d 打印gcnt的值

我们接下来将用表格来模拟两个线程的运行状态,跟踪和分析其运行结果。在表中,如果线程在时刻t执行上述的某个步骤,则其对应列的对应行标记为对应的字母a-d,如果线程在t时刻执行a-d之外的操作,则统一标记为o,如果程序此时刻未运行(被调度器切换出CPU),则标记为s。我们考察下表:

在t0-t2过程中,线程1执行了a-c3个步骤(步骤c条件不成立,所以d没执行),线程2在做其它的事情,这时候两个线程没有操作相关联的内容,不存在竞争,因此程序如我们意料不输出任何信息。从t3-t4,线程1执行其它的操作,线程2执行了步骤a,因为条件不成立,b-d没执,这时程序运行结果也如我们预期。

经过一段时间的运行,时间到了t(x+0)时刻,我们假使在这之前,gcnt的值为偶数,如100,我们一步一步来分析接下来几个时刻的程序状态:

t(x+0) : 线程1执行步骤a,使gcnt = 101,此时线程2在做其它事情,不修改gcnt的值。

t(x+1):线程1执行步骤b,此时gcnt % 2的条件成立,线程2依然在做其它的事情,与线程1没有冲突。

t(x+2):线程1被切出,暂停运行;线程2执行步骤a,使gcnt=102。

t(x+3):线程1被切进,继续运行,此时执行步骤c,条件!(gcnt % 2)成立;线程2些时执行步骤b,条件gcnt % 2不成立。

t(x+4):线程1的两次判条件都成立(虽然看起来是互斥不可能同时发生的),于是执行步骤d,在屏幕上输出gcnt的值:102。

经过对t(x+0)到t(x+4)共5个时刻程序微观运行状态的分析,我们找到了gcnt是奇数又是偶数这个不可能事件发生的确切证据。微妙的事情发生在t(x+1)-t(x+3)这三个步骤中,在t(x+2)时刻,线程1因为调度,被突然切出cpu了,此时线程2则还在运行,并且执行了步骤a,改变了全局变量gcnt的值,因此在t(x+3)时刻,线程1被切入继续运行,此时执行步骤c,其条件判断中gcnt的值已经不是在t(x+1)时刻的那个值了,因此在t(x+1)判断gcnt为奇数结果为真,在t(x+3)时刻,判断gcnt为偶数结果也为真,gcnt即是奇数又是偶数并不与数学定律冲突:因为两次判断,gcnt并不对应同一个值。

类似微妙的事情,在多线程环境中无时无刻不存发生,如果我们没有意识到这种微妙事件,则很难驾驭我们自己所写的程序,无论概率有多小,无论你认为是多么不应该,它总是会发生,给们带来未曾预料的结果,产生不可思异的事情,正如本例,一个数即是奇数又是偶数,怎么可能,却是事实。

如何确保不可能的事情真的不去发生,是多线程编程的一件难事,也是一件大事。在下一章节,我们将完善本示例,使既是奇数又是偶数的事情不再发生。

眼尖的同学可能已发现,与之前的程序相比,本示例程序中还有一个小改动:原来在循环中sleep(1),现在是usleep(1)。sleep与useep的作用都是让程序休眠一小会,只是前者参数指定的是秒数,后参数指定的是微秒,因此slee(1)等同于usleep(1000000)。今天程序中线程每次休眠的时间变智短,是为了让程序循环次更快次数更多,加大“不可能事情”发生的概率。

相关主题
相关文档
最新文档