C++ Builder 多執行緒程式設計技術經驗談

發表於2016-11-26

執行緒之可行性

在很多情況下,可能需要為程式建立執行緒。這裡給出其中一些可能性:

(1)如果建立的是一個多文件介面(Multiple Document Interface,MDI)程式,那麼為每個視窗分配一個執行緒就顯得十分重要了,例如,對於一個通過多個Modem同時連線到多個主機的MDI通訊程式而言,如果每個視窗都有它自己的執行緒來和一個主機通訊,那麼整個事情就簡化很多。

(2)如果使用的是一臺有多個處理器的機器,並希望充分利用所有可能獲得的CPU資源,那麼就需要將應用程式分解成多個執行緒。Windows2000中CPU的劃分單位為執行緒。因此,如果程式只包含一個執行緒,那麼,預設環境下該程式只能使用其中一個CPU.但是,如果將此程式劃分為多個執行緒,那麼Windows2000就可以在不同的CPU上執行各個執行緒。

(3)在後臺執行的某些任務的同時,要求使用者還可以繼續使用應用程式進行工作。利用執行緒很容易實現這點。例如:可以將一些冗長的重算、頁面格式化操作、檔案的讀寫等活動都放在單獨的執行緒中,使其在後臺執行,而不會對使用者造成影響。

同步

撰寫多執行緒程式的一個最具挑戰性的問題就是:如何讓一個執行緒和另一個執行緒合作。這引出了一個非常重要的問題:同步。所謂同步是指程式、執行緒間相互通訊時避免破壞各自資料的能力。Windows環境下的同步問題是由Win32系統的CPU時間片分配方式引起的。雖然在某一時刻,只有一個執行緒佔用CPU(單CPU)時間,但是無法知道在什麼時候,在什麼地方執行緒被打斷,這樣如何保證執行緒之間不破壞彼此的資料就顯得格外重要。同步問題是如此重要,也相當有趣,因而吸引了不少學者對他進行研究,由此產成了一系列經典的程式同步問題,其中較有代表性的是”生產者-消費者問題”、”讀者-寫者問題””哲學家進餐問題”等。在此,本文簡要討論了C++Builder平臺下如何利用多執行緒程式設計技術實現”生產者-消費者”問題,幫助我們更好得理解同步概念及其實現方法。

生產者-消費者問題

生產者-消費者問題是一個著名的程式同步問題。它描述的是:有一群生產者程式在生產訊息,並將此訊息提供給消費者程式去消費。為使生產者程式和消費者程式能併發進行,在他們之間設定了一個具有N個緩衝區的緩衝池,生產者程式可以將它所生產的訊息放入一個緩衝區中,消費者程式可以從一個緩衝區中取得一個訊息消費。儘管所有的生產者程式和消費者程式都是以非同步方式進行的,但他們之間必須保持同步,即不允許消費者程式到一個空的緩衝區中去取訊息,也不允許生產者程式向一個已裝滿訊息且尚未被取走訊息的緩衝區中投放訊息。

C++Builder 多執行緒應用程式程式設計基礎

1、使用 C++Builder 提供的 TThread 類

VCL類庫提供了用於執行緒程式設計的TThread類。在TThread類中封裝了Windows中關於執行緒機制的WindowSAPI.對於大多數的應用程式來說,可在應用程式中使用執行緒物件來表示執行執行緒。執行緒物件通過封裝使用執行緒所需的內容,簡化了多執行緒應用程式的編寫。注意,執行緒物件不允許控制執行緒堆疊的大小或其安全屬性。若需要控制這些,必須使用WindowsAPI的Create Thread()或Begin Thread()函式。

TThread類有以下一些屬性和方法:

1) 屬性:

·Priority:優先順序屬性。可以設定執行緒的優先順序。
·Return Value:返回值屬性。當執行緒介紹時返回給其他執行緒一個數值。
·Suspended:掛起屬性。可以判斷執行緒是否被掛起。
·Terminated:結束屬性。用來標誌是否應該結束執行緒。
·ThreadID:標識號屬性。在整個系統中執行緒的標識號。使用Windows API函式時該屬性非常有用。

2) 方法

·Do Terminate:產生一個On Terminate事件,但是不結束執行緒的執行。
·Resume:喚醒一個執行緒繼續執行。
·Suspend:掛起一個執行緒,要與Resume過程成對使用。
·Synchronize:由主VCL執行緒呼叫的一個同步過程。
·Terminate:將Terminate屬性設定為True,中止執行緒的執行。
·Wait For:等待執行緒的中止並返回Return Value屬性的數值。

2、協調執行緒

在編寫執行緒執行時執行的程式碼時,必須考慮到可能同步執行的其他執行緒的行為。特別注意,避免兩個執行緒試圖同時使用相同的全域性物件或變數。另外,一個執行緒中的程式碼會依賴其他執行緒執行任務的結果。

1) 避免同時訪問

