2.3 并发性
当在相同的内存空间中有许多并发执行的线程时,我们如何构建一个正确工作的程序?操作系统需要哪些基本类型?硬件应该提供哪些机构?我们如何使用它们来解决并发性问题?
本书另一个很重要的主题是并发性。我们使用这样一个概念术语去描述同一个程序要同时处理多个任务时会发生的并要求解决的一些问题。并发性问题首先出现在操作系统本身中;正如上述你看到的一些虚拟化的例子,操作系统同时处理许多事情,先运行一个进程,然后另一个,等等。人们后来会发现,这么做会引起许多影响深刻并有趣的事情。
一个多线程程序示例:threads.c
不幸的是,并发性问题不再仅仅局限于操作系统本身。现代多线程程序也出现了同样的问题。让我们看看示例程序threads.c。
即使此刻你不能完全理解这个例子(我们将在后续的章节并发性中,学习这个例子的有关知识),基本思想很简单。主程序使用pthreads_create()创建了两个线程。你可以把线程看做与其他函数共享内存空间的一个函数,在他们中可以有一个以上的同时在活动中。在这个例子中,每个线程都在一个称为worker()的例程中开始运行,在这个例程中,它只是在一个循环中增加计数器的循环次数。
下面是运行这个程序时发生的事情,变量循环的输入值设置为1000。loops的值将决定在一个循环中,这两个worker每个将增加共享计数器多少次。当我们将值设置为1000时,你预期计数器最后的值将会是多少?
你可能猜到了,当这两个线程结束的时候,计数器最后的值是2000次,每个计数器增加1000次。真的,我们希望当我们输入值为N时,我们预期输出结果为2N,但是事实证明,生活常常不会如此简单,我们给Loops一个更大的值,运行这个同样的程序,看他会输出我们的预期结果吗?
在这个过程中,当我们输入值为100,000时,我们得到的不是200,000的最终值,而是143,012。然后,当我们再次运行程序时,我们不仅再次得到错误的值,而且与上次得到的值不同。事实上,如果您用循环的高值一遍又一遍地运行该程序,您可能会发现有时甚至会得到正确的答案!为什么会这样呢?
事实证明,这些奇怪和不寻常的结果的原因与指令如何执行有关,而指令是一次执行一个。不幸的是,上面程序的一个关键部分,也就是增加共享计数器的部分,需要三条指令:一是将计数器的值从存储器装入寄存器,一个是增加1,一个是重新存进存储器。因为这三条指令不是原子执行,所以奇怪的事情就这样发生了。我们将在本书的第二部分详细讨论并发性问题。