(轉)LabWindows™/CVI中的多線程技術

轉自:https://blog.csdn.net/grief_of_the_nazgul/article/details/8008349

目錄(?)[+]

http://zone.ni.com/devzone/cda/tut/p/id/6943#toc3

LabWindows™/CVI中的多線程技術

2 ratings | 2.50 out of 5
Read in 
 |  Print

Overview

 


多核編程基礎系列白皮書

多任務、多線程和多處理這些術語經常被交替地使用,但是它們在本質上是不同的概念。多任務是指操作系統具有在任務間快速切換使得這些任務看起來是在同步執行的能力。在一個搶佔式多任務系統中,應用程序可以隨時被暫停。使用多線程技術,應用程序可以把它的任務分配到單獨的線程中執行。在多線程程序中,操作系統讓一個線程的代碼執行一段時間(被稱爲時間片)後,會切換到另外的線程繼續運行。暫停某個線程的運行而開始執行另一個線程的行爲被稱爲線程切換。通常情況下,操作系統進行線程切換的速度非常快,令用戶覺得有多個線程在同時運行一樣。多處理指的是在一臺計算機上使用多個處理器。在對稱式多處理(SMP)系統中,操作系統自動使用計算機上所有的處理器來執行所有準備運行的線程。藉助於多處理的能力,多線程應用程序可以同時執行多個線程,在更短的時間內完成更多的任務。

單線程應用程序移植到多核處理器上運行不會獲得性能上的改進,這是因爲它們只能在其中一個處理器上運行,而不能像多線程應用程序那樣在所有的處理器上同時運行。而且單線程應用程序需要承受操作系統在處理器間切換所需要的開銷。爲了在多線程操作系統和/或多處理器計算機上獲得最優異的性能,我們必須使用多線程技術來編寫應用程序。

 

進行多線程編程的原因

在程序中使用多線程技術的原因主要有四個。最常見的原因是多個任務進行分割,這些任務中的一個或多個是對時間要求嚴格的而且易被其他任務的運行所幹涉。例如,進行數據採集並顯示用戶界面的程序就很適合使用多線程技術實現。在這種類型的程序中,數據採集是時間要求嚴格的任務,它很可能被用戶界面的任務打斷。在LabWindows/CVI程序中使用單線程方法時,程序員可能需要從數據採集緩衝區讀出數據並將它們顯示到用戶界面的曲線上,然後處理事件對用戶界面進行更新。當用戶在界面上進行操作(如在圖表上拖動光標)時,線程將繼續處理用戶界面事件而不能返回到數據採集任務,這將導致數據採集緩衝區的溢出。而在LabWindows/CVI程序中使用多線程技術時,程序員可以將數據採集操作放在一個線程中,而將用戶界面處理放在另一個線程中。這樣,在用戶對界面進行操作時,操作系統將進行線程切換,爲數據採集線程提供完成任務所需的時間。

在程序中使用多線程技術的第二個原因是程序中可能需要同時進行低速的輸入/輸出操作。例如,使用儀器來測試電路板的程序將從多線程技術中獲得顯著的性能提升。在LabWindows/CVI程序中使用單線程技術時,程序員需要從串口發送數據,初始化電路板。,程序需要等待電路板完成操作之後,再去初始化測試儀器。必須要等待測試儀器完成初始化之後,再進行測量,。在LabWindows/CVI程序中使用多線程技術時,你可以使用另一個線程來初始化測試儀器。這樣,在等待電路板初始化的同時等待儀器初始化。低速的輸入/輸出操作同時進行,減少了等待所需要的時間總開銷。

在程序中使用多線程技術的第三個原因是藉助多處理器計算機來提高性能。計算機上的每個處理器可以都執行一個線程。這樣,在單處理器計算機上,操作系統只是使多個線程看起來是同時執行的,而在多處理器計算機上,操作系統纔是真正意義上同時執行多個線程的。例如,進行數據採集、將數據寫入磁盤、分析數據並且在用戶界面上顯示分析數據,這樣的程序很可能通過多線程技術和多處理器計算機運行得到性能提升。將數據寫到磁盤上和分析用於顯示的數據是可以同時執行的任務。

在程序中使用多線程技術的第四個原因是在多個環境中同時執行特定的任務。例如,程序員可以在應用程序中利用多線程技術在測試艙進行並行化測試。使用單線程技術,應用程序需要動態分配空間來保存每個艙中的測試結果。應用程序需要手動維護每個記錄及其對應的測試艙的關係。而使用多線程技術,應用程序可以創建獨立的線程來處理每個測試艙。然後,應用程序可以使用線程局部變量爲每個線程創建測試結果記錄。測試艙與結果記錄間的關係是自動維護的,使應用程序代碼得以簡化。

選擇合適的操作系統

微軟公司的Windows 9x系列操作系統不支持多處理器計算機。所以,你必須在多處理器計算機上運行Windows Vista/XP/2000/NT 4.0系統來享受多處理器帶來的好處。而且,即使在單處理器計算機上,多線程程序在Windows Vista/XP/2000/NT 4.0上的性能也比在Windows 9x上好。這要歸功於Windows Vista/XP/2000/NT 4.0系統有着更爲高效的線程切換技術。但是,這種性能上的差別在多數多線程程序中體現得並不是十分明顯。

對於程序開發,特別是編寫和調試多線程程序而言,Windows Vista/XP/2000/NT 4.0系列操作系統比Windows 9x系列更爲穩定,當運行操作系統代碼的線程被暫停或終止的時候,操作系統的一些部分有可能出於不良狀態中。這種情況使得Windows 9x操作系統崩潰的機率遠遠高於Windows Vista/XP/2000/NT 4.0系統的機率。所以,NI公司推薦用戶使用運行Windows Vista/XP/2000/NT 4.0操作系統的計算機來開發多線程程序。

LabWindows/CVI中的多線程技術簡介

