POSIX 執行緒詳解(3) (轉)

worldblog發表於2008-01-24
POSIX 執行緒詳解(3) (轉)[@more@]

Daniel Robbins
總裁兼 CEO,Gentoo Technologies, Inc.
2000 年 9 月

本文是 POSIX 執行緒三部曲系列的最後一部分,Daniel 將詳細討論如何使用條件變數。條件變數是 POSIX 執行緒結構,可以讓您在遇到某些條件時“喚醒”執行緒。可以將它們看作是一種執行緒的訊號傳送。Daniel 使用目前您所學到的知識實現了一個多執行緒工作組應用,本文將圍繞著這一示例而進行討論。/develop/read_article.?id=19727">上一篇文章結束時,我描述了一個比較特殊的難題:如果執行緒正在等待某個特定條件發生,它應該如何處理這種情況?它可以重複對互斥鎖定和解鎖,每次都會檢查共享資料結構,以查詢某個值。但這是在浪費時間和資源,而且這種繁忙查詢的非常低。解決這個問題的最佳方法是使用 pthread_cond_wait() 來等待特殊條件發生。

瞭解 pthread_cond_wait() 的作用非常重要 -- 它是 POSIX 執行緒訊號傳送的核心,也是最難以理解的部分。

首先,讓我們考慮以下情況:執行緒為檢視已連結列表而鎖定了互斥物件,然而該列表恰巧是空的。這一特定執行緒什麼也幹不了 -- 其設計意圖是從列表中除去節點,但是現在卻沒有節點。因此,它只能:

鎖定互斥物件時,執行緒將呼叫 pthread_cond_wait(&mycond,&mymutex)。pthread_cond_wait() 呼叫相當複雜,因此我們每次只它的一個操作。

pthread_cond_wait() 所做的第一件事就是同時對互斥物件解鎖(於是其它執行緒可以修改已連結列表),並等待條件 mycond 發生(這樣當 pthread_cond_wait() 接收到另一個執行緒的“訊號”時,它將甦醒)。現在互斥物件已被解鎖,其它執行緒可以訪問和修改已連結列表,可能還會新增項。

此時,pthread_cond_wait() 呼叫還未返回。對互斥物件解鎖會立即發生,但等待條件 mycond 通常是一個阻塞操作,這意味著執行緒將睡眠,在它甦醒之前不會消耗 週期。這正是我們期待發生的情況。執行緒將一直睡眠,直到特定條件發生,在這期間不會發生任何浪費 CPU 時間的繁忙查詢。從執行緒的角度來看,它只是在等待 pthread_cond_wait() 呼叫返回。

現在繼續說明,假設另一個執行緒(稱作“2 號執行緒”)鎖定了 mymutex 並對已連結列表新增了一項。在對互斥物件解鎖之後,2 號執行緒會立即呼叫 pthread_cond_broadcast(&mycond)。此操作之後,2 號執行緒將使所有等待 mycond 條件變數的執行緒立即甦醒。這意味著第一個執行緒(仍處於 pthread_cond_wait() 呼叫中)現在將甦醒。

現在,看一下第一個執行緒發生了什麼。您可能會認為在 2 號執行緒呼叫 pthread_cond_broadcast(&mymutex) 之後,1 號執行緒的 pthread_cond_wait() 會立即返回。不是那樣!實際上,pthread_cond_wait() 將執行最後一個操作:重新鎖定 mymutex。一旦 pthread_cond_wait() 鎖定了互斥物件,那麼它將返回並允許 1 號執行緒繼續執行。那時,它可以馬上檢查列表,檢視它所感興趣的更改。

訊號,而是來自 pthread_cond_signal() 或 pthread_cond_broadcast() 呼叫的訊號),它就會甦醒。但 pthread_cond_wait() 沒有立即返回 -- 它還要做一件事:重新鎖定 mutex:

pthread_mutex_lock(&mymutex);


pthread_cond_wait() 知道我們在查詢 mymutex “背後”的變化,因此它繼續操作,為我們鎖定互斥物件,然後才返回。

pthread_cond_t mycond;


然後,呼叫以下函式進行初始化:

pthread_cond_init(&mycond,NULL);


瞧,初始化完成了!在釋放或廢棄條件變數之前,需要毀壞它,如下所示:

