本节书摘来自华章出版社《多核与GPU编程:工具、方法及实践》一书中的第3章,第3.1节, 作 者 Multicore and GPU Programming: An Integrated Approach[阿联酋]杰拉西莫斯·巴拉斯(Gerassimos Barlas) 著,张云泉 贾海鹏 李士刚 袁良 等译
第3章
共享内存编程:线程
本章目标:
学习线程的定义以及创建方法。
学习完成特定任务的初始化线程方法。
学习多种终止多线程程序的技术。
理解多线程访问共享数据过程中的主要问题,例如竞争和死锁。
学习信号量和监视器的定义和使用方法。
熟悉经典同步问题及其解决方法。
学习运行时动态管理线程。
学习多线程程序的调试技术。
3.1 引言
从20世纪60年代麻省理工学院引入兼容分时系统(CTSS)以来,多个程序并发执行的现象已经变得较为常见。操作系统通过中断当前正在执行的程序并将CPU的控制权交由另一个程序来完成这一功能。这种能有效地在多个程序间共享CPU时间(也即分时)的切换可以通过以下条件触发:
时钟或定时器的定期硬件中断
不定期硬件中断,例如某些设备的请求
调用操作系统功能,例如执行输入/输出(I/O)操作
每个运行中的程序组成一个进程,亦即为了管理程序,一个包括代码、数据、资源和执行状态信息的操作系统实体。
因此,每个进程值能在一个“时间片”内获得CPU的控制权,然后将控制权交回操作系统,操作系统再将CPU交于另一个进程,以此类推。单CPU上的分时如图3-1a所示。
分时这种方式可以提高计算资源的使用效率,但是它不能提高单个进程的运行速度,并且分时可能会由于减少分配给某个进程的时间片而直接降低程序性能,或者由于频繁调用操作系统的任务调度程序的开销而间接降低程序性能。
为了使程序获得更多的计算时间(利用调度制导语句改变程序优先级),必须将其划分为多个进程,在多核系统上也必须采取同样的方法才能提高性能,此类示例如图3-1b所示。最原始的生成(creating或者spawning)方法是调用fork函数,代码清单3-1给出了一个示例。
生成一个进程需要精确复制当前进程的内存,包括所有代码和数据,如图3-2所示。在原始程序(父进程)和生成程序(子进程)间的唯一区别是子进程继续执行fork语句的后的部分,亦即子进程没有收到返回值,因此子进程中的ChildID为0(见代码清单3-1第13行)。逻辑上看,就像一个新的进程开始在此处执行,从而使得两个进程执行不同任务(使两个进程执行同样的任务是无意义的)。
这种进程生成机制中一些显而易见的问题包括:
为什么复制代码?它不应该在内存不可变区吗?
子进程有父进程的所有数据的副本,那么如何在两个进程间交换信息?
由于有些程序在运行时改变代码,因此需要复制代码。这也是病毒中的常见特征,但这并不是一个正常的程序行为。对于后一个问题,系统已经提供了多种解决该问题 的机制,但是最为简单的还是访问一组共享变量。
由于fork函数中保留和复制程序映像的开销,因此这种方法较为低效。幸运的是,还存在另一种通过线程来实现并发的方法,这将在下面进行介绍。