NI LabWindows/CVI軟件自二十世紀九十年代中期誕生之日起就支持多線程應用程序的創建。現在,隨着多核CPU的廣泛普及,用戶可以使用LabWindows/CVI來充分利用多線程技術的優勢。

與Windows SDK threading API(Windows 軟件開發工具包線程API)相比,LabWindows/CVI的多線程庫提供了以下多個性能優化:

  • Thread pools幫助用戶將函數調度到獨立的線程中執行。Thread pools處理線程緩存來最小化與創建和銷燬線程相關的開銷。
  • Thread-safe queues對線程間的數據傳遞進行了抽象。一個線程可以在另一個線程向隊列寫入數據的同時,從隊列中讀取數據。
  • Thread-safe variables高效地將臨界代碼段和任意的數據類型結合在一起。用戶可以調用簡單的函數來獲取臨界代碼段,設定變量值,然後釋放臨界代碼段。
  • Thread locks提供了一致的API並在必要時自動選擇合適的機制來簡化臨界代碼段和互斥量的使用。例如,如果需要在進程間共享互斥鎖,或者線程需要在等待鎖的時候處理消息,LabWindows/CVI會自動使用互斥量。臨界代碼段使用在其它場合中,因爲它更加高效。
  • Thread-local variables爲每個線程提供變量實例。操作系統對每個進程可用的線程局部變量的數量進行了限制。LabWindows/CVI在實現過程中對線程局部變量進行了加強,程序中的所有線程局部變量只使用一個進程變量。

可以在Utility Library»Multithreading下的LabWindows/CVI庫函數樹狀圖中找到所有的多線程函數。

在LabWindows/CVI的輔助線程中運行代碼

單線程程序中的線程被稱爲主線程。在用戶告訴操作系統開始執行特定的程序時,操作系統將創建主線程。在多線程程序中,除了主線程外,程序還通知操作系統創建其他的線程。這些線程被稱爲輔助線程。主線程和輔助線程的主要區別在於它們開始執行的位置。操作系統從main或者WinMain函數開始執行主線程,而由開發人員來指定輔助線程開始執行的位置。

在典型的LabWindows/CVI多線程程序中,開發者使用主線程來創建、顯示和運行用戶界面,而使用輔助線程來進行其它時間要求嚴格的操作,如數據採集等。LabWindows/CVI提供了兩種在輔助進程中運行代碼的高級機制。這兩種機制是線程池(thread pools)和異步定時器。線程池適合於執行若干次的或者一個循環內執行的任務。而異步定時器適合於定期進行的任務。

使用線程池

爲了使用LabWindows/CVI的線程池在輔助線程中執行代碼,需要調用Utility Library中的CmtScheduleThreadPoolFunction函數。將需要在輔助線程中運行的函數名稱傳遞進來。線程池將這個函數調度到某個線程中執行。根據配置情況和當前的狀態,線程池可能會創建新的線程來執行這個函數、也可能會使用已存在的空閒進程執行函數或者會等待一個活躍的線程變爲空閒然後使用該線程執行預定的函數。傳遞給CmtScheduleThreadPoolFunction的函數被稱爲線程函數。線程池中的線程函數可以選擇任意的名稱,但是必須遵循以下原型:

int CVICALLBACK ThreadFunction (void *functionData);

下面的代碼顯示瞭如何使用CmtScheduleThreadPoolFunction函數在輔助進程中執行一個數據採集的線程。

int CVICALLBACK DataAcqThreadFunction (void *functionData);
int main(int argc, char *argv[])
{
    int panelHandle;
    int functionId;
 
    if (InitCVIRTE (0, argv, 0) == 0)
      return -1; /* out of memory */
    if ((panelHandle = LoadPanel(0, "DAQDisplay.uir", PANEL)) < 0)
      return -1;
    DisplayPanel (panelHandle);

    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, DataAcqThreadFunction, NULL, &functionId);
    RunUserInterface ();
    DiscardPanel (panelHandle);
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK DataAcqThreadFunction (void *functionData)
{
    while (!quit) {
        Acquire(. . .);
        Analyze(. . .);
    }
    return 0;
}

在前面的代碼中,主線程調用了CmtScheduleThreadPoolFunction函數,使線程池創建了一個新的線程來運行DataAcqThreadFunction線程函數。主線程從CmtScheduleThreadPoolFunction函數返回,而無須等待DataAcqThreadFunction函數完成。在輔助線程中的DataAcqThreadFunction函數與主線程中的調用是同時執行的。

CmtScheduleThreadPoolFunction函數的第一個參數表示用於進行函數調度的線程池。LabWindows/CVI的Utility Library中包含了內建的默認線程池。傳遞常數DEFAULT_THREAD_POOL_HANDLE表示用戶希望使用默認的線程池。但是用戶不能對默認線程池的行爲進行自定義。用戶可以調用CmtNewThreadPool函數來創建自定義的線程池。CmtNewThreadPool函數返回一個線程池句柄,這個句柄將作爲第一個參數傳遞給CmtScheduleThreadPoolFunction函數。程序員需要調用CmtDiscardThreadPool函數來釋放由CmtNewThreadPool函數創建的線程池資源。

CmtScheduleThreadPoolFunction函數中的最後一個參數返回一個標識符,用於在後面的函數調用中引用被調度的函數。調用CmtWaitForThreadPoolFunctionCompletion函數使得主線程等待線程池函數結束後再退出。如果主線程在輔助線程完成之前退出,那麼可能會造成輔助線程不能正確地清理分配到的資源。這些輔助線程使用的庫也不會被正確的釋放掉。

使用異步定時器

