从pthread到Win32thread
——Lilytask2.5基于Win32thread的实现
段孟成(dmc@net.pku.edu)
Lilytask是以任务为单位的并行编程模型,Lilytask2.5β版最初是在Linux系统上基于POSIX thread实现的,为了更好的适应并行计算环境中的异构性,又在β版的基础上实现了for Windows版,在实现过程中,需要用Win32thread library替换POSIX thread library,下文将主要描述POSIX thread(下文称之pthread)与Win32thread的关系以及在Lilytask2.5 for Windows中的具体实现。
一.什么是线程。
线程(thread)是为了提高系统内程序的并发(concurrency)执行程度而提出来的概念,它是比进程更小的能够独立运行的基本单位。在引入线程的系统中,线程是处理器调度(schedule)的基本单位,而传统的进程则只是资源分配的基本单位。同一进程中的线程共享这个进程的全部资源与地址空间,除此之外,线程基本上不拥有其他任何系统资源,当然,每个线程也必须拥有
自己的程序计数器(Program Counter),寄存器堆(register file)和栈(stack)等等。即线程是一个轻量级实体(light-weight entity),它的结构(thread structure)相对简单,在切换速度上非常得快,同一进程中的线程切换不会引起进程的切换,对于并行计算来讲,有效的利用线程能够改善计算的效率,简化计算的复杂性,所以Lilytask正是基于线程实现的。
二.线程的标准。
目前,主要有三种不同的线程库的定义,分别是Win32,waitforsingleobject函数OS/2,以及POSIX,前两种定义只适合于他们各自的平台,而POSIX 定义的线程库是适用于所有的计算平台的,目前基本上所有基于UNIX的系统都实现了pthread。本文主要讨论Win32和POSIX的线程定义。
线程的实现一般有两种方法,一是用户级的线程(user-level thread library),另一种是核心级的线程(kernel-level library)。对于用户级的线程来说,它只存在于用户空间中(user space),它的创建,撤销和切换都不利用系统调用,与核心无关;而核心级的线程依赖于核心,它的创建,撤销和切换都是由核心完成的,系统通过核心中保留的线程控制块来感知线程的存在并对线程进行控制。Win32thread是基于核心级线程实现的,而pthread部分是基于用户级线程实现的。
三.pthread和Win32thread的具体实现。
1.关于线程创建和消亡的操作。
1.1 创建和撤销一个POSIX线程
pthread_create(&tid, NULL, start_fn, arg); pthread_exit(status); |
1.2 创建和撤销一个Win32线程
CreateThread(NULL, NULL, start_fn, arg, NULL, NULL); ExitThread(status); |
start_fn是该线程要执行的代码的入口。线程创建后,就拥有了一个TID(thread ID),以后对于该线程的操作都是通过TID来进行的。Win32虽然也定义了TID,但是它对于线程的操作是通过另外定义的一个句柄(handle)来进行的,总之,在线程创建完毕后,都有一个唯一了标识符来确定对该线程的引用。线程的撤销可以显式的调用上面列举的函数来实现,如果没有
显式调用撤销函数,则该线程执行的函数(即start_fn)返回时,该线程被撤销。关于创建和撤销线程,POSIX和Win32并无太大的区别。
2.关于线程的等待(join or wait for)的操作。
在多线程模型下,一个线程有可能必须等待其他的线程结束了才能继续运行。比如说司机和售票员,司机只有当售票员确定所有的人都上车了,即售票员的行动结束以后才能开车,在这之前司机必须等待。
2.1等待一个POSIX线程
pthread_join(T1); |
2.2等待一个Win32线程
WaitForSingleObject(T1); |
当调用上面的函数是,调用者会被阻塞起来,直到要等待的线程结束。对于POSIX,线程分
为可等待(non-detached)和不可等待(detached),只能对可等待的线程调用pthread_join(),而对不可等待的线程调用pthread_join()时,会返回一个错误。对于不可等待的线程,当线程消亡时,它的线程结构,栈,堆等资源会自动的归还给操作系统;而对于可等待的线程,当它消亡时并不自动归还,而需要程序员显式的调用等待函数来等待这个线程,由等待函数负责资源的归还。在线程创建的时候,你可以且定该线程是否为不可等待,如果没有显式确定,则默认为可等待。当然也可以通过调用pthread_detach()来动态的修改线程是否可等待。在Win32的线程中没有不可等待这个概念,所有的线程都是可以等待的,另外,Win32还提供一个调用WaitForMulitpleObject(T[]),可以用来等待多个线程。
关于为什么要使用等待操作,上面解释的很清楚,但实际上并不是如此,我们之所以要使用等待操作,是因为我们认为序关系中的前驱线程结束以后,后续线程才能从阻塞态恢复执行,在这里我们要明白一个问题,我们等待的仅仅是前驱线程执行的任务结束而不是前驱线程本身的结束,或许前驱线程在执行完任务后会有一些其它的操作,那么等待前驱线程的结束会浪费我们的时间,所以通常不是利用上面的等待函数,而是利用下面要提到的同步机制(synchronization)来解决这个问题。
3.关于线程挂起(suspend)的操作。
线程的挂起是指线程停止执行,进入睡眠状态,直到其他线程调用一个恢复函数时,该线程才脱离睡眠状态,恢复执行。
3.1 挂起和恢复一个Win32线程
SuspendThread(T1); ResumeThread(T1); |
POSIX并没有实现线程的挂起和恢复操作,对于某些场合,挂起操作可能非常有用,但在大多数情况下,挂起操作可能会带来致命的错误,如果被挂起的线程正拥有一个互斥量(mutex)或一个临界区(critical section),则不可避免的会出现死锁状态,所以通常也不使用挂起操作,如果一定要使用,必须检查会不会出现死锁情况。
4.关于线程的强制撤销(cancellation or killing)的操作。
一个线程有可能会通知另一个线程执行撤销操作,比如一个发送信息线程和一个接收信息线程,当发送方法送完所有信息,自身需要撤销时,它必须通知接受方发送完毕并且要求接受
方也要撤销。对于上面的这种情况,POSIX称为cancellation,Win32称为killing,在实质上二者并没有多大区别。
4.1 撤销一个POSIX线程
pthread_cancel(T1); |
4.2 撤销一个Win32线程
TerminateThread(T1); |
5.线程的调度(scheduling)
线程的调度机制通常分为两种:一种是进程局部调度(process local scheduling),一种是系统全局调度(system global scheduling)。局部调度是指线程的调度机制都是线程库自身在进程中完成的,与核心没有关系。POSIX对于两种调度机制都实现了,而Win32由于实现的是核心级线程,所以它的调度机制是全局的。线程的调度机制相当复杂,但对于线程库的使用者而不是开发者而言,线程的调度并不是最重要的东西,因为它主要是由操作系统和线程库
来实现,并不需要使用者使用多少。
6.线程的同步机制(synchronization)
线程的同步是一个非常重要的概念,也是使用者最需要注意的地方之一。如果线程的同步机制使用不当,非常容易造成死锁。同步机制是基于原子操作(atomic action)实现的,所谓原子操作是指该操作本身是不可分割的。为什么要使用线程同步机制?因为在程序中,可能会有共享数据和共享代码,对于共享数据,我们要确保对该数据的访问(通常是对数据的修改)是互斥的,不能两个线程同时访问这个共享数据,否则会造成错误;而对于共享的代码,如果这段代码要求的是互斥执行(通常把这段代码称为临界区),则也需要同步机制来实现。另外,对于一个线程,可能会需要等待另一个线程完成一定的任务才能继续执行,在这种情况下,也需要同步机制来控制线程的执行流程。通常,同步机制是由同步变量来实现的,一般说来,同步变量分为互斥量,信号量和条件量。
6.1 互斥量mutex是最简单的同步变量,它实现的操作实际上就是一把互斥锁,如果一个线程拥有了这个mutex,其他线程在申请拥有这个mutex的时候,就会被阻塞,直到等到先那个线程释放这个mutex。在任何时候,mutex至多只有一个拥有者,它的操作是完全排他性
的。
6.1.1 POSIX的mutex操作
pthread_mutex_init(MUTEX, NULL); pthread_mutex_lock(MUTEX); pthread_mutex_trylock(MUTEX); pthread_mutex_timedlock(MUTEX, ABSTIME); pthread_mutex_unlock(MUTEX); pthread_mutex_destroy(MUTEX); |
6.1.2 Win32的mutex操作
CreateMutex(NULL, FALSE, NULL); WaitForSingleObject(MUTEX); ReleaseMutex(MUTEX); CloseHandle(MUTEX); |
POSIX的mutex操作提供了trylock和timedlock的调用,目的是为了防止死锁,Win32的wait操作本身可以设定超时,因此可以用设定超时的方法来模拟POSIX中的trylock,虽然二者在操作的集合上不等势,但显然二者在功能上是等价的。另外,Win32还提供一个叫做CriticalSeciton的互斥量,简单说来,它就是一个轻量级的mutex,并且只能实现统一进程中的线程的同步,不能实现跨进程的线程间的同步。CriticalSection较之mutex来说,更快更高效,而且与POSIX相似,CriticalSection操作提供一个TryEnterCriticalSection的操作,用来监测该CriticalSection是否被锁上。但它没有实现与timedlock相似的功能。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论