pthread_cond_destroy(&mycond);


很簡單吧。接著討論 pthread_cond_wait() 呼叫。

等待
一旦初始化了互斥物件和條件變數,就可以等待某個條件,如下所示:

pthread_cond_wait(&mycond, &mymutex);


請注意,程式碼在邏輯上應該包含 mycond 和 mymutex。一個特定條件只能有一個互斥物件,而且條件變數應該表示互斥資料“內部”的一種特殊的條件更改。一個互斥物件可以用許多條件變數(例如,cond_empty、cond_full、cond_cleanup),但每個條件變數只能有一個互斥物件。

傳送訊號和廣播
對於傳送訊號和廣播,需要注意一點。如果執行緒更改某些共享資料,而且它想要喚醒所有正在等待的執行緒,則應使用 pthread_cond_broadcast 呼叫,如下所示:

pthread_cond_broadcast(&mycond);


在某些情況下,活動執行緒只需要喚醒第一個正在睡眠的執行緒。假設您只對佇列新增了一個工作作業。那麼只需要喚醒一個工作程式執行緒(再喚醒其它執行緒是不禮貌的!):

pthread_cond_signal(&mycond);


此函式只喚醒一個執行緒。如果 POSIX 執行緒標準允許指定一個整數,可以讓您喚醒一定數量的正在睡眠的執行緒,那就更完美了。但是很可惜,我沒有被邀請參加會議。

工作組
我將演示如何建立多執行緒工作組。在這個方案中,我們建立了許多工作程式執行緒。每個執行緒都會檢查 wq(“工作佇列”),檢視是否有需要完成的工作。如果有需要完成的工作,那麼執行緒將從佇列中除去一個節點,執行這些特定工作,然後等待新的工作到達。

與此同時,主執行緒負責建立這些工作程式執行緒、將工作新增到佇列,然後在它退出時收集所有工作程式執行緒。您將會遇到許多 C 程式碼,好好準備吧!

佇列
需要佇列是出於兩個原因。首先,需要佇列來儲存工作作業。還需要可用於跟蹤已終止執行緒的資料結構。還記得前幾篇文章(請參閱本文結尾處的
Linux/thread/posix_thread3/index.shtml#res">參考資料

)中,我曾提到過需要使用帶有特定程式標識的 pthread_join 嗎?使用“清除佇列”(稱作 "cq")可以解決無法等待任何已終止執行緒的問題(稍後將詳細討論這個問題)。以下是標準佇列程式碼。將此程式碼儲存到 queue.h 和 queue.c:

queue.h

/* queue.h ** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc. ** Author: Daniel Robbins ** Date: 16 Jun 2000 */ typedef struct node { struct node *next; } node; typedef struct queue { node *head, *tail; } queue; void queue_init(queue *my); void queue_put(queue *myroot, node *mynode); node *queue_get(queue *myroot);


queue.c

/* queue.c ** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc. ** Author: Daniel Robbins ** Date: 16 Jun 2000 ** ** This set of queue functions was originally thread-aware. I ** redesigned the code to make this set of queue routines ** thread-ignorant (just a generic, boring yet very fast set of queue ** routines). Why the change? Because it makes more sense to have ** the thread support as an optional add-on. Consr a situation ** where you want to add 5 nodes to the queue. With the ** thread-enabled version, each call to queue_put() would ** automatically lock and unlock the queue mutex 5 times -- that's a ** lot of unnecessary overhead. However, by moving the thread stuff ** out of the queue routines, the caller can lock the mutex once at ** the beginning, then insert 5 items, and then unlock at the end. ** Moving the lock/unlock code out of the queue functions allows for ** optimizations that aren't possible otherwise. It also makes this ** code useful for non-threaded applications. ** ** We can easily thread-enable this data structure by using the ** data_control type defined in control.c and control.h. */ #include #include "queue.h" void queue_init(queue *myroot) { myroot->head=NULL; myroot->tail=NULL; } void queue_put(queue *myroot,node *mynode) { mynode->next=NULL; if (myroot->tail!=NULL) myroot->tail->next=mynode; myroot->tail=mynode; if (myroot->:head==NULL) myroot->head=mynode; } node *queue_get(queue *myroot) { //get from root node *mynode; mynode=myroot->head; if (myroot->head!=NULL) myroot->head=myroot->head->next; return mynode; }