爲了使用LabWindows/CVI的異步定時器在輔助線程中運行代碼,需要調用Toolslib中的NewAsyncTimer函數。需要向函數傳遞在輔助線程中運行的函數名稱和函數執行的時間間隔。傳遞給NewAsyncTimer的函數被稱爲異步定時器回調函數。異步定時器儀器驅動程序會按照用戶指定的週期調用異步定時器回調函數。異步定時器回調函數的名稱是任意的,但是必須遵循下面的原型:

 int CVICALLBACK FunctionName (int reserved, int timerId, int event, void *callbackData, int eventData1, int eventData2);

由於LabWindows/CVI的異步定時器儀器驅動使用Windows多媒體定時器來實現異步定時器回調函數,所以用戶可指定的最小間隔是隨使用的計算機不同而變化的。如果用戶指定了一個比系統可用的最大分辨率還小的時間間隔,那麼可能會產生不可預知的行爲。不可預知的行爲通常發生在設定的時間間隔小於10ms時。同時,異步定時器儀器驅動使用一個多媒體定時器線程來運行單個程序中註冊的所有異步定時器回調函數。所以,如果用戶希望程序並行地執行多個函數,那麼NI公司推薦使用LabWindows/CVI Utility Library中的線程池函數來代替異步定時器函數。

保護數據

在使用輔助線程的時候,程序員需要解決的一個非常關鍵的問題是數據保護。在多個線程同時進行訪問時,程序需要對全局變量、靜態局部變量和動態分配的變量進行保護。不這樣做會導致間歇性的邏輯錯誤發生,而且很難發現。LabWindows/CVI提供了各種高級機制幫助用戶對受到併發訪問的數據進行保護。保護數據時,一個重要的考慮就是避免死鎖。

如果一個變量被多個線程訪問,那麼它必須被保護,以確保它的值可靠。例如下面一個例子,一個多線程程序在多個線程中對全局整型counter變量的值進行累加。

count = count + 1;

這段代碼按照下列CPU指令順序執行的:

1.將變量值移入處理器的寄存器中

2.增加寄存器中的變量值

3.把寄存器中的變量值寫回count變量

由於操作系統可能在線程運行過程中的任意時刻打斷線程,所以執行這些指令的兩個線程可能按照如下的順序進行(假設count初始值爲5):

線程1:將count變量的值移到寄存器中。(count=5,寄存器=5),然後切換到線程2(count=5,寄存器未知)。

線程2:將count變量的值移到寄存器中(count=5,寄存器=5)。

線程2: 增加寄存器中的值(count=5,寄存器=6)。

線程2: 將寄存器中的值寫回count變量(count=6,寄存器=6),然後切換回線程1.(count=6,寄存器=5)。

線程1: 增加寄存器的值。(count=6,寄存器=6)。

線程1: 將寄存器中的值寫回count變量(count = 6, register = 6)。

由於線程1在增加變量值並將其寫回之前被打斷,所以變量count的值被設爲6而不是7。操作系統爲系統中地每一個線程的寄存器都保存了副本。即使編寫了count++這樣的代碼,用戶還是會遇到相同的問題,因爲處理器會將代碼按照多條指令執行。注意,特定的時序狀態導致了這個錯誤。這就意味着程序可能正確運行1000次,而只有一次故障。經驗告訴我們,有着數據保護不當問題的多線程程序在測試的過程中通常是正確的,但是一到客戶安裝並運行它們時,就會發生錯誤。

需要保護的數據類型

只有程序中的多個線程可以訪問到的數據是需要保護的。全局變量、靜態局部變量和動態分配內存位於通常的內存空間中,程序中的所有線程都可以訪問它們。多個線程對內存空間中存儲的這些類型的數據進行併發訪問時,必須加以保護。函數參數和非靜態局部變量位於堆棧上。操作系統爲每個線程分配獨立的堆棧。因此,每個線程都擁有參數和非靜態局部變量的獨立副本,所以它們不需要爲併發訪問進行保護。下面的代碼顯示了必須爲併發訪問而保護的數據類型。

int globalArray[1000];// Must be protected
static staticGlobalArray[500];// Must be protected
int globalInt;// Must be protected

void foo (int i)// i does NOT need to be protected
{
    int localInt;// Does NOT need to be protected
    int localArray[1000];// Does NOT need to be protected
    int *dynamicallyAllocdArray;// Must be protected
    static int staticLocalArray[1000];// Must be protected

    dynamicallyAllocdArray = malloc (1000 * sizeof (int));
}

如何保護數據

通常說來,在多線程程序中保存數據需要將保存數據的變量與操作系統的線程鎖對象關聯起來。在讀取或者設定變量值的時候,需要首先調用操作系統API函數來獲取操作系統的線程鎖對象。在讀取或設定好變量值後,需要將線程鎖對象釋放掉。在一個特定的時間內,操作系統只允許一個線程獲得特定的線程鎖對象。一旦線程調用操作系統API函數試圖獲取另一個線程正在持有的線程鎖對象,那麼試圖獲取線程鎖對象的線程回在操作系統API獲取函數中等待,直到擁有線程鎖對象的線程將它釋放掉後才返回。試圖獲取其它線程持有的線程鎖對象的線程被稱爲阻塞線程。LabWindows/CVI Utility Library提供了三種保護數據的機制:線程鎖、線程安全變量和線程安全隊列。

線程鎖對操作系統提供的簡單的線程鎖對象進行了封裝。在三種情況下,你可能要使用到線程鎖。如果有一段需要訪問多個共享數據變量的代碼,那麼在運行代碼前需要獲得線程鎖,而在代碼運行後釋放線程鎖。與對每段數據都進行保護相比,這個方法的好處是代碼更爲簡單,而且不容易出錯。缺點是減低了性能,因爲程序中的線程持有線程鎖的時間可能會比實際需要的時間長,這會造成其它線程爲獲得線程鎖而阻塞(等待)的時間變長。使用線程鎖的另一種情況是需要對訪問非線程安全的第三方庫函數時進行保護。例如,有一個非線程安全的DLL用於控制硬件設備而你需要在多個線程中調用這個DLL,那麼可以在線程中調用DLL前創建需要獲得的線程鎖。第三種情況是,你需要使用線程鎖來保護多個程序間共享的資源。共享內存就是這樣一種資源。

