《Symbian OS:線程編程》 Symbian作業系統中的線程和進程在Symbian作業系統中,每個進程都有一個或多個線程。線程是執行的基本單位。一個進程的主線程是在進程啟動時產生的。 Symbian屬於搶佔式多任務作業系統,這意味著每個線程都有自己的執行時間,直到系統將CPU使用權給予其他線程。當系統調度時,具有最高優先權的線程將首先獲得執行。 進程邊界是受記憶體保護的。所有的使用者進程都有自己的記憶體位址空間,同一進程中的所有線程共用這一空間,使用者進程不能直接存取其他進程的地址空間。 每個線程都有它自己的stack和heap,這裡heap可以是私人的,也可以被其他線程共用。應用程式架構產生並安裝了一個active scheduler,並且為主線程準備了清除棧。如果沒有使用架構(如編寫exe程式)那就要手動產生這些了:) Symbian作業系統專為單線程應用最佳化,因此強烈推薦使用“使用中的物件”代替多線程。 一、使用單線程的優點 在每個線程都有自己的stack空間時,使用單線程可以減少記憶體耗費。 線上程間切換上下文要比切換使用中的物件(通過active scheduler)慢得多。 不需要處理互斥現象,這減少了程式的負擔,簡化了代碼,減少了錯誤發生的幾率。 一些資源只能為主線程所用,因此它們並不是安全執行緒的,如動態數組。 二、使用多線程的優點 有時為了保證所執行的任務的持久性,如播放聲音時,我們可以將其歸在一個單獨的線程中處理。 將複雜的多線程或長時間運行程式移植到Symbian上,如果不使用多執行緒,可能會比較難也更耗時間。 (題外話:我曾綺將一個棋類程式移植到symbian上,裡面複雜的遞迴運算使我不得不使用多線程,這樣的情況下,你是很難將時間有序的分化開來,使用使用中的物件的) 三、線程的基本使用方法 RThread提供了線程的各項功能。線程是為核心所擁有的對象,RThread對象封裝了這些對象的控制代碼。 1、產生一個新線程 新的線程可以通過構造一個RThread對象,並調用它的Create()函數產生。如:
Code:1: TInt threadFunction(TAny *aPtr)2: {3: // points to iParameter4: TInt *i = (TInt *)aPtr;5: ?_6: }7:8: RThread thread;9: thread.Create(KThreadName, threadFunction, 4096,10: KMinHeapSize, 256*KMinHeapSize, &iParameter);11: thread.Resume();2、線程狀態 一個線程的最重要的狀態為運行、準備、等待和暫停。在產生後,線程首先處於暫停狀態,你可以調用Resume()函數來啟動它的運行。一個線程也可以通過調用Suspend()來進入中斷狀態。 線程一般通過Kill(TInt aReason)來結束,Terminate()與其相似。如果一個進程的主線程結束,則該進程與所屬所有線程都將結束。 一種非正常關閉線程的方式就是調用Panic(const TDesC& aCategory, TInt aReason)來中斷執行。 如何獲得中斷線程的資訊呢,我們可通過ExitType(),ExitReason()以及ExitCategory()方法來獲得。 線程可以在中斷時發出請求,我們通過調用非同步方法呼叫Logon()來完成此任務。傳回值在aStatus中。LogonCancel()可以取消前次請求。 void Logon(TRequestStatus& aStatus) const; TInt LogonCancel(TRequestStatus& aStatus) const; 我們可以通過SetProtected(ETrue)方法將線程保護起來,也可以通過SetProtected(EFalse)來取消保護。在保護時,另一個線程是無法中斷、結束、異常中斷或設定該線程的優先順序的。Protected()方法可以返回該線程的保護狀態。 3、訪問線程及進程 我們可以通過構造一個RThread對象來訪問當前線程。Id()方法可以返回改線程的ID。 擁有此線程的進程可以通過訪問RThread的Process(RProcess& aProcess)方法來獲得。這裡傳入的參數應該是一個RProcess對象。 其他線程可以通過Open()方法來訪問。我們通過傳遞TThreadId、線程名稱或TFindThread對象來開啟線程。 TInt Open(const TDesC& aFullName, TOwnerType aType=EOwnerProcess); TInt Open(TThreadId aID, TOwnerType aType=EOwnerProcess); TInt Open(const TFindThread& aFind, TOwnerType aType=EOwnerProcess); Code:1: // * as a wildcard for the name search2: _LIT(KFindAll, “*”);3: // default RThread object, has a handle of the current thread4: RThread thread;5: TFullName fullName;6: TFindThread finder(KFindAll);7:8: while (finder.Next(fullName) == KErrNone)9: {10: // on success, variable ‘thread’ will contain a handle of11: // a thread found by finder12: thread.Open(finder);13:14: // get thread’s memory information15: TInt heapSize, stackSize;16: thread.GetRamSizes(heapSize, stackSize);17:18: // show fullName, heapSize and stackSize19: ...20: }4、線程優先順序 線程可以被賦予一個絕對或相對的優先順序。絕對優先順序定義了這個線程的總體優先順序,不需要考慮其擁有者進程的優先順序了。而賦予相對優先順序時則將此線稱定義為擁有者進程的優先順序加上該相對優先順序後的結果。 下面粗體標示的優先順序值可以由使用者代碼設定:
Code:enum TProcessPriority{ EPriorityLow=150, EPriorityBackground=250, EPriorityForeground=350, EPriorityHigh=450, EPriorityWindowServer=650, EPriorityFileServer=750, EPriorityRealTimeServer=850, EPrioritySupervisor=950};enum TThreadPriority{EPriorityNull=(-30),EPriorityMuchLess=(-20),EPriorityLess=(-10),EPriorityNormal=0,EPriorityMore=10,EPriorityMuchMore=20,EPriorityRealTime=30,EPriorityAbsoluteVeryLow=100,EPriorityAbsoluteLow=200,EPriorityAbsoluteBackground=300,EPriorityAbsoluteForeground=400,EPriorityAbsoluteHigh=500};上面枚舉出來的值中絕對優先順序值為: EPriorityAbsoluteVeryLow, EPriorityAbsoluteLow, EPriorityAbsoluteBackground, EPriorityAbsoluteForeground, EPriorityAbsoluteHigh. 相對優先順序值為: EPriorityMuchLess, EPriorityLess, EPriorityNormal, EPriorityMore, EPriorityMuchMore. EPriorityNull是一個特殊值,它定義了最低的層級,Kernel idel thread使用的就是它*_* EPriorityRealTime定義了除核心服務線程優先順序外最高的總體優先順序。 RThread中的Priority()方法返回了一個線程的優先順序(按以上描述值)。我們也可以通過SetPriority(TThreadPrioriy aPriority)方法來修改優先順序。 ProcessPriority()方法返回了擁有該線程之進程的優先順序(按TProcessPriority描述值)。我們也可以通過SetProcessPriority(TProcessPriority)方法來修改該進程的優先順序。 5、異常處理 每個線程都有自己的異常處理模組。當線程發生異常時會調用異常處理模組。異常處理模組的訽型為: typedef void TExceptionHandler(TExcType); RThread包含了下列異常處理相關的方法: TExceptionHandler* ExceptionHandler() 返回該線程當前異常處理模組的地址。 TInt SetExceptionHandler(TExceptionHandler* aHandler, TUint32 aMask); 定義了該線程新的異常處理模組的地址,以及它所處理異常的類別。 void ModifyExceptionMask(TUint32 aClearMask, TUint32 aSetMask) 修改異常處理模組所定之異常類別,aClearMask參數定義了不再為異常處理模組所處理的類別,而aSetMask則定義了新的處理類別。 TInt RaiseException(TExcType aType); 引發線程上指定類型的異常,這時異常處理模組將被啟動執行(發生在調用之後)。 TBool IsExceptionHandled(TExcType aType); 檢查線程的異常處理模組是否捕捉到aType類型的異常。 (1)異常類別及類型 異常類型是一組針對單個異常的類型識別,主要用在異常發生時。 異常類別則代表一組異常形式。 異常類別的一個集是由一個或多個異常類別通過OR形式組合成的,如KExceptionInteger|KExceptionDebug,這些值用來設定及修改異常處理模組所處理的類別。 下面列示了所有的類型及類別。 異常類別 異常類型 KExceptionInterrupt ->EExcGeneral, EExcUserInterrupt KExceptionInteger ->EExcIntegerDivideByZero, EExcIntegerOverflow KExceptionDebug->EExcSingleStep, EExcBreakPoint KExceptionFault ->EExcBoundsCheck, EExcInvalidOpCode, EExcDoubleFault, EExcStackFault, EExcAccessViolation, EExcPrivInstruction, EExcAlignment, EExcPageFault KExceptionFpe ->EExcFloatDenormal, EExcFloatDivideByZero, EExcFloatIndexactResult, EExcFloatInvalidOperation, EExcFloatOverflow, EExcFloatStackCheck, EExcFloatUnderflow KExceptionAbort ->EExcAbort KExceptionKill->EExcKill 6、其他線程函數 TInt Rename(const TDesC& aName) 為線程定義個新名字。 void RequestComplete(TRequestStatus*& aStatus, TInt aReason) 通知線程與一個非同步請求綁定的請求狀態物件aStatus已綺完成。sStatus完成代碼將負責設定aReason及發出線程請求訊號的通知。 TInt RequestCount() 返回線程請求訊號的數目。如果是負值則表示該線程正在等待至少一個異常請求的完成。 void HandleCount(TInt& aProcessHandleCount, TInt& aThreadHandleCount) 得到線程中及擁有該線程的進程中處理模組的數目。 RHeap* Heap() 返回一個指向改線程堆的指標。 TInt GetRamSizes(TInt& aHeapSize, TInt& aStackSize) 得到該線程中堆和棧的大小。 TInt GetCpuTime(TTimeIntervalMicroSeconds& aCpuTime) 得到改線程所分配到的CPU時間 void Context(TDes8& aDes) 得到該線程( sleeping狀態)所註冊的上下文環境。 四、線程內部的通訊 1)共用記憶體 線上程間交換資訊最直接的方法就是使用共用記憶體。線程入口函數中有一個參數TAny* aPtr,這個指標可以用於任何目的。通常可以用它來傳遞一個負責線程間共用資訊的資料結構或類執行個體。因為同一進程中的線程是共用記憶體位址空間的,因此這裡指標所指向的資料可以被兩個線程所共用,注意訪問該資料時必須是同步形式。 另外這裡的指標參數可以使用SetInitialParameter(TAny* aPtr)方法來改變,但這時線程應處於suspend狀態。 2)Client/Server API Symbian作業系統提供了一組基於server/session的API,允許一個線程扮演server的角色,向其他線程或進程提供服務。這裡API也提供處理一組方法處理資訊的傳遞,非同步以資料轉送。 3)進程內資料轉送 如果兩個線程分屬不同的進程,則他們無法直接管理需要通訊的資料,因為他們沒有共用的資料區。這裡可以使用RThread提供的ReadL()方法及WriteL()方法,我們可以用來在當前線程和由RThread提供的另一個線程間的地址空間拷貝8/16位的資料。這裡當前線程和另一個線程可以歸屬同一個進程也可分屬不同進程。 資料的傳輸是通過拷貝資料來完成的,RThread提供了方法返回在它地址空間內一個descriptor的長度及最大允許長度。 a>讀取另個線程所提供的descriptor void ReadL(const TAny* aPtr,TDes8& aDes,TInt anOffset) const; void ReadL(const TAny* aPtr,TDes16 &aDes,TInt anOffset) const; 這裡ReadL()方法從另一個線程的descriptor(由aPtr所指)中拷貝一組資料,傳遞到當前線程的descriptor(由aDes所指)。 aPtr指標必須指向一個在RThread控制代碼所指線程的地址空間中有效descriptor。 從源descriptor中的內容是從anOffset位置那裡開始拷貝到目的descriptor(aDes)的。 b>向另個線程寫入descriptor void WriteL(const TAny* aPtr, const TDesC8& aDes, TInt anOffset) const; void WriteL(const TAny* aPtr, const TDesC16& aDes, TInt anOffset) const; 用這個方法將當前線程descritor(aDes)所提供的資料都拷貝在另一個線程(aPtr所指)的descriptor中。這裡anOffset參數設定了目標descriptor的初始化拷貝位置。 aPtr為線程地址空間內有效可修改descriptor。 如果拷貝進去的資料長度超過目標descriptor的最大長度,則函數會發生異常。 c>Descriptor協助函數 TInt GetDesLength(const TAny* aPtr) const; TInt GetDesMaxLength(const TAny* aPtr) const; 這裡RThread的GetDesLength()方法可以返回aPtr所指向的descriptor長度。這裡descriptor必須為RThread控制代碼所指定的線程的地址空間中。 RThread的GetMaxDesLength()方法返回aPtr所指向descriptor的最大長度。descriptor也應在RThread控制代碼所指的線程地址空間中。 建議在ReadL()和WriteL()等方法前使用這些函數。 4)線程局部儲存(TLS) Symbian作業系統是不允許在DLL中出現可寫靜態變數的。然而每個DLL中每個線程都會分配一個32位字元空間。這個字元用來存放一個指向資料結構或類樣本的指標。分配和釋放這些資源可在例如DLL的入口函數E32Dll中處理。 另一個使用線程局部儲存的樣本為儲存指向類樣本的指標,這樣靜態回呼函數可以訪問與線程相聯絡的該對象。當我們處理自訂異常處理模組時是很有用的。 Dll::SetTls(TAny *aPtr)函數負責設定線程局部儲存的指標。 Dll::Tls()函數負責返回一個指向線程局部儲存的指標。取得後該指標所指定資料可以正常使用。 5) User-Interrupt Exception 如3.5“Exception Handling”所述,線程可以引發其他線程的異常。有一種異常類型是專為使用者所保留的,那就是EExcUserInterrupt,可以通過指定異常類型KExceptionUserInterrupt來處理。其他要傳遞的資訊應該通過共用記憶體來處理。這是在最段時間內向其他線程傳遞資訊的方式,當異常發生時調用RaiseException()函數可切換到另個線程的異常處理模組。 6) Publish & Subsribe Publish & Subscrible是一個進程間的通訊機制(在SymbianOS v8.0a和Series 60 Platform 2nd Editon, Feature Pack2中有所介紹),可以查看相關的文擋。 這個機制包括了三個基本方面:properties, publishers, 和subscribers.Properties是由一個標準SymbianOS UID所定義的全域唯一變數,它定義了屬性類別,而另一個整數定義了property sub-key。 Publishers是負責更新屬性的線程。Subscribers是負責監聽屬性變化的線程。 7) 訊息佇列 訊息佇列是另一個處理序間通訊的機制(在SymbianOS v8.0a和Series 60 Platform 2nd Editon, Feature Pack2中有所介紹)。 訊息佇列用來向隊列發送訊息,而無需獲得接收者的狀態標識資訊。任何進程(都在同一隊列中的)或任何同一進程中的線程(在局部隊列中)都可以讀取這些資訊。 五、同步 1)目的 如果多個線程在沒有保護機制的情況下使用同一資源,就會出現一些問題。如,線程A更新了部分descriptor,而線程B接手後又重寫了內容。回到線程A後,又開始更新內容。這樣descriptor的內容就在A與B中來回修改了。 為了防止這類情況的發生,你需要使用非搶佔式client/server機制或同步對象來處理。同步對象(mutex, semaphore, critical section)都是核心對象,可以通過控制代碼來訪問。他們會限制或直接鎖住對多線程們所要訪問的資源,這種資源形式被稱為共用資源。 在任何時刻只能有一個線程對共用資源進行寫操作,每個要訪問資源的線程都應使用同步機制來管理資源。 同步操作一般有如下步驟: 1. Call Wait() of the synchronization object reserved for this resource. 2. Access the shared resource. 3. Call Signal() of the synchronization object reserved for this resource. 注意,當kill線程時要小心點。因為如果線程使用已綺登出的對象,不同的同步對象其處理方式是不同的。因此,忽略使用同步類型而kill一個已綺更新過部分資源的線程是會引發問題的。 2)使用Semaphores(訊號) Semaphores可以管理共用資源的同步化訪問。這裡semaphore的控制代碼可通過RSemaphore類獲得。 Semaphore限制了同一時刻訪問共用資源的數目。semaphore計數的初始化工作可以放在建構函式中進行。 Semaphore可以是全域的也可以是局部的,全域的semaphore有自己的名稱,可以被其他進程搜尋並使用。而局部的semaphore沒有名稱,只能在同一進程間的線程中使用。 調用semaphore的Wait()方法將減少semaphore計數,而如果計數為負的話,調用線程就會進入等待狀態。 調用semaphore的Signal()方法將增加semaphore計數,如果增長之前為負數,則等待訊號的第一個線程將設定為準備運行狀態。 調用semaphore的Signal(TInt aCount)和調用n次Signal()效果是一樣的。 當線程死亡時,只有該線程正等待該訊號時,訊號才能被通知。因為訊號在下面這樣的情況也是可以執行的:在一個線程中調用Wait(),在另一個線程中調用Signal(),這樣的訊號無法在使用它的線程死亡時被通知。這樣只會導致訊號計數減低。 3)使用互斥(Mutex) 互斥主要使用在同步下獨佔訪問共用資源。它的控制代碼可以通過RMutex類來獲得。 和訊號一樣,互斥可以是全域也可以是局部的。唯一的不同在於其計數初始化時總為1。Mutex因此只允許最多一個訪問共用資源。 如果一個線程已綺為mutex調用Wait(),但沒有Signal(),則線程死亡時該互斥將被通知。 4)使用臨界區(Critical Sections) Critical Sections可用來在一單獨進程中獨佔訪問共用資源。Critical Sections控制代碼可以通過RCriticalSection類來獲得。 Critical Sections只能用在同一進程的線程間,通常它用來管理某段代碼的訪問,每次只能有一個線程來訪問。 同一線程中,在調用Wait()前調用Signale()將會引發線程的異常。但不會出現在其他類型的同步對象中。 線程的中斷是不會影響critical sections的狀態的,因此使用critical sections的線程將不會被其他線程殺死,除非不在critical sections中。當不在需要時,線程的死亡是不會有癬,很安全的。 5)同步執行個體
Code:1: class CMessageBuffer2: {3: public:4: CMessageBuffer();5: void AddMessage(const TDes &aMsg);6: void GetMessages(TDes &aMsgs);7:8: public:9: RMutex iMutex;10: TDes iMsgs;11: };12:13: CMessageBuffer::CMessageBuffer()14: {15: iMutex.CreateLocal();16: }17:18: void CMessageBuffer::AddMessage(const TDes &aMsg)19: {20: iMutex.Wait();21: iMsgs.Append(aMsg);22: iMutex.Signal();23: }24:25: void CMessageBuffer::GetMessages(TDes &aMsgs)26: {27: iMutex.Wait();28: aMsg.Copy(iMsgs);29: iMsgs.Zero();30: iMutex.Signal();31: }32:33: static void CMyClass::threadFunction(TAny *aPtr)34: {35: CMessageBuffer *msgBuffer = (CMessageBuffer *)TAny;36: TInt count = 0;37: TBuf<40> msg;38:39: while (TRUE)40: {41: msg.Format(“ID: %d, count: %d/n”, RThread().Id(), count++);42: msgBuffer->AddMessage(msg);43: User::After(1000 * 1000);44: }45: }在上面所述中,CMessageBuffer是一個半成品類,它允許使用者增加訊息到buffer中,也允許獲得所有訊息。 線程函數CMyClass::threadFunction負責向CMessageBuffer共用對象添加資訊,這裡記憶體配置和錯誤檢查並沒有列出,需要讀者自己完成。 假設有多個線程要共用CMessageBuffer對象執行個體,則在實際訪問buffer時必須要同步來處理。我們也可線上程函數中完成,但在CMessageBuffer中完成同步將使得該類成為安全執行緒級,同樣它也可以被用在單個線程中了。 六、總結 很多情況下都需要多線程的,當使用多線程時,同步及互斥排他也要考慮在內,以便保證線程通訊的安全性。如果線程使用共用資源,我們應該使用某種同步機制來避免異常的發生,Semaphores, critical sections,和mutexes都提供了基本的解決方案。此外,如果使用使用中的物件或清除機制,我們還需要手工設定active scheduler和清除棧。總的來說,線程編程不是這麼容易的,因為這類編程需要全面理解架構、多任務和安全執行緒機制。
|