data_control 程式碼
我編寫的並不是執行緒安全的佇列例程,事實上我建立了一個“資料包裝”或“控制”結構,它可以是任何執行緒支援的資料結構。看一下 control.h:

control.h

#include typedef struct data_control { pthread_mutex_t mutex; pthread_cond_t cond; int active; } data_control;


現在您看到了 data_control 結構定義,以下是它的視覺表示:

-7-171424251.gif" border=0 valign="TOP">


影像中的鎖代表互斥物件,它允許對資料結構進行互斥訪問。黃色的星代表條件變數,它可以睡眠,直到所討論的資料結構改變為止。on/off 開關表示整數 "active",它告訴執行緒此資料是否是活動的。在程式碼中,我使用整數 active 作為標誌,告訴工作佇列何時應該關閉。以下是 control.c:

a thread work crew to shut ** down instead of processing new jobs. Use the control_activate() ** and control_deactivate() functions, which will also broadcast on ** the data_control struct's condition variable, so that all threads ** stuck in pthread_cond_wait() will wake up, have an opportunity to ** notice the change, and then tenate. */ #include "control.h" int control_init(data_control *mycontrol) { int mystatus; if (pthread_mutex_init(&(mycontrol->mutex),NULL)) return 1; if (pthread_cond_init(&(mycontrol->cond),NULL)) return 1; mycontrol->active=0; return 0; } int control_destroy(data_control *mycontrol) { int mystatus; if (pthread_cond_destroy(&(mycontrol->cond))) return 1; if (pthread_cond_destroy(&(mycontrol->cond))) return 1; mycontrol->active=0; return 0; } int control_activate(data_control *mycontrol) { int mystatus; if (pthread_mutex_lock(&(mycontrol->mutex))) return 0; mycontrol->active=1; pthread_mutex_unlock(&(mycontrol->mutex)); pthread_cond_broadcast(&(mycontrol->cond)); return 1; } int control_deactivate(data_control *mycontrol) { int mystatus; if (pthread_mutex_lock(&(mycontrol->mutex))) return 0; mycontrol->active=0; pthread_mutex_unlock(&(mycontrol->mutex)); pthread_cond_broadcast(&(mycontrol->cond)); return 1; }


時間
在開始除錯之前,還需要一個檔案。以下是 ug.h:

.h

#define dabort() { printf("Aborting at line %d in source file %sn",__LINE__,__FILE__); abort(); }


此程式碼用於處理工作組程式碼中的不可糾正錯誤。

工作組程式碼
說到工作組程式碼,以下就是:

workcrew.c