線程安全變量技術將操作系統的線程鎖對象和需要保護的數據結合起來。與使用線程鎖來保護一段數據相比,這種方法更爲簡單而且不容易出錯。你必須使用線程安全變量來保護所有類型的數據,包括結構體類型。線程安全變量比線程鎖更不容易出錯,是因爲用戶需要調用Utility Library API函數來訪問數據。而API函數獲取操作系統的線程鎖對象,避免用戶不小心在未獲取OS線程鎖對象的情況下對數據進行訪問的錯誤。線程安全變量技術比線程鎖更簡單,因爲用戶只需要使用一個變量(線程安全變量句柄),而線程鎖技術則需要使用兩個變量(線程鎖句柄和需要保護的數據本身)。

線程安全隊列是一種在線程間進行安全的數組數據傳遞的機制。在程序中有一個線程生成數組數據而另外一個線程對數組數據進行處理時,需要使用線程安全隊列。這類程序的一個例子就是在一個線程中採集數據,而在另一個線程中分析數據或者將數據顯示在LabWindows/CVI的用戶界面上。與一個數組類型的線程安全變量相比,線程安全隊列有着如下的優勢:

  • 線程安全隊列在其內部使用了一種鎖策略,一個線程可以從隊列讀取數據而同時另一個線程向隊列中寫入數據(例如,讀取和寫入線程不會互相阻塞)。
  • 用戶可以爲基於事件的訪問配置線程安全隊列。用戶可以註冊一個讀取回調函數,在隊列中有一定數量的數據可用時,調用這個函數,並且/或者註冊一個寫入回調函數,在隊列中有一定的空間可用時,調用這個函數。
  • 用戶可以對線程安全隊列進行配置,使得在數據增加而空間已滿時,隊列可以自動生長。

線程鎖技術

在程序初始化的時候,調用CmtNewLock函數來爲每個需要保護的數據集合創建線程鎖。這個函數返回一個句柄,用戶可以使用它在後續的函數調用中指定線程鎖。在訪問由鎖保護的數據和代碼前,線程必須調用CmtGetLock函數來獲取線程鎖。在訪問數據後,線程必須調用CmtReleaseLock函數來釋放線程鎖。在同一個線程中,可以多次調用CmtGetLock(不會對後續調用產生阻塞),但是用戶每一次調用CmtGetLock都需要調用一次CmtReleaseLock來釋放。在程序退出時,調用CmtDiscardLock函數來釋放線程鎖資源。下面的代碼演示瞭如何使用LabWindows/CVI Utility Library中的線程鎖來保護全局變量。

int lock;
int count;

int main (int argc, char *argv[])
{
    int functionId;
    CmtNewLock (NULL, 0, &lock);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    CmtGetLock (lock);
    count++;
    CmtReleaseLock (lock);
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    CmtDiscardLock (lock);
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    CmtGetLock(lock);
    count++;
    CmtReleaseLock(lock);
    return 0;
}

線程安全變量

線程安全變量技術將數據和操作系統線程鎖對象結合成爲一個整體。這個方法避免了多線程編程中一個常見的錯誤:程序員在訪問變量時往往忘記首先去獲得鎖。這種方法還使得在函數間傳遞保護的數據變得容易,因爲只需要傳遞線程安全變量句柄而不需要既傳遞線程鎖句柄又要傳遞保護的變量。LabWindows/CVI Utility Library API中包含了幾種用於創建和訪問線程安全變量的函數。利用這些函數可以創建任何類型的線程安全變量。因爲,傳遞到函數中的參數在類型上是通用的,而且不提供類型安全。通常,你不會直接調用LabWindows/CVI Utility Library中的線程安全變量函數。

LabWindows/CVI Utility Library中的頭文件中包含了一些宏,它們提供了配合Utility Library函數使用的類型安全的封裝函數。除了提供類型安全,這些宏還幫助避免了多線程編程中的其它兩個常見錯誤。這些錯誤是在訪問數據後忘記釋放鎖對象,或者是在前面沒有獲取鎖對象時試圖釋放鎖對象。使用DefineThreadSafeScalarVar和DefineThreadSafeArrayVar宏來創建線程安全變量和類型安全的函數供使用和訪問。如果需要從多個源文件中訪問線程安全變量,請在include(.h)文件中使用DeclareThreadSafeScalarVar或者DeclareThreadSafeArrayVar宏來創建訪問函數的聲明。DefineThreadSafeScalarVar (datatype, VarNamemaxGetPointerNestingLevel)宏創建以下訪問函數:

int InitializeVarName (void);
void UninitializeVarName (void);
datatype *GetPointerToVarName (void);
void ReleasePointerToVarName (void);
void SetVarName (datatype val);
datatype GetVarName (void);

注意事項:這些宏使用傳遞進來的第二個參數(在這個例子中爲VarName)作爲標識來爲線程安全變量創建自定義的訪問函數名稱。

注意事項maxGetPointerNestingLevel參數將在“檢測GetPointerToVarName不匹配調用”一節中進行進一步討論。

在第一次訪問線程安全變量前首先調用一次(只在一個線程裏)InitializeVarName函數。在程序中止前調用UninitializeVarName函數。如果需要對變量當前的值進行更改(如,增加一個整數的值),那麼請調用GetPointerToVarName函數,更改變量值,然後調用ReleasePointerToVarName函數。在同一個線程中,可以多次調用GetPointerToVarName函數(在後續的調用中不會發生阻塞),但是必須調用相同次數的ReleasePointerToVarName函數與GetPointerToVarName一一對應。如果在相同的線程中,調用了ReleasePointerToVarName函數,而前面沒有與之相匹配的GetPointerToVarName調用,那麼ReleasePointerToVarName將會報告一個run-time error錯誤。

