摘自《程序员的自我修养》
Windows对进程和线程的实现如果教科书一般标准,Windows内核有明确的线程和进程的概念。在WindowsAPI中,可以使用明确的API:CreateProcess和CreateThread来创建进程和线程,并且有一系列的API来操作它们。但对于Linux来说,线程并不是一个通用的概念。
Linux对线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程概念。Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一内存空间的多个任务构成了一个进程,这些任务也就成了进程中的线程。在Linux下,用以下方法可以创建一个新的任务:
系统调用 作用
fork 复制当前进程
exec 使用新的可执行映像覆盖当前可执行映像
clone 创建子进程并从指定位置开始执行
fork()函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数里返回。pid_t pid;
if(pid = fork())
{
...
}
在fork函数调用之后,新的任务将启动并和本任务一起从fork函数返回。但不同的是本任务的fork将返回新任务的pid,而新任务的fork将返回0。
fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务一起共享一个写时复制(Copy on Write,COW)的内存空间。所谓写时复制,指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其它的任务使用。
fork只能够产生本任务的镜像,因此需要使用exec配合才能够启动别的新任务。exec可以用新的可执行映像替换当前的可执行映像,因此在fork产生了一个新任务之后,新任务可以调用exec来执行新的可执行文件。fork和exec通常用于产生新任务,而如果要产生新线程,则可以使用clone。
int clone(int (*fn)(void *), void* child_stack, int flags, void* arg);
使用clone可以产生一个新的任务,从指定的位置开始执行,并且(可选的)共享当前进程的内存空间和文件等。如此就可以在实际效果上产生一个线程。
线程安全
同步与锁
为了避免多个线程同时读写同一个数据而产生不可预料的后果,我们需要将各个线程对同一个数据的访问同步(Synchronization)。所谓同步,既是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。由此,对数据的访问被原子化了。
同步的最常用方法是使用锁(Lock)。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
二元信号量(Binary Semaphore)是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。挡二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其它的所有试图获取该二元信号量的线程将会等待,知道该锁被释放。
对于允许多个线程并发访问的资源,多元信号量简称信号量(Semaphore),它是一个很好的选择。一个初始值未N的信号量允许N哥线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:
1.将信号量的值减1;
2.如果信号量的值小于0,则进入等待状态,否则继续执行。
访问完资源之后,线程释放信号量,进行如下操作:
1.将信号量的值加1;
2.如果信号量的值小于1,唤醒一个等待中的线程。
互斥量(Mutex)和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任何线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其它线程越俎代庖去释放互斥量是无效的。
临界区(Critical Section)是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其它的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。