pthread到Win32thread

一.什么是线程。      程序员

线程(thread)是为了提升系统内程序的并发(concurrency)执行程度而提出来的概念,它是比进程更小的可以独立运行的基本单位。在引入线程的系统中,线程是处理器调度(schedule)的基本单位,而传统的进程则只是资源分配的基本单位。同一进程中的线程共享这个进程的所有资源与地址空间,除此以外,线程基本上不拥有其余任何系统资源,固然,每一个线程也必须拥有本身的程序计数器(Program Counter),寄存器堆(register file)和栈(stack)等等。即线程是一个轻量级实体(light-weight entity),它的结构(thread structure)相对简单,在切换速度上很是得快,同一进程中的线程切换不会引发进程的切换,对于并行计算来说,有效的利用线程可以改善计算的效率,简化计算的复杂性,因此Lilytask正是基于线程实现的。web

 

二.线程的标准。编程

       目前,主要有三种不一样的线程库的定义,分别是Win32,OS/2,以及POSIX,前两种定义只适合于他们各自的平台,而POSIX 定义的线程库是适用于全部的计算平台的,目前基本上全部基于UNIX的系统都实现了pthread。本文主要讨论Win32和POSIX的线程定义。多线程

线程的实现通常有两种方法,一是用户级的线程(user-level thread library),另外一种是核心级的线程(kernel-level library)。对于用户级的线程来讲,它只存在于用户空间中(user space),它的建立,撤销和切换都不利用系统调用,与核心无关;而核心级的线程依赖于核心,它的建立,撤销和切换都是由核心完成的,系统经过核心中保留的线程控制块来感知线程的存在并对线程进行控制。Win32thread是基于核心级线程实现的,而pthread部分是基于用户级线程实现的。并发

 

三.pthread和Win32thread的具体实现。函数

1.关于线程建立和消亡的操做。atom

1.1 建立和撤销一个POSIX线程spa

pthread_create(&tid, NULL, start_fn, arg);操作系统

pthread_exit(status);.net

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类似的功能。

       6.1.3 Win32的CriticalSection操做

InitializeCriticalSection(&cs);

EnterCriticalSection(&cs);

TryEnterCriticalSection(&cs);

LeaveCriticalSection(&cs);

DeleteCriticalSection(&cs);

6.2信号量semaphore最初是由E.W.Dijkstra于20世纪60年代引入的。一般,信号量是一个计数器和对于这个计数器的两个操做(分别称之为P,V操做),以及一个等待队列的总和。一个P操做使得计数器减小一次,若是计数器大于零,则执行P操做的线程继续执行,若是小于零,那么该线程就会被放入到等待队列中;一个V操做使得计数器增长一次,若是等待队列中由等待的线程,便释放一个线程。简单的,咱们能够经过一个图示来了解P,V操做的意义:

P操做:                                                                V操做:

 

continue

从图中能够看出,信号量的操做其实是包括了互斥量的。通常地说来,信号量的操做能够在不一样的线程中进行,而互斥量只能在同一个线程中操做,当互斥量和信号量要同时操做时,必定要注意互斥量的lock操做和信号量的P操做的顺序,一般应该是信号量的P操做在互斥量的lock操做以前,不然容易出现死锁。而互斥量的unlock操做和信号量的V操做则不存在这种序关系。

6.2.1 POSIX的信号量操做

sem_init(SEM, 0, VALUE);

sem_wait(SEM);

sem_trywait(SEM);

sem_destroy(SEM);

6.2.2 Win32的信号量操做

CreateSemaphore(NULL, 0, MaxVal, NULL);

WaitForSingleObject(SEM);

ReleaseSemaphore(SEM);

CloseHandle(SEM);

       6.3条件量condition variables是一种很是相似于信号量的同步变量,不一样的是,信号量关注的是counter的计数是多少,而条件量关注的仅仅是条件是否知足,换一句话说,条件量能够简单看做是计数器最大取值不超过1的信号量,但在它绝对不是信号量的简单实现,某些状况下,它比信号量更直观。同信号量同样,条件量是由一个待测条件,一组PV操做和一个等待队列组成的。它的PV操做和信号量的PV操做也很是的相似。

可是必须注意一点,在信号量中,lock和unlock是在信号量内部完成的,也就是说不须要使用者显式指定一个互斥量来进行互斥操做,而对于条件量来讲,就必须显式地指定一个互斥量来保证操做的原子性。因此条件量老是与一个相关的互斥量成对出现的。

       6.3.1 POSIX的条件量的操做

phtread_cond_init(COND, NULL);

phtread_cond_wait(COND, MUTEX);

phtread_cond_timedwait(COND, MUTEX, TIME);

phtread_cond_signal(COND);

phtread_cond_broadcast(COND);

phtread_cond_destroy(COND);

其中broadcast是用来唤醒全部等在该条件量上的线程。

Win32中并无条件量这个概念,可是它实现了一种叫作Event的同步变量,实质上和条件量是差很少的。

 

整体上来说,POSIX和Win32实现的线程库在功能上基本上重叠的,也就是说用其中一种线程库实现的程序大多数的时候都可以比较容易的用另外一种线程库来实现。下面列出了一张表,对比了一下两个线程库的异同:

 

POSIX thread library

Win32 thread library

设计思想

简单

复杂

级别

用户级/核心级

核心级

调度策略

进程局部/系统全局

系统全局

线程挂起/恢复

未实现

实现

互斥量

实现

实现

信号量

实现

实现

条件量

实现

实现(事件对象Event)

线程建立/撤销

实现

实现

线程等待

实现

实现

 

四.Lilytask2.5的Win32thread实现。