如果需要對變量值進行設定而不需要考慮其當前值,那麼請調用SetVarName函數。如果需要獲得變量的當前值,請調用GetVarName函數。需要了解的一點是,在GetVarName從內存中讀出變量值後而在其將變量值返回給你前,變量的值是有可能改變的。

下面的代碼顯示瞭如何使用線程安全變量作爲前面例子中提到的計數變量。

DefineThreadSafeScalarVar (int, Count, 0);
int CVICALLBACK ThreadFunction (void *functionData);

int main (int argc, char *argv[])
{
    int functionId;
    int *countPtr;
   
    InitializeCount();
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    countPtr = GetPointerToCount();
    (*countPtr)++;
    ReleasePointerToCount();
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    UninitializeCount();
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    int *countPtr;

    countPtr = GetPointerToCount();
    (*countPtr)++;
    ReleasePointerToCount();
    return 0;
}

使用數組作爲線程安全變量


DefineThreadSafeArrayVar宏與DefineThreadSafeScalarVar宏相似,但是它還需要一個額外的參數來指定數組中元素的個數。同時,與DefineThreadSafeScalarVar不同,DefineThreadSafeArrayVar沒有定義GetVarName和SetVarName函數。下面的聲明定義了有10個整數的線程安全數組。
DefineThreadSafeArrayVar (int, Array, 10, 0);

將多個變量結合成單個線程安全變量

如果有多個彼此相關的變量,那麼必須禁止兩個線程同時對這些變量進行修改。例如,有一個數組和記錄數組中有效數據數目的count變量。如果一個線程需要刪除數組中的數據,那麼在另一個線程訪問數據前,必須對數組和變量count值進行更新。雖然可以使用單個LabWindows/CVI Utility Library線程鎖來對這兩種數據的訪問保護,但是更安全的做法是定義一個結構體,然後使用這個結構體作爲線程安全變量。下面的例子顯示瞭如何使用線程安全變量來安全地向數組中填加一個數據。

typedef struct {
    int data[500];
    int count;
} BufType;

DefineThreadSafeVar(BufType, SafeBuf);

void StoreValue(int val)
{
    BufType *safeBufPtr;
    safeBufPtr = GetPointerToSafeBuf();
    safeBufPtr->data[safeBufPtr->count] = val;
    safeBufPtr->count++;
    ReleasePointerToSafeBuf();
}

檢測對GetPointerToVarName的不匹配調用

可以通過DefineThreadSafeScalarVar和DefineThreadSafeArrayVar的最後一個參數(maxGetPointerNestingLevel),來指定最大數目的嵌套調用。通常可以把這個參數設爲0,這樣GetPointerToVarName在檢測到同一線程中對GetPointerToVarName的兩次連續調用而中間沒有對ReleasePointerToVarName進行調用時,就會報出一個運行錯誤。例如,下面的代碼在第二次執行的時候會報出run-time error的錯誤,因爲它忘記了調用ReleasePointerToCount函數。 

int IncrementCount (void)
{
    int *countPtr;

    countPtr = GetPointerToCount(); /* run-time error on second execution */
    (*countPtr)++;
    /* Missing call to ReleasePointerToCount here */
    return 0;
} 

 

如果代碼中必須對GetPointerToVarName進行嵌套調用時,那麼可將maxGetPointerNestingLevel參數設爲一個大於零的整數。例如,下面的代碼將maxGetPointerNestingLevel參數設定爲1,因此它允許對GetPointerToVarName進行一級嵌套調用。

DefineThreadSafeScalarVar (int, Count, 1);
int Count (void)
{
    int *countPtr;
    countPtr = GetPointerToCount();
    (*countPtr)++;
    DoSomethingElse(); /* calls GetPointerToCount */
    ReleasePointerToCount ();
    return 0;
}
void DoSomethingElse(void)
{
    int *countPtr;
    countPtr = GetPointerToCount(); /* nested call to GetPointerToCount */
    ... /* do something with countPtr */
    ReleasePointerToCount ();
}

 

如果不知道GetPointerToVarName的最大嵌套級別,那麼請傳遞TSV_ALLOW_UNLIMITED_NESTING來禁用對GetPointerToVarName函數的不匹配調用檢查。

線程安全隊列

使用LabWindows/CVI Utility Library的線程安全隊列,可以在線程間安全地傳遞數據。當需要用一個線程來採集數據而用另一個線程來處理數據時,這種技術非常有用。線程安全隊列在其內部處理所有的數據鎖定。通常說來,應用程序中的輔助線程獲取數據,而主線程在數據可用時讀取數據然後分析並/或顯示數據。下面的代碼顯示了線程如何使用線程安全隊列將數據傳遞到另外一個線程。在數據可用時,主線程利用回調函數來讀取數據。

int queue;
int panelHandle;

int main (int argc, char *argv[])
{
    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    if ((panelHandle = LoadPanel(0, "DAQDisplay.uir", PANEL)) < 0)
        return -1;
    /* create queue that holds 1000 doubles and grows if needed */
    CmtNewTSQ(1000, sizeof(double), OPT_TSQ_DYNAMIC_SIZE, &queue);
    CmtInstallTSQCallback (queue, EVENT_TSQ_ITEMS_IN_QUEUE, 500, QueueReadCallback, 0, CmtGetCurrentThreadID(), NULL);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, DataAcqThreadFunction, NULL, NULL);
    DisplayPanel (panelHandle);
    RunUserInterface();
    . . .
    return 0;
}
void CVICALLBACK QueueReadCallback (int queueHandle, unsigned int event, int value, void *callbackData)
{
    double data[500];
    CmtReadTSQData (queue, data, 500, TSQ_INFINITE_TIMEOUT, 0);
}

避免死鎖