為避免在訪問全域性物件或變數時與其他執行緒發生衝突,可能需要暫停其他執行緒的執行,直到該執行緒程式碼完成操作。

(1)鎖定物件。

一些物件內建了鎖定功能,以防止其他執行緒使用該物件的例項。例如,畫布物件(TCanvas及其派生類)有一種Lock()函式可以防止其他執行緒訪問畫布,直到呼叫Unlock()函式。顯然,這種方法只對部分類有效。

(2)使用重要區段。

若物件沒有提供內建的鎖定功能,可使用重要區段。重要區段像門一樣,每次只允許一個執行緒進入,要使用重要區段,需建立TCriticalSection的全域性例項。TCriticalSection有兩個函式:Acquire()(阻止其他執行緒執行該區域)及Release()(取消對其他執行緒的阻止)。

(3)使用多重讀、獨佔寫的同步器。

當使用重要區段來保護全域性記憶體時,每次只有一個執行緒可以使用該記憶體。這種保護可能會超出了需要,特別是有一個經常讀但很少寫的物件或變數時更是如此。多個執行緒同時讀相同記憶體但沒有執行緒寫記憶體是沒有危險的。當有一些經常被讀,但是很少寫的全域性變數時,可用TMultiReadExclusiveWriteSynchronizer物件保護它。這個物件和重要區段一樣,但它允許多個執行緒同時讀,只要沒有執行緒寫即可。每個需要讀記憶體的執行緒首先要呼叫Begin Read()函式(確保當前無其他執行緒寫記憶體),執行緒完成對保護記憶體讀操作後,要呼叫End Read()函式。任何執行緒需要防寫記憶體必須呼叫Begin Write()函式(確保當前無其他執行緒讀或寫記憶體),完成對保護記憶體寫操作後,呼叫End Write()函式。

(4)使用Synchronize函式:Void __fast call Synchronize (TThreadMethod &Method);

其中引數Method為一個不帶引數的過程名。在這個不帶引數的過程中是一些訪問VCL的程式碼。我們可以在Execute過程中呼叫Synchronize過程來避免對VCL的併發訪問。程式執行期間的具體過程實際上是由Synchronize過程來通知主執行緒,然後主執行緒在適當的時機來執行Synchronize過程的引數列表中的那個不帶引數的過程。在多個執行緒的情況下,主執行緒將Synchronize過程發過來的通知放到訊息佇列中,然後逐個地響應這些訊息。通過這種機制Synchronize實現了執行緒之間地同步。

3 等待其他執行緒

若執行緒必須等待另一執行緒完成某項任務,可讓執行緒臨時中斷執行。然後,要麼等待另一執行緒完全執行結束,要麼等待另一執行緒通知完成了該任務。

(1)等待執行緒執行結束

要等待另一執行緒執行結束,使用它地Wait For()函式。Wait For函式直到那個執行緒終止才返回,終止的方式要麼完成了其Execute()函式,要麼由於一個異常。

(2)等待任務完成。

有時,只需要等待執行緒完成一些操作而不是等待執行緒執行結束。為此,可使用一個事件物件。事件物件(TEvent)應具有全域性範圍以便他們能夠為所有執行緒可見。當一個執行緒完成一個被其他執行緒依賴的操作時,呼叫TEvent::Set Event()函式。Set Event發出一個訊號,以便其他執行緒可以檢查並得知操作完成。要關掉訊號,則使用Reset Event()函式。

例如,當必須等待若干執行緒完成其執行而不是單個執行緒時。因為不知道哪個執行緒最後完成,也就不能對某個執行緒使用Wait For()函式。此時,可通過呼叫Set Event以線上程結束時累加計數值並在最後一個執行緒結束時發出訊號以指示所有執行緒結束。

多執行緒應用程式程式設計例項

下面是一個實現”生產者-消費者問題”的多執行緒應用例項。在此例中,我們按上面介紹的方法構造了兩個TThread的子類TProducerThread(生產者執行緒)和TCustomerThread(消費者執行緒),生產和消費的商品僅僅是一個整數。在協調生產和消費的過程中,重要區段(TCriticalSection)和事件(TEvent)得到了應用。生產者通過TEvent類的物件Begin Consume來通知消費者開始消費,而消費者通過TEent類的物件Begin Produce通知生產者開始生產。程式中共有兩個生產者,一個消費者。在兩個生產者之間,通過TCriticalSection類的物件同步。其執行介面如圖1所示。

圖1 程式執行效果

主要源程式如下所示:

生產者執行緒

消費者執行緒

結論

本文討論了多執行緒程式設計及其可行性,說明了在Windows環境下進行多執行緒程式設計的意義,並重點討論了C++Builder平臺下如何開發多執行緒應用程式這一問題,通過實現”生產者-消費者問題”這一著名的程式同步問題,比較清晰地反映了在Windows環境下進行多執行緒程式設計技術及其實現的作用和效果。

相關文章