#include #include #include "control.h" #include "queue.h" #include "dbug.h" /* the work_queue holds tasks for the various threads to complete. */ struct work_queue { data_control control; queue work; } wq; /* I added a job number to the work node. Normally, the work node would contain additional data that needed to be processed. */ typedef struct work_node { struct node *next; int jobnum; } wnode; /* the cleanup queue holds stopped threads. Before a thread terminates, it adds itself to this list. Since the main thread is waiting for changes in this list, it will then wake up and clean up the newly terminated thread. */ struct cleanup_queue { data_control control; queue cleanup; } cq; /* I added a thread number (for debugging/instructional purposes) and a thread id to the cleanup node. The cleanup node gets passed to the new thread on startup, and just before the thread stops, it attaches the cleanup node to the cleanup queue. The main thread monitors the cleanup queue and is the one that performs the necessary cleanup. */ typedef struct cleanup_node { struct node *next; int threadnum; pthread_t tid; } cnode; void *threadfunc(void *myarg) { wnode *mywork; cnode *mynode; mynode=(cnode *) myarg; pthread_mutex_lock(&wq.control.mutex); while (wq.control.active) { while (wq.work.head==NULL && wq.control.active) { pthread_cond_wait(&wq.control.cond, &wq.control.mutex); } if (!wq.control.active) break; //we got something! mywork=(wnode *) queue_get(&wq.work); pthread_mutex_unlock(&wq.control.mutex); //perform processing... printf("Thread number %d processing job %dn",mynode->threadnum,mywork->jobnum); free(mywork); pthread_mutex_lock(&wq.control.mutex); } pthread_mutex_unlock(&wq.control.mutex); pthread_mutex_lock(&cq.control.mutex); queue_put(&cq.cleanup,(node *) mynode); pthread_mutex_unlock(&cq.control.mutex); pthread_cond_signal(&cq.control.cond); printf("thread %d shutting down...n",mynode->threadnum); return NULL; } #define NUM_WORKERS 4 int numthreads; void join_threads(void) { cnode *curnode; printf("joining threads...n"); while (numthreads) { pthread_mutex_lock(&cq.control.mutex); /* below, we sleep until there really is a new cleanup node. This takes care of any false wakeups... even if we break out of pthread_cond_wait(), we don't make any assumptions that the condition we were waiting for is true. */ while (cq.cleanup.head==NULL) { pthread_cond_wait(&cq.control.cond,&cq.control.mutex); } /* at this point, we hold the mutex and there is an item in the list that we need to process. First, we remove the node from the queue. Then, we call pthread_join() on the tid stored in the node. When pthread_join() returns, we have cleaned up after a thread. Only then do we free() the node, decrement the number of additional threads we need to wait for and repeat the entire process, if necessary */ curnode = (cnode *) queue_get(&cq.cleanup); pthread_mutex_unlock(&cq.control.mutex); pthread_join(curnode->tid,NULL); printf("joined with thread %dn",curnode->threadnum); free(curnode); numthreads--; } } int create_threads(void) { int x; cnode *curnode; for (x=0; xthreadnum=x; if (pthread_create(&curnode->tid, NULL, threadfunc, (void *) curnode)) return 1; printf("created thread %dn",x); numthreads++; } return 0; } void initialize_structs(void) { numthreads=0; if (control_init(&wq.control)) dabort(); queue_init(&wq.work); if (control_init(&cq.control)) { control_destroy(&wq.control); dabort(); } queue_init(&wq.work); control_activate(&wq.control); } void cleanup_structs(void) { control_destroy(&cq.control); control_destroy(&wq.control); } int main(void) { int x; wnode *mywork; initialize_structs(); /* CREATION */ if (create_threads()) { printf("Error starting threads... cleaning up.n"); join_threads(); dabort(); } pthread_mutex_lock(&wq.control.mutex); for (x=0; x<16000; x++) { mywork=malloc(sizeof(wnode)); if (!mywork) { printf("ouch! can't malloc!n"); break; } mywork->jobnum=x; queue_put(&wq.work,(node *) mywork); } pthread_mutex_unlock(&wq.control.mutex); pthread_cond_broadcast(&wq.control.cond); printf("slee...n"); sleep(2); printf("deactivating work queue...n"); control_deactivate(&wq.control); /* CLEANUP */ join_threads(); cleanup_structs(); }

初排程式碼。定義的第一個結構稱作 "wq",它包含了 data_control 和佇列頭。data_control 結構用於仲裁對整個佇列的訪問,包括佇列中的節點。下一步工作是定義實際的工作節點。要使程式碼符合本文中的示例,此處所包含的都是作業號。

接著,建立清除佇列。註釋說明了它的工作方式。好,現在讓我們跳過 threadfunc()、join_threads()、create_threads() 和 initialize_structs() 呼叫,直接跳到 main()。所做的第一件事就是初始化結構 -- 這包括初始化 data_controls 和佇列,以及啟用工作佇列。

。我們還將清除節點作為初始自變數傳遞給每一個新的工作程式執行緒。為什麼這樣做?

因為當某個工作程式執行緒退出時,它會將其清除節點連線到清除佇列,然後終止。那時,主執行緒會在清除佇列中檢測到這個節點(利用條件變數),並將這個節點移出佇列。因為 TID(執行緒標識)在清除節點中,所以主執行緒可以確切知道哪個執行緒已終止了。然後,主執行緒將呼叫 pthread_join(tid),並聯接適當的工作程式執行緒。如果沒有做記錄,那麼主執行緒就需要按任意順序聯接工作程式執行緒,可能是按它們的建立順序。由於執行緒不一定按此順序終止,那麼主執行緒可能會在已經聯接了十個執行緒時,等待聯接另一個執行緒。您能理解這種設計決策是如何使關閉程式碼加速的嗎(尤其在使用幾百個工作程式執行緒的情況下)?

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-998296/,如需轉載,請註明出處,否則將追究法律責任。

相關文章