當兩個線程同時等待對方持有的線程鎖定對象時,代碼就不能繼續運行了。這種狀況被稱爲死鎖。如果用戶界面線程發生死鎖,那麼它就不能響應用戶的輸入。用戶必須非正常地結束程序。下面的例子解釋了死鎖是如何發生的。

線程1:調用函數來獲取線程鎖A(線程1:無線程鎖,線程2:無線程鎖)。

線程1:從獲取線程鎖的函數返回(線程1:持有線程鎖A,線程2:無線程鎖)。

切換到線程2:(線程1:持有線程鎖A,線程2:無線程鎖)。

線程2:調用函數來獲取線程鎖B(線程1:持有線程鎖A,線程2:無線程鎖)。

線程2:從獲取線程鎖的函數返回(線程1:持有線程鎖A,線程2:持有線程鎖B)。

線程2:調用函數來獲取線程鎖A(線程1:持有線程鎖A,線程2:持有線程鎖B)。

線程2:由於線程1持有線程鎖A而被阻塞(線程1:持有線程鎖A,線程2:持有線程鎖B)。

切換到線程1:調用函數來獲取線程鎖B(線程1:持有線程鎖A,線程2:持有線程鎖B)。

線程1:調用函數來獲取線程鎖B(線程1:持有線程鎖A,線程2:持有線程鎖B)。

線程1:由於線程2持有線程鎖A而被阻塞(線程1:持有線程鎖A,線程2:持有線程鎖B)。

與不對數據進行保護時產生的錯誤相似,由於程序運行的情況不同導致線程切換的時序不同,死鎖錯誤間歇性地發生。例如,如果直到線程1持有線程鎖A和B後才切換到線程2,那麼線程1就可以完成工作而釋放掉這些線程鎖,讓線程2在晚些時候獲取到。就像上面所說的那樣,死鎖現象只有在線程同時獲取線程鎖時纔會發生。所以你可以使用簡單的規則來避免這種死鎖。當需要獲取多個線程鎖對象時,程序中的每個線程都需要按照相同的順序來獲取線程鎖對象。下面的LabWindows/CVI Utility Library函數獲取線程鎖對象,並且返回時並不釋放這些對象。

  • CmtGetLock
  • CmtGetTSQReadPtr
  • CmtGetTSQWritePtr

注意事項:通常說來,不需要直接調用CmtGetTSVPtr函數。它是通過DeclareThreadSafeVariable宏創建的GetPtrToVarName函數調用的。因此,對於調用的GetPtrToVarName函數需要將它作爲線程鎖對象獲取函數來對待,應該注意死鎖保護的問題。
The following Windows SDK functions can acquire thread-locking objects without releasing them before returning. Note: This is not a comprehensive list.

下面的Windows SDK函數可以獲取線程鎖對象但在返回時並不釋放這些對象。注意,這不是完整的列表。

  • EnterCriticalSection
  • CreateMutex
  • CreateSemaphore
  • SignalObjectAndWait
  • WaitForSingleObject
  • MsgWaitForMultipleObjectsEx

監視和控制輔助線程

在把一個函數調度到獨立的線程中運行時,需要對被調度函數的運行狀態進行監視。爲了獲得被調度函數的運行狀態,調用CmtGetThreadPoolFunctionAttribute來獲得ATTR_TP_FUNCTION_EXECUTION_STATUS屬性的值。也可以註冊一個回調函數,線程池調用之後立即運行被調度的函數和/或開始運行後立即由線程池調用。如果需要註冊這樣的回調函數,必須使用CmtScheduleThreadFunctionAdv來對函數進行調度。

通常說來,輔助進程需要在主線程結束程序前完成。如果主線程在輔助線程完成之前結束,那麼輔助線程將不能夠將分配到的資源清理掉。同時,可能導致這些輔助線程所使用的庫函數也不能被正確清除。

可以調用CmtWaitForThreadPoolFunctionCompletion函數來安全地等待輔助線程結束運行,然後允許主線程結束。

在一些例子中,輔助線程函數必須持續完成一些工作直到主線程讓它停止下來。在這類情況下,輔助線程通常在while循環中完成任務。while循環的條件是主線程中設定的整數變量,當主線程需要告知輔助線程停止運行時,將其設爲非零整數。下面的代碼顯示瞭如何使用while循環來控制輔助線程何時結束執行。

 volatile int quit = 0;

int main (int argc, char *argv[])
{
    int functionId;
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    // This would typically be done inside a user interface
    // Quit button callback.
    quit = 1;
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    while (!quit) {
        . . .
    }
    return 0;
}

注意事項:如果使用volatile關鍵字,這段代碼在經過優化的編譯器(如Microsoft Visual C++)後功能是正常的。優化的編譯器確定while循環中的代碼不會修改quit變量的值。因此,作爲優化,編譯器可能只使用quit變量在while循環條件中的初始值。使用volatile關鍵字是告知編譯器另一個線程可能會改變quit變量的值。這樣,編譯器在每次循環運行時都使用更新過後的quit變量值。

有些時候,當主線程進行其他任務的時候需要暫停輔助線程的運行。如果你暫停正在運行操作系統代碼的線程,可能會使得操作系統處於非法狀態。因此,在需要暫停的線程中需要始終調用Windows SDK的SuspendThreadfunction函數。這樣,可以確保線程在運行關鍵代碼時不被暫停。在另一個線程中調用Windows SDK的ResumeThreadfunction是安全的。下面的代碼展示瞭如何使用它們。

volatile int quit = 0;

int main (int argc, char *argv[])
{
    int functionId;
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    // This would typically be done inside a user interface
    // Quit button callback.
    quit = 1;
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    while (!quit) {
        . . .
    }
    return 0;
} 

 進程和線程優先級

在Windows操作系統中,可以指定每個進程和線程工作的相對重要性(被稱爲優先級)。如果給予進程或線程以較高的優先級,那麼它們將獲得比優先級較低的線程更好的優先選擇。這意味着當多個線程需要運行的時候,具有最高優先級的線程首先運行。

