摘要:生產(chǎn)者與消費者是多線程應用中一個必須解決的問題,它涉及到了線程之間的通訊的順暢。通過對C#中多種線程方法的研究,有效地完成了它們之間的同步運行。
關鍵詞:生產(chǎn)者和消費者;C#;線程同步
中圖分類號:TP311文獻標識碼:A文章編號:1009-3044(2008)35-2163-02
Study on Producer and Consumer Multi-threaded Synchronism Based on C#
JIANG Shan-shan, QUAN Lei
(Information Engineering School, East China Institute of Technology, Fuzhou 344000, China)
Abstract: Producers and consumers is a multi-threaded applications that must be addressed, it relates to the communications issues between threads . A variety of methods of research on C# simultaneous threads solves the problem of synchronization between them in an effective way.
Key words: producers and consumers; C#; thread synchronization
1 引言
引入線程的目的是為了支持多線程程序設計,即在一個程序中創(chuàng)建了多個線程。在多線程的程序中,當多個線程并發(fā)執(zhí)行時,雖然各個線程中語句的執(zhí)行順序是確定的,但線程的相對執(zhí)行順序是不確定的,在多個線程需要共享。共享存儲結(jié)構(gòu)時這種執(zhí)行順序的不確定性可能會產(chǎn)生執(zhí)行結(jié)果的不確定性,甚至可能造成程序出現(xiàn)錯誤。本文主要討論如何控制互相交互的線程之間的運行進度,使線程執(zhí)行時不出現(xiàn)錯誤結(jié)果,即線程間的同步。
2 線程同步
合理地同步一個程序是最精細的軟件開發(fā)任務之一。在深入到細節(jié)之前,應該首先確認使用同步是否不可避免。應該意識到,對程序中資源的訪問進行同步時,其難點來自于是使用細粒度鎖還是粗粒度鎖這個兩難的選擇。如果采用粗粒度的同步方式,雖然可以簡化代碼,但也會把自己暴露在爭用瓶頸的問題上。因此在開始談論有關同步機制之前,有必要先了解一下競態(tài)條件和死鎖。
2.1 競態(tài)條件
競態(tài)條件指的是一種特殊的情況,在這種情況下各個執(zhí)行單元以一種沒有邏輯的順序執(zhí)行動作,從而導致意想不到的結(jié)果。舉例,線程T修改資源R后,釋放了它對R的寫訪問權(quán),之后又重新奪回R的讀訪問權(quán)再使用它,并以為它的狀態(tài)仍然保持在它釋放它之后的狀態(tài)。但是在寫訪問權(quán)釋放后到重新奪回讀訪問權(quán)的這段時間間隔中,可能另一個線程已經(jīng)修改了R的狀態(tài)。
2.2 死鎖
死鎖指的是由于兩個或多個執(zhí)行單元之間相互等待對方結(jié)束而引起阻塞的情況。例如:
1) 一個線程T1獲得了對資源R1的訪問權(quán)。2) 一個線程T2獲得了對資源R2的訪問權(quán)。3) T1請求對R2的訪問權(quán)但是由于此權(quán)力被T2所占而不得不等待。4) T2請求對R1的訪問權(quán)但是由于此權(quán)力被T1所占而不得不等待。 T1和T2將永遠維持等待狀態(tài),此時我們陷入了死鎖的處境,針對此問題主要有三種解決方案:
1) 在同一時刻不允許一個線程訪問多個資源。2) 為所有訪問資源的請求系統(tǒng)地定義一個最大等待時間(超時時間),并妥善處理請求失敗的情況。幾乎所有的.NET的同步機制都提供了這個功能。 前兩種技術效率更高但是也更加難于實現(xiàn)。事實上,它們都需要很強的約束,而這點隨著應用程序的演變將越來越難以維護。大的項目通常使用第三種方法。
3線程同步主要實現(xiàn)方法
3.1 用lock語句實現(xiàn)互斥
Lock語句的形式如下:lock(e){訪問共享資源的代碼} ,這對{}內(nèi)部的代碼就是要鎖定的代碼(即臨界區(qū))
1) 其中e表示要鎖定的對象,鎖定該對象內(nèi)所有臨界區(qū),必須是引用類型,一般為this。
2) Lock語句將訪問共享資源的代碼標記為臨界區(qū)。臨界區(qū)的意義是:假設線程1正在執(zhí)行e對象的(某一個)臨界區(qū)中的代碼時,如其他線程也要求執(zhí)行這個e對象的任何臨界區(qū)中代碼,將被阻塞,一直到線程1退出臨界區(qū)。
3) 一個對象e臨界區(qū)可以具有多個。把訪問共享資源的代碼放在臨界區(qū)中。
4) 當某個線程(比如線程1執(zhí)行的代碼)進入到某個對象的一個臨界區(qū)(代碼)時,則其他線程都不能進入該對象的任何一個臨界區(qū)中去執(zhí)行;直到線程1(執(zhí)行的代碼的位置)離開該對象的臨界區(qū)以后,其他線程才可進入該對象的臨界區(qū)(去執(zhí)行代碼)。
3.2 用Mutex類實現(xiàn)互斥
可以使用Mutex類對象保護共享資源(如上例中的總?cè)藬?shù)變量)不被多個線程同時訪問。
Mutex類WaitOne方法和ReleaseMutex方法之間代碼是互斥體,這些代碼要訪問共享資源。Mutex類對象的WaitOne方法分配互斥體訪問權(quán),該方法只向一個線程授予對互斥體的獨占訪問權(quán)。如果一個線程獲取了互斥體,則要獲取該互斥體的第二個線程將被掛起,直到第一個線程用ReleaseMutex方法釋放該互斥體。這樣通過把訪問共享資源的代碼放在Mutex類對象的互斥體中,就可以避免多個線程同時執(zhí)行會訪問共享資源的代碼。
3.3 用Monitor類實現(xiàn)互斥
也可以使用Monitor類保護共享資源不被多個線程或進程同時訪問。
1) Monitor類通過向單個線程授予對象鎖來控制對對象的訪問。只有擁有對象鎖的線程才能執(zhí)行臨界區(qū)的代碼,此時其他任何線程都不能(再)獲取該對象鎖。
2) 只能使用Monitor類中的靜態(tài)方法,不能創(chuàng)建Monitor類的實例。Monitor類中的靜態(tài)方法主要有:
① 方法Enter:獲取參數(shù)指定對象的對象鎖。此方法放在臨界區(qū)的開頭。
② 方法Wait:釋放參數(shù)指定對象的對象鎖,以便允許其他被阻塞的線程獲取對象鎖。
③ 方法Pulse和PulseAll:向等待線程隊列中第一個或所有等待參數(shù)指定對象的對象鎖的線程發(fā)送信息,占用對象鎖的線程準備釋放對象鎖。
④ 方法Exit:釋放參數(shù)指定對象的對象鎖。
4 生產(chǎn)者線程和消費者線程的同步
在多線程應用中,產(chǎn)生數(shù)據(jù),并把數(shù)據(jù)存到公共數(shù)據(jù)區(qū)的線程稱之為生產(chǎn)者,使用數(shù)據(jù),從公共數(shù)據(jù)區(qū)取出數(shù)據(jù)的線程稱之為消費者。顯然如果公共數(shù)據(jù)區(qū)只能存一個數(shù)據(jù),那么在消費者線程取出數(shù)據(jù)前,生產(chǎn)者線程不能放新數(shù)據(jù)到公共數(shù)據(jù)區(qū),否則消費者線程將丟失數(shù)據(jù)。如果生產(chǎn)者線程還沒有把數(shù)據(jù)放到公共數(shù)據(jù)區(qū),這就表示公共數(shù)據(jù)區(qū)中沒有數(shù)據(jù),那么自然消費者線程不能去取數(shù)據(jù)。
上面敘述的就是所謂的生產(chǎn)者和消費者關系,實現(xiàn)時必須要求生產(chǎn)者線程和消費者線程同步。
下面以一個具體的例程來講解生產(chǎn)者線程和消費者線程同步的實現(xiàn)的具體方法:
首先定義1個全局布爾變量(用作產(chǎn)生/使用數(shù)據(jù)的標志變量):bool mark=1。
1) 變量mark=1時,表示① 數(shù)據(jù)還未放到公共數(shù)據(jù)區(qū)(這里假設為變量x)中。
② 生產(chǎn)者線程可以放數(shù)據(jù)到公共數(shù)據(jù)區(qū)中,由于沒有數(shù)據(jù),消費線程不能取數(shù)據(jù),必須等待。
2) 變量mark=true時,表示① 數(shù)據(jù)已放到公共數(shù)據(jù)區(qū)(x)中,消費線程還未取數(shù)據(jù)。
② 由于公共數(shù)據(jù)區(qū)(x)中有了數(shù)據(jù),消費線程可以取數(shù)據(jù)。③ 這時生產(chǎn)者線程不能再放新數(shù)據(jù)到公共數(shù)據(jù)區(qū)中,必須等待。
然后按照下面的樣例,安排生產(chǎn)者線程和消費者線程要執(zhí)行的代碼,就可實現(xiàn)同步。
public void Fun1()// 生產(chǎn)者線程執(zhí)行的方法,負責產(chǎn)生數(shù)據(jù)
{for(int k=1;k<5;k++)
{Monitor.Enter(this); //這里this是Form1類對象,得到this的對象鎖
//Monitor.Enter(this)和Monitor.Exit(this)是臨界區(qū)
if(mark) //如消費者數(shù)據(jù)未取走,釋放對象鎖,生產(chǎn)者等待
Monitor.Wait(this);mark=!mark;x=k;
Monitor.Pulse(this);//激活消費者線程
Monitor.Exit(this);//釋放this的對象鎖
}}
public void Fun2()//消費者線程執(zhí)行的方法,消費數(shù)據(jù)
{for(int k=0;k<4;k++)
{Monitor.Enter(this);
if(!mark) Monitor.Wait(this);//如果生產(chǎn)者未放數(shù)據(jù),消費者等待
mark=!mark; sum+=x;Monitor.Pulse(this);Monitor.Exit(this);
}}
在上述例中,生產(chǎn)者線程和消費者線程同步是通過等待Monitor.Pulse()來完成的。生產(chǎn)者生產(chǎn)一個值,同時消費者處于等待狀態(tài),生產(chǎn)者的“脈沖(Pulse)”過來通知它生產(chǎn)完成,消費者進入消費狀態(tài),生產(chǎn)者等待消費者完成操作后將調(diào)用Monitor.Pulese()發(fā)出的“脈沖”。
5 結(jié)束語
多線程共享數(shù)據(jù)或共享存儲結(jié)構(gòu)時,可能造成執(zhí)行結(jié)果的不確定性。事實上,上面這個簡單的例子已經(jīng)幫助我們解決了多線程應用程序中可能出現(xiàn)的大問題,只要領悟了解決線程間沖突的基本方法,很容易把它應用到比較復雜的程序中去。
參考文獻:
[1] Akhter S.多核程序設計技術——通過軟件多線程提升性能[M].北京:電子工業(yè)出版社,2007.
[2] 周炎濤.Windows中的多線程編程技術和實現(xiàn)[J].計算技術與自動化,2002(3).
[3] Beveridge J.Win32多線程程序設計[M].武漢:華中科技大學出版社,2002.