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的預編譯器調用不一樣的庫來實現的,大大的減輕了用戶的負擔。

相關文章
相關標籤/搜索