Windows將優先級分類。同一進程中的所有線程擁有相同的優先級類別。同一進程中的每個線程都有着與進程優先級類別相關的優先級。可以調用Windows SDK中的SetProcessPriorityClass函數來設定系統中線程的優先級。

NI公司不推薦用戶將線程的優先級設爲實時優先級,除非只在很短時間內這樣做。當進程被設爲實時優先級時,它運行時系統中斷會被阻塞。這會造成鼠標、鍵盤、硬盤及其它至關重要的系統特性不能工作,並很可能造成系統被鎖定。

如果你是使用CmtScheduleThreadFunctionAdv函數來將函數調度到線程池中運行,那麼還可以指定執行所調度函數的線程的優先級。線程池在運行被調度的函數前會改變線程優先級。在函數結束運行後,線程池會將線程優先級恢復到原來的優先級。可使用CmtScheduleThreadFunctionAdv函數來在默認的和自定義的線程池中指定線程的優先級。

 

在創建自定義的LabWindows/CVI Utility Library線程池(調用CmtNewThreadPool函數)時,可以設定池中各線程的默認優先級。

消息處理

每個創建了窗口的線程必須對Windows消息進行處理以避免系統鎖定。用戶界面庫中的RunUserInterfacefunction函數包含了處理LabWindows/CVI用戶界面事件和Windows消息的循環。用戶界面庫中的GetUserEvent和ProcessSystemEventsfunctions函數在每次被調用時對Windows消息進行處理。如果下列情況中的之一被滿足,那麼程序中的每個線程都需要調用GetUserEventor和ProcessSystemEventsregularly函數來處理Windows消息。

  • 線程創建了窗口但沒有調用RunUserInterface函數。
  • 線程創建了窗口並調用了RunUserInterface函數,但是在返回到RunUserInterface循環前需要運行的回調函數佔用了大量時間(多於幾百毫秒)。

但是,在代碼中的某些地方不適合用於處理Windows消息。在LabWindows/CVI的用戶界面線程中調用了GetUserEvent、ProcessSystemEvents或RunUserInterface函數時,線程可以調用一個用戶界面回調函數。如果在用戶界面回調函數中調用這些函數之一,那麼線程將調用另外一個回調函數。除非需要這樣做,否則這種事件將產生不可預知的行爲。

Utility Library中的多線程函數會造成線程在循環中等待,允許你指定是否在等待線程中對消息進行處理。例如,CmtWaitForThreadPoolFunctionCompletion函數中有個Option參數,可以使用它來指定處理Windows消息的等待線程。

有的時候,線程對窗口的創建不是那麼顯而易見的。用戶界面庫函數如LoadPanel、CreatePanel和FileSelectPopup等都創建了用於顯示和丟棄的窗口。這些函數還爲每個調用它們的線程創建了隱藏的窗口。在銷燬可見的窗口時,這個隱藏的窗口並沒有被銷燬。除了這些用戶界面庫函數外,各種其它的LabWindows/CVI庫函數和Windows API函數創建了隱藏的背景窗口。爲了避免系統的鎖定,必須在線程中對使用這兩種方法創建的窗口的Windows消息進行處理。

使用線程局部變量

線程局部變量與全局變量相似,可以在任意線程中對它們進行訪問。但是,全局變量對於所有線程只保存一個值,而線程局部變量爲每個訪問的線程保存一個獨立的值。當程序中需要同時在多個上下文中進行相同的任務,而其中每個上下文都對應一個獨立的線程時,通常需要使用到線程局部變量。例如,你編寫了一個並行的測試程序,其中的每個線程處理一個待測單元,那麼你可能需要使用線程局部變量來保存每個單元的特定信息(例如序列號)。

Windows API提供了用於創建和訪問線程局部變量的機制,但是該機制對每個進程中可用的線程局部變量的數目進行了限定。LabWindows/CVI Utility Library中的線程局部變量函數沒有這種限制。下面的代碼展示瞭如何創建和訪問一個保存了整數的線程局部變量。

volatile int quit = 0;
volatile int suspend = 0;
int main (int argc, char *argv[])
{
    int functionId;
    HANDLE threadHandle;
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
    . . .
    // This would typically be done in response to user input or a
    // change in program state.
    suspend = 1;
    . . .
    CmtGetThreadPoolFunctionAttribute (DEFAULT_THREAD_POOL_HANDLE, functionId, ATTR_TP_FUNCTION_THREAD_HANDLE, &threadHandle);
    ResumeThread (threadHandle);
    . . .
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    while (!quit) {
        if (suspend) {
            SuspendThread (GetCurrentThread ());
            suspend = 0;
        }
        . . .
    }
    return 0;
} 

 

int CVICALLBACK ThreadFunction (void *functionData);
int tlvHandle;
int gSecondaryThreadTlvVal;

int main (int argc, char *argv[])
{
    int functionId;
    int *tlvPtr;

    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    CmtNewThreadLocalVar (sizeof(int), NULL, NULL, NULL, &tlvHandle);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, 0, &functionId);
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    CmtGetThreadLocalVar (tlvHandle, &tlvPtr);
    (*tlvPtr)++;
    // Assert that tlvPtr has been incremented only once in this thread.
    assert (*tlvPtr == gSecondaryThreadTlvVal);
    CmtDiscardThreadLocalVar (tlvHandle);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
    int *tlvPtr;

    CmtGetThreadLocalVar (tlvHandle, &tlvPtr);
    (*tlvPtr)++;
    gSecondaryThreadTlvVal = *tlvPtr;
    return 0;
}

在線程局部變量中存儲動態分配的數據