Lilytask中涉及到的Win32thread,主要表如今线程的建立和同步两个方面上,下面就简单的讲述如下这两个方面的实现。

1.线程的建立以及相关的处理。

在每个节点上,主线程要建立相应的线程:接收消息线程,发送消息线程,若是同一节点上的taskpool数目超过一个,则还要建立从处理线程,在lily_initial函数里边,要建立这几个线程:

//建立接收线程,启动_thread_routine_recv

thread_id=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)_thread_routine_recv,

                          NULL, 0, NULL);

//建立发送线程,启动_thread_routine_send

thread_id2=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)_thread_routine_recv,

                             NULL, 0, NULL);

//建立从处理线程

for(i=0; i<numofthrs-1; i++)

{

     thrid=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)lily_run_ready_task,

                             (void*)i, 0, NULL);

}

在建立完线程后,主线程和从线程都要求取得本线程的tid以及handle。线程的id用来肯定该线程对应的taskpool是哪个,线程的handle用来引用其自身,在wait的过程当中会用到。在pthread的实现中, tid和handle是一体的,由pthread_self调用就能够获得;而对于Win32thread来讲,要稍微的麻烦一点,tid和handle是不一样的,因此要分别保存线程的id和handle。获得线程的id用GetCurrentThreadId,而与此对应的有GetCurrentThread,是否是用该函数就能获得线程的handle呢?答案不必定的。GetCurrentThread返回的是一个pseudo-handle,是一个伪句柄,只能由GetCurrentThread的调用者引用,即thread自己才能引用这个pseudo-handle,而咱们的要求是其余的线程也能够引用这个handle,因此GetCurrentThread并不能知足咱们的要求。可是,咱们能够经过调用DuplicateHandle来复制一个真实的句柄,DuplicateHandle能够把一个进程中的一个handle(能够是pseudo-handle)复制给另外一个进程的一个handle,获得是一个真实的其余线程也能够引用的handle:

//保存线程的id

taskpool[numofthrs-1].pthid2=GetCurrentThreadId();

//保存线程的handle

DuplicateHandle(GetCurrentProcess(),GetCurrentThread(),GetCurrentProcess(),

         &taskpool[numofthrs-1].pthid, 0, FALSE, DUPLICATE_SAME_ ACCESS);

2.线程的同步问题。

Lilytask基于任务并行来实现,而且定义了任务的序关系,这就使得任务间必然存在等待的情形,因此在Lilytask中,线程的同步问题很是的重要。在Lilytask中,互斥量被大量使用,一方面是大量临界资源的存在,另外一方面是配合条件量的使用,上文已经指出,Win32 thread库中并无实现条件量,与此对应的是Event Object,可是在Lilytask2.5βforWindows的实现中,咱们并无用Event Object,一是由于利用Event Object的实现相对麻烦一些,而信号量则相对简单易懂;另外一缘由是在Lilytask中用到的条件量,在大多数时候能够看做是一个最大值不超过1的信号量,基于上面的两个缘由,Lilytask for Windows的主要的同步机制是利用互斥量和信号量来实现的。在Win32 thread中提供一个API:SignalObjectAndWait,能够用这个函数来模拟条件量的wait。条件量的wait操做在上文中的图示已经画出来了,其关键是开始时要lock临界区,而后判断条件,若是条件不成立,则unlock和sleep,其中unlock和sleep必须是原子操做的,正好SignalObjectAndWait也具备这个特性,因此用来模拟条件量很是的方便。

//互斥量做为临界区的锁来使用

WaitForSingleObject(taskpool[i].mutex_readylist_access, INFINITE);

taskpool[i].isSignal=TRUE;

……               //临界区操做

ReleaseMutex(taskpool[i].mutex_readylist_access);

 

//互斥量与信号量配合使用,实现的条件量的Wait操做

WaitForSingleObject(taskpool[i].mutex_readylist_access, INFINITE);

……             //临界区操做

SignalObjectAndWait(taskpool[i].mutex_readylist_access,

              taskpool[i].cond_readylist_access, INFINITE, FALSE);

WaitForSingleObject(taskpool[i].mutex_readylist_access, INFINITE);

……            //临界区操做

ReleaseMutex(taskpool[i].mutex_readylist_access);

信号量的释放操做,相对而言就比较简单,与pthread下的实现并没有二样。

除此以外,线程的同步还涉及到lily_finalize时要等待全部从线程的结束,虽然咱们说过彻底能够用信号量计数的方法取代wati(join)线程的方法,但就Lilytask这个实例来说,用wait线程的方法更简单明了。

//等待从线程的结束

for(i=0; i<numofthrs-1; i++)

{

WaitForSingleObject(taskpool[i].pthid, INFINITE);

……

}

另外还有一些关于线程的同步操做,好比pthread中trylock, timedlock等等,在上文的讨论中已经详细的说明了在Win32thread环境中的解决方法,就不一一赘述了。

 

五.总结。

总的说来,这两者在实现上是不同的,但在提供给用户的接口上,基本上是同样的(固然,你能够说API的名字是不同的,但咱们探讨仅仅是API的实质即它提供给用户的功能接口)。对于Lilytask,之因此要作for Windows的版本,是基于系统的异构性的缘由,Lilytask能够向上为用户屏蔽掉系统异构的差别,提供给用户一个不透明的编程模式,用户只需用Lilytask的原语写并行程序,不须要考虑系统的异构性,即用户写得程序无需作任何改动就能够在不一样的系统上运行,而这些解决异构这些繁琐的问题这是由Lilytask的预编译器调用不一样的库来实现的,大大的减轻了用户的负担。

相关文章
相关标签/搜索