如果你使用線程局部變量來存儲動態分配到的資源,那麼你需要釋放掉分配的資源的每一個拷貝。也就是說,你需要釋放掉每個線程中分配到的資源拷貝。使用LabWindows/CVI的線程局部變量,你可以指定用於銷燬線程局部變量的回調函數。當你銷燬線程局部變量時,每個訪問過變量的線程都會調用指定的回調函數。下面的代碼展示瞭如何創建和訪問保存了動態分配的字符串的線程局部變量。

int CVICALLBACK ThreadFunction (void *functionData);
void CVICALLBACK StringCreate (char *strToCreate);
void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID);
int tlvHandle;
volatile int quit = 0;
volatile int secondStrCreated = 0;

int main (int argc, char *argv[])
{
    int functionId;

    if (InitCVIRTE (0, argv, 0) == 0)
        return -1; /* out of memory */
    CmtNewThreadLocalVar (sizeof(char *), NULL, StringDiscard, NULL, &tlvHandle);
    CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, "Secondary Thread", &functionId);
    StringCreate ("Main Thread");
    while (!secondStrCreated){
        ProcessSystemEvents ();
        Delay (0.001);
    }
    CmtDiscardThreadLocalVar (tlvHandle);
    quit = 1;
    CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
    return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
   char **sString;

   // Create thread local string variable
   StringCreate ((char *)functionData);

   // Get thread local string and print it
   CmtGetThreadLocalVar (tlvHandle, &sString);
   printf ("Thread local string: %s/n", *sString);

   secondStrCreated = 1;

   while (!quit)
   {
       ProcessSystemEvents ();
       Delay (0.001);
   }

   return 0;
}
void CVICALLBACK StringCreate (char *strToCreate)
{
    char **tlvStringPtr;
    CmtGetThreadLocalVar (tlvHandle, &tlvStringPtr);
    *tlvStringPtr = malloc (strlen (strToCreate) + 1);
    strcpy (*tlvStringPtr, strToCreate);
}
void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID)
{
    char *str = *(char **)threadLocalPtr;
    free (str);
} 

一些分配的資源必須在分配到它們的線程中釋放。這些資源被稱爲擁有線程關聯度。例如,面板必須在創建它的線程中銷燬掉。在調用CmtDiscardThreadLocalVar時,Utility Library在線程中調用被稱爲CmtDiscardThreadLocalVar的線程局部變量銷燬回調函數。Utility Library爲每一個訪問過該變量的線程調用一次銷燬回調函數。它將threadID參數傳遞給銷燬回調函數,這個參數指定了調用銷燬回調函數的線程的ID號。你可以使用這個線程ID來確定是否可以直接釋放掉擁有線程關聯度的資源還是必須在正確的線程中調用Toolslib中的PostDeferredCallToThreadAndWait函數來釋放資源。下面的代碼顯示瞭如何更改前面的例子以在分配字符串的線程中將它們釋放掉。

void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID)
{
    char *str = *(char **)threadLocalPtr;
   
    if (threadID == CmtGetCurrentThreadID ())
        free (str);
    else
        PostDeferredCallToThreadAndWait (free, str, threadID, POST_CALL_WAIT_TIMEOUT_INFINITE);
} 

在獨立線程中運行的回調函數

使用LabWindows/CVI中的一些庫,你可以在系統創建的線程中接收回調函數。因爲這些庫會自動創建執行回調函數的線程,所以你不需要創建線程或者將函數調度到單獨的線程中執行。在程序中,你仍然需要對這些線程和其它線程間共享的數據進行保護。這些回調函數的實現通常被稱爲是異步事件。

LabWindows/CVI的GPIB/GPIB 488.2庫中,可以調用ibnotify來註冊事件發生時GPIB/GPIB 488.2庫調用的回調函數。你可以爲每一個電路板或器件指定一個回調函數。可以爲事件指定調用的回調函數。GPIB/GPIB 488.2庫會創建用於執行回調函數的線程。

在LabWindows/CVI的虛擬儀器軟件構架 (VISA) 庫中,你可以調用viInstallHandler函數來註冊多個事件句柄(回調函數)用於在特定的ViSession 中接收VISA事件(I/O完成、服務請求等等)類型。VISA庫通常創建獨立的線程來執行回調函數。VISA可能會對一個進程中的所有回調函數使用同一個線程,或者對每個ViSession 使用單獨的線程。你需要爲某個指定的事件類型調用viEnableEvent函數以通知VISA庫調用已註冊的事件句柄。

在LabWindows/CVI VXI庫中,每個中斷或回調函數類型都有自己的回調註冊和使能函數。例如,爲了接收NI-VXI中斷,你必須調用SetVXIintHandler和EnableVXIint函數。VXI庫使用自己創建的獨立線程來執行回調函數。對於同一進程中所有的回調函數,VXI都使用相同的線程。

爲線程設定首選的處理器

可以使用平臺SDK中的SetThreadIdealProcessor函數來指定執行某一線程的處理器。這個函數的第一個參數是線程句柄。第二個參數是以零爲索引起始的處理器。可以調用LabWindows/CVI Utility Library中的CmtGetThreadPoolFunctionAttribute函數,使用ATTR_TP_FUNCTION_THREAD_HANDLE屬性來獲取線程池線程的句柄。可以調用LabWindows/CVI Utility Library中的CmtGetNumberOfProcessors函數來通過程序來確定運行該程序的計算機上處理器的數量。

可以使用平臺SDK中的SetProcessAffinityMask函數來指定允許執行你的程序的處理器。可以使用平臺SDK中的SetThreadAffinityMask函數來指定允許執行程序中特定線程的處理器。傳遞到SetThreadAffinityMask中的mask變量必須是傳遞到SetProcessAffinityMask中的mask變量的子集。

這些函數只有程序在裝有Microsoft Windows XP/2000/NT 4.0系統的多處理器計算機上運行纔有效果。Microsoft Windows 9x系列的操作系統不支持多處理器計算機。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章