新聞中心

EEPW首頁 > 測試測量 > 設(shè)計(jì)應(yīng)用 > LabWindows?/CVI中的多線程技術(shù)

LabWindows?/CVI中的多線程技術(shù)

作者: 時(shí)間:2016-12-23 來源:網(wǎng)絡(luò) 收藏

1. 進(jìn)行多線程編程的原因

在程序中使用多線程技術(shù)的原因主要有四個(gè)。最常見的原因是多個(gè)任務(wù)進(jìn)行分割,這些任務(wù)中的一個(gè)或多個(gè)是對時(shí)間要求嚴(yán)格的而且易被其他任務(wù)的運(yùn)行所干涉。例如,進(jìn)行數(shù)據(jù)采集并顯示用戶界面的程序就很適合使用多線程技術(shù)實(shí)現(xiàn)。在這種類型的程序中,數(shù)據(jù)采集是時(shí)間要求嚴(yán)格的任務(wù),它很可能被用戶界面的任務(wù)打斷。在LabWindows/CVI程序中使用單線程方法時(shí),程序員可能需要從數(shù)據(jù)采集緩沖區(qū)讀出數(shù)據(jù)并將它們顯示到用戶界面的曲線上,然后處理事件對用戶界面進(jìn)行更新。當(dāng)用戶在界面上進(jìn)行操作(如在圖表上拖動(dòng)光標(biāo))時(shí),線程將繼續(xù)處理用戶界面事件而不能返回到數(shù)據(jù)采集任務(wù),這將導(dǎo)致數(shù)據(jù)采集緩沖區(qū)的溢出。而在LabWindows/CVI程序中使用多線程技術(shù)時(shí),程序員可以將數(shù)據(jù)采集操作放在一個(gè)線程中,而將用戶界面處理放在另一個(gè)線程中。這樣,在用戶對界面進(jìn)行操作時(shí),操作系統(tǒng)將進(jìn)行線程切換,為數(shù)據(jù)采集線程提供完成任務(wù)所需的時(shí)間。

本文引用地址:http://m.butianyuan.cn/article/201612/333285.htm

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

在程序中使用多線程技術(shù)的第三個(gè)原因是借助多處理器計(jì)算機(jī)來提高性能。計(jì)算機(jī)上的每個(gè)處理器可以都執(zhí)行一個(gè)線程。這樣,在單處理器計(jì)算機(jī)上,操作系統(tǒng)只是使多個(gè)線程看起來是同時(shí)執(zhí)行的,而在多處理器計(jì)算機(jī)上,操作系統(tǒng)才是真正意義上同時(shí)執(zhí)行多個(gè)線程的。例如,進(jìn)行數(shù)據(jù)采集、將數(shù)據(jù)寫入磁盤、分析數(shù)據(jù)并且在用戶界面上顯示分析數(shù)據(jù),這樣的程序很可能通過多線程技術(shù)和多處理器計(jì)算機(jī)運(yùn)行得到性能提升。將數(shù)據(jù)寫到磁盤上和分析用于顯示的數(shù)據(jù)是可以同時(shí)執(zhí)行的任務(wù)。

在程序中使用多線程技術(shù)的第四個(gè)原因是在多個(gè)環(huán)境中同時(shí)執(zhí)行特定的任務(wù)。例如,程序員可以在應(yīng)用程序中利用多線程技術(shù)在測試艙進(jìn)行并行化測試。使用單線程技術(shù),應(yīng)用程序需要?jiǎng)討B(tài)分配空間來保存每個(gè)艙中的測試結(jié)果。應(yīng)用程序需要手動(dòng)維護(hù)每個(gè)記錄及其對應(yīng)的測試艙的關(guān)系。而使用多線程技術(shù),應(yīng)用程序可以創(chuàng)建獨(dú)立的線程來處理每個(gè)測試艙。然后,應(yīng)用程序可以使用線程局部變量為每個(gè)線程創(chuàng)建測試結(jié)果記錄。測試艙與結(jié)果記錄間的關(guān)系是自動(dòng)維護(hù)的,使應(yīng)用程序代碼得以簡化。

2. 選擇合適的操作系統(tǒng)

微軟公司的Windows 9x系列操作系統(tǒng)不支持多處理器計(jì)算機(jī)。所以,你必須在多處理器計(jì)算機(jī)上運(yùn)行Windows Vista/XP/2000/NT 4.0系統(tǒng)來享受多處理器帶來的好處。而且,即使在單處理器計(jì)算機(jī)上,多線程程序在Windows Vista/XP/2000/NT 4.0上的性能也比在Windows 9x上好。這要?dú)w功于Windows Vista/XP/2000/NT 4.0系統(tǒng)有著更為高效的線程切換技術(shù)。但是,這種性能上的差別在多數(shù)多線程程序中體現(xiàn)得并不是十分明顯。

對于程序開發(fā),特別是編寫和調(diào)試多線程程序而言,Windows Vista/XP/2000/NT 4.0系列操作系統(tǒng)比Windows 9x系列更為穩(wěn)定,當(dāng)運(yùn)行操作系統(tǒng)代碼的線程被暫?;蚪K止的時(shí)候,操作系統(tǒng)的一些部分有可能出于不良狀態(tài)中。這種情況使得Windows 9x操作系統(tǒng)崩潰的幾率遠(yuǎn)遠(yuǎn)高于Windows Vista/XP/2000/NT 4.0系統(tǒng)的幾率。所以,NI公司推薦用戶使用運(yùn)行Windows Vista/XP/2000/NT 4.0操作系統(tǒng)的計(jì)算機(jī)來開發(fā)多線程程序。

3. LabWindows/CVI中的多線程技術(shù)簡介

NI LabWindows/CVI軟件自二十世紀(jì)九十年代中期誕生之日起就支持多線程應(yīng)用程序的創(chuàng)建?,F(xiàn)在,隨著多核CPU的廣泛普及,用戶可以使用LabWindows/CVI來充分利用多線程技術(shù)的優(yōu)勢。

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

  • Thread pools幫助用戶將函數(shù)調(diào)度到獨(dú)立的線程中執(zhí)行。Thread pools處理線程緩存來最小化與創(chuàng)建和銷毀線程相關(guān)的開銷。
  • Thread-safe queues對線程間的數(shù)據(jù)傳遞進(jìn)行了抽象。一個(gè)線程可以在另一個(gè)線程向隊(duì)列寫入數(shù)據(jù)的同時(shí),從隊(duì)列中讀取數(shù)據(jù)。
  • Thread-safe variables高效地將臨界代碼段和任意的數(shù)據(jù)類型結(jié)合在一起。用戶可以調(diào)用簡單的函數(shù)來獲取臨界代碼段,設(shè)定變量值,然后釋放臨界代碼段。
  • Thread locks提供了一致的API并在必要時(shí)自動(dòng)選擇合適的機(jī)制來簡化臨界代碼段和互斥量的使用。例如,如果需要在進(jìn)程間共享互斥鎖,或者線程需要在等待鎖的時(shí)候處理消息,LabWindows/CVI會(huì)自動(dòng)使用互斥量。臨界代碼段使用在其它場合中,因?yàn)樗痈咝А?/li>
  • Thread-local variables為每個(gè)線程提供變量實(shí)例。操作系統(tǒng)對每個(gè)進(jìn)程可用的線程局部變量的數(shù)量進(jìn)行了限制。LabWindows/CVI在實(shí)現(xiàn)過程中對線程局部變量進(jìn)行了加強(qiáng),程序中的所有線程局部變量只使用一個(gè)進(jìn)程變量。

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

4. 在LabWindows/CVI的輔助線程中運(yùn)行代碼

單線程程序中的線程被稱為主線程。在用戶告訴操作系統(tǒng)開始執(zhí)行特定的程序時(shí),操作系統(tǒng)將創(chuàng)建主線程。在多線程程序中,除了主線程外,程序還通知操作系統(tǒng)創(chuàng)建其他的線程。這些線程被稱為輔助線程。主線程和輔助線程的主要區(qū)別在于它們開始執(zhí)行的位置。操作系統(tǒng)從main或者WinMain函數(shù)開始執(zhí)行主線程,而由開發(fā)人員來指定輔助線程開始執(zhí)行的位置。

在典型的LabWindows/CVI多線程程序中,開發(fā)者使用主線程來創(chuàng)建、顯示和運(yùn)行用戶界面,而使用輔助線程來進(jìn)行其它時(shí)間要求嚴(yán)格的操作,如數(shù)據(jù)采集等。LabWindows/CVI提供了兩種在輔助進(jìn)程中運(yùn)行代碼的高級機(jī)制。這兩種機(jī)制是線程池(thread pools)和異步定時(shí)器。線程池適合于執(zhí)行若干次的或者一個(gè)循環(huán)內(nèi)執(zhí)行的任務(wù)。而異步定時(shí)器適合于定期進(jìn)行的任務(wù)。

使用線程池

為了使用LabWindows/CVI的線程池在輔助線程中執(zhí)行代碼,需要調(diào)用Utility Library中的CmtScheduleThreadPoolFunction函數(shù)。將需要在輔助線程中運(yùn)行的函數(shù)名稱傳遞進(jìn)來。線程池將這個(gè)函數(shù)調(diào)度到某個(gè)線程中執(zhí)行。根據(jù)配置情況和當(dāng)前的狀態(tài),線程池可能會(huì)創(chuàng)建新的線程來執(zhí)行這個(gè)函數(shù)、也可能會(huì)使用已存在的空閑進(jìn)程執(zhí)行函數(shù)或者會(huì)等待一個(gè)活躍的線程變?yōu)榭臻e然后使用該線程執(zhí)行預(yù)定的函數(shù)。傳遞給CmtScheduleThreadPoolFunction的函數(shù)被稱為線程函數(shù)。線程池中的線程函數(shù)可以選擇任意的名稱,但是必須遵循以下原型:

int CVICALLBACK ThreadFunction (void *functionData);

下面的代碼顯示了如何使用CmtScheduleThreadPoolFunction函數(shù)在輔助進(jìn)程中執(zhí)行一個(gè)數(shù)據(jù)采集的線程。

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;
}

在前面的代碼中,主線程調(diào)用了CmtScheduleThreadPoolFunction函數(shù),使線程池創(chuàng)建了一個(gè)新的線程來運(yùn)行DataAcqThreadFunction線程函數(shù)。主線程從CmtScheduleThreadPoolFunction函數(shù)返回,而無須等待DataAcqThreadFunction函數(shù)完成。在輔助線程中的DataAcqThreadFunction函數(shù)與主線程中的調(diào)用是同時(shí)執(zhí)行的。

CmtScheduleThreadPoolFunction函數(shù)的第一個(gè)參數(shù)表示用于進(jìn)行函數(shù)調(diào)度的線程池。LabWindows/CVI的Utility Library中包含了內(nèi)建的默認(rèn)線程池。傳遞常數(shù)DEFAULT_THREAD_POOL_HANDLE表示用戶希望使用默認(rèn)的線程池。但是用戶不能對默認(rèn)線程池的行為進(jìn)行自定義。用戶可以調(diào)用CmtNewThreadPool函數(shù)來創(chuàng)建自定義的線程池。CmtNewThreadPool函數(shù)返回一個(gè)線程池句柄,這個(gè)句柄將作為第一個(gè)參數(shù)傳遞給CmtScheduleThreadPoolFunction函數(shù)。程序員需要調(diào)用CmtDiscardThreadPool函數(shù)來釋放由CmtNewThreadPool函數(shù)創(chuàng)建的線程池資源。

CmtScheduleThreadPoolFunction函數(shù)中的最后一個(gè)參數(shù)返回一個(gè)標(biāo)識(shí)符,用于在后面的函數(shù)調(diào)用中引用被調(diào)度的函數(shù)。調(diào)用CmtWaitForThreadPoolFunctionCompletion函數(shù)使得主線程等待線程池函數(shù)結(jié)束后再退出。如果主線程在輔助線程完成之前退出,那么可能會(huì)造成輔助線程不能正確地清理分配到的資源。這些輔助線程使用的庫也不會(huì)被正確的釋放掉。

使用異步定時(shí)器

為了使用LabWindows/CVI的異步定時(shí)器在輔助線程中運(yùn)行代碼,需要調(diào)用Toolslib中的NewAsyncTimer函數(shù)。需要向函數(shù)傳遞在輔助線程中運(yùn)行的函數(shù)名稱和函數(shù)執(zhí)行的時(shí)間間隔。傳遞給NewAsyncTimer的函數(shù)被稱為異步定時(shí)器回調(diào)函數(shù)。異步定時(shí)器儀器驅(qū)動(dòng)程序會(huì)按照用戶指定的周期調(diào)用異步定時(shí)器回調(diào)函數(shù)。異步定時(shí)器回調(diào)函數(shù)的名稱是任意的,但是必須遵循下面的原型:

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

由于LabWindows/CVI的異步定時(shí)器儀器驅(qū)動(dòng)使用Windows多媒體定時(shí)器來實(shí)現(xiàn)異步定時(shí)器回調(diào)函數(shù),所以用戶可指定的最小間隔是隨使用的計(jì)算機(jī)不同而變化的。如果用戶指定了一個(gè)比系統(tǒng)可用的最大分辨率還小的時(shí)間間隔,那么可能會(huì)產(chǎn)生不可預(yù)知的行為。不可預(yù)知的行為通常發(fā)生在設(shè)定的時(shí)間間隔小于10ms時(shí)。同時(shí),異步定時(shí)器儀器驅(qū)動(dòng)使用一個(gè)多媒體定時(shí)器線程來運(yùn)行單個(gè)程序中注冊的所有異步定時(shí)器回調(diào)函數(shù)。所以,如果用戶希望程序并行地執(zhí)行多個(gè)函數(shù),那么NI公司推薦使用LabWindows/CVI Utility Library中的線程池函數(shù)來代替異步定時(shí)器函數(shù)。

5. 保護(hù)數(shù)據(jù)

在使用輔助線程的時(shí)候,程序員需要解決的一個(gè)非常關(guān)鍵的問題是數(shù)據(jù)保護(hù)。在多個(gè)線程同時(shí)進(jìn)行訪問時(shí),程序需要對全局變量、靜態(tài)局部變量和動(dòng)態(tài)分配的變量進(jìn)行保護(hù)。不這樣做會(huì)導(dǎo)致間歇性的邏輯錯(cuò)誤發(fā)生,而且很難發(fā)現(xiàn)。LabWindows/CVI提供了各種高級機(jī)制幫助用戶對受到并發(fā)訪問的數(shù)據(jù)進(jìn)行保護(hù)。保護(hù)數(shù)據(jù)時(shí),一個(gè)重要的考慮就是避免死鎖。

如果一個(gè)變量被多個(gè)線程訪問,那么它必須被保護(hù),以確保它的值可靠。例如下面一個(gè)例子,一個(gè)多線程程序在多個(gè)線程中對全局整型counter變量的值進(jìn)行累加。

count = count + 1;

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

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

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

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

由于操作系統(tǒng)可能在線程運(yùn)行過程中的任意時(shí)刻打斷線程,所以執(zhí)行這些指令的兩個(gè)線程可能按照如下的順序進(jìn)行(假設(shè)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的值被設(shè)為6而不是7。操作系統(tǒng)為系統(tǒng)中地每一個(gè)線程的寄存器都保存了副本。即使編寫了count++這樣的代碼,用戶還是會(huì)遇到相同的問題,因?yàn)樘幚砥鲿?huì)將代碼按照多條指令執(zhí)行。注意,特定的時(shí)序狀態(tài)導(dǎo)致了這個(gè)錯(cuò)誤。這就意味著程序可能正確運(yùn)行1000次,而只有一次故障。經(jīng)驗(yàn)告訴我們,有著數(shù)據(jù)保護(hù)不當(dāng)問題的多線程程序在測試的過程中通常是正確的,但是一到客戶安裝并運(yùn)行它們時(shí),就會(huì)發(fā)生錯(cuò)誤。

需要保護(hù)的數(shù)據(jù)類型

只有程序中的多個(gè)線程可以訪問到的數(shù)據(jù)是需要保護(hù)的。全局變量、靜態(tài)局部變量和動(dòng)態(tài)分配內(nèi)存位于通常的內(nèi)存空間中,程序中的所有線程都可以訪問它們。多個(gè)線程對內(nèi)存空間中存儲(chǔ)的這些類型的數(shù)據(jù)進(jìn)行并發(fā)訪問時(shí),必須加以保護(hù)。函數(shù)參數(shù)和非靜態(tài)局部變量位于堆棧上。操作系統(tǒng)為每個(gè)線程分配獨(dú)立的堆棧。因此,每個(gè)線程都擁有參數(shù)和非靜態(tài)局部變量的獨(dú)立副本,所以它們不需要為并發(fā)訪問進(jìn)行保護(hù)。下面的代碼顯示了必須為并發(fā)訪問而保護(hù)的數(shù)據(jù)類型。

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));
}

如何保護(hù)數(shù)據(jù)

通常說來,在多線程程序中保存數(shù)據(jù)需要將保存數(shù)據(jù)的變量與操作系統(tǒng)的線程鎖對象關(guān)聯(lián)起來。在讀取或者設(shè)定變量值的時(shí)候,需要首先調(diào)用操作系統(tǒng)API函數(shù)來獲取操作系統(tǒng)的線程鎖對象。在讀取或設(shè)定好變量值后,需要將線程鎖對象釋放掉。在一個(gè)特定的時(shí)間內(nèi),操作系統(tǒng)只允許一個(gè)線程獲得特定的線程鎖對象。一旦線程調(diào)用操作系統(tǒng)API函數(shù)試圖獲取另一個(gè)線程正在持有的線程鎖對象,那么試圖獲取線程鎖對象的線程回在操作系統(tǒng)API獲取函數(shù)中等待,直到擁有線程鎖對象的線程將它釋放掉后才返回。試圖獲取其它線程持有的線程鎖對象的線程被稱為阻塞線程。LabWindows/CVI Utility Library提供了三種保護(hù)數(shù)據(jù)的機(jī)制:線程鎖、線程安全變量和線程安全隊(duì)列。

線程鎖對操作系統(tǒng)提供的簡單的線程鎖對象進(jìn)行了封裝。在三種情況下,你可能要使用到線程鎖。如果有一段需要訪問多個(gè)共享數(shù)據(jù)變量的代碼,那么在運(yùn)行代碼前需要獲得線程鎖,而在代碼運(yùn)行后釋放線程鎖。與對每段數(shù)據(jù)都進(jìn)行保護(hù)相比,這個(gè)方法的好處是代碼更為簡單,而且不容易出錯(cuò)。缺點(diǎn)是減低了性能,因?yàn)槌绦蛑械木€程持有線程鎖的時(shí)間可能會(huì)比實(shí)際需要的時(shí)間長,這會(huì)造成其它線程為獲得線程鎖而阻塞(等待)的時(shí)間變長。使用線程鎖的另一種情況是需要對訪問非線程安全的第三方庫函數(shù)時(shí)進(jìn)行保護(hù)。例如,有一個(gè)非線程安全的DLL用于控制硬件設(shè)備而你需要在多個(gè)線程中調(diào)用這個(gè)DLL,那么可以在線程中調(diào)用DLL前創(chuàng)建需要獲得的線程鎖。第三種情況是,你需要使用線程鎖來保護(hù)多個(gè)程序間共享的資源。共享內(nèi)存就是這樣一種資源。

線程安全變量技術(shù)將操作系統(tǒng)的線程鎖對象和需要保護(hù)的數(shù)據(jù)結(jié)合起來。與使用線程鎖來保護(hù)一段數(shù)據(jù)相比,這種方法更為簡單而且不容易出錯(cuò)。你必須使用線程安全變量來保護(hù)所有類型的數(shù)據(jù),包括結(jié)構(gòu)體類型。線程安全變量比線程鎖更不容易出錯(cuò),是因?yàn)橛脩粜枰{(diào)用Utility Library API函數(shù)來訪問數(shù)據(jù)。而API函數(shù)獲取操作系統(tǒng)的線程鎖對象,避免用戶不小心在未獲取OS線程鎖對象的情況下對數(shù)據(jù)進(jìn)行訪問的錯(cuò)誤。線程安全變量技術(shù)比線程鎖更簡單,因?yàn)橛脩糁恍枰褂靡粋€(gè)變量(線程安全變量句柄),而線程鎖技術(shù)則需要使用兩個(gè)變量(線程鎖句柄和需要保護(hù)的數(shù)據(jù)本身)。

線程安全隊(duì)列是一種在線程間進(jìn)行安全的數(shù)組數(shù)據(jù)傳遞的機(jī)制。在程序中有一個(gè)線程生成數(shù)組數(shù)據(jù)而另外一個(gè)線程對數(shù)組數(shù)據(jù)進(jìn)行處理時(shí),需要使用線程安全隊(duì)列。這類程序的一個(gè)例子就是在一個(gè)線程中采集數(shù)據(jù),而在另一個(gè)線程中分析數(shù)據(jù)或者將數(shù)據(jù)顯示在LabWindows/CVI的用戶界面上。與一個(gè)數(shù)組類型的線程安全變量相比,線程安全隊(duì)列有著如下的優(yōu)勢:

  • 線程安全隊(duì)列在其內(nèi)部使用了一種鎖策略,一個(gè)線程可以從隊(duì)列讀取數(shù)據(jù)而同時(shí)另一個(gè)線程向隊(duì)列中寫入數(shù)據(jù)(例如,讀取和寫入線程不會(huì)互相阻塞)。
  • 用戶可以為基于事件的訪問配置線程安全隊(duì)列。用戶可以注冊一個(gè)讀取回調(diào)函數(shù),在隊(duì)列中有一定數(shù)量的數(shù)據(jù)可用時(shí),調(diào)用這個(gè)函數(shù),并且/或者注冊一個(gè)寫入回調(diào)函數(shù),在隊(duì)列中有一定的空間可用時(shí),調(diào)用這個(gè)函數(shù)。
  • 用戶可以對線程安全隊(duì)列進(jìn)行配置,使得在數(shù)據(jù)增加而空間已滿時(shí),隊(duì)列可以自動(dòng)生長。

線程鎖技術(shù)

在程序初始化的時(shí)候,調(diào)用CmtNewLock函數(shù)來為每個(gè)需要保護(hù)的數(shù)據(jù)集合創(chuàng)建線程鎖。這個(gè)函數(shù)返回一個(gè)句柄,用戶可以使用它在后續(xù)的函數(shù)調(diào)用中指定線程鎖。在訪問由鎖保護(hù)的數(shù)據(jù)和代碼前,線程必須調(diào)用CmtGetLock函數(shù)來獲取線程鎖。在訪問數(shù)據(jù)后,線程必須調(diào)用CmtReleaseLock函數(shù)來釋放線程鎖。在同一個(gè)線程中,可以多次調(diào)用CmtGetLock(不會(huì)對后續(xù)調(diào)用產(chǎn)生阻塞),但是用戶每一次調(diào)用CmtGetLock都需要調(diào)用一次CmtReleaseLock來釋放。在程序退出時(shí),調(diào)用CmtDiscardLock函數(shù)來釋放線程鎖資源。下面的代碼演示了如何使用LabWindows/CVI Utility Library中的線程鎖來保護(hù)全局變量。

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;
}

線程安全變量

線程安全變量技術(shù)將數(shù)據(jù)和操作系統(tǒng)線程鎖對象結(jié)合成為一個(gè)整體。這個(gè)方法避免了多線程編程中一個(gè)常見的錯(cuò)誤:程序員在訪問變量時(shí)往往忘記首先去獲得鎖。這種方法還使得在函數(shù)間傳遞保護(hù)的數(shù)據(jù)變得容易,因?yàn)橹恍枰獋鬟f線程安全變量句柄而不需要既傳遞線程鎖句柄又要傳遞保護(hù)的變量。LabWindows/CVI Utility Library API中包含了幾種用于創(chuàng)建和訪問線程安全變量的函數(shù)。利用這些函數(shù)可以創(chuàng)建任何類型的線程安全變量。因?yàn)?,傳遞到函數(shù)中的參數(shù)在類型上是通用的,而且不提供類型安全。通常,你不會(huì)直接調(diào)用LabWindows/CVI Utility Library中的線程安全變量函數(shù)。

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

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

注意事項(xiàng):這些宏使用傳遞進(jìn)來的第二個(gè)參數(shù)(在這個(gè)例子中為VarName)作為標(biāo)識(shí)來為線程安全變量創(chuàng)建自定義的訪問函數(shù)名稱。

注意事項(xiàng):maxGetPointerNestingLevel參數(shù)將在“檢測GetPointerToVarName不匹配調(diào)用”一節(jié)中進(jìn)行進(jìn)一步討論。

在第一次訪問線程安全變量前首先調(diào)用一次(只在一個(gè)線程里)InitializeVarName函數(shù)。在程序中止前調(diào)用UninitializeVarName函數(shù)。如果需要對變量當(dāng)前的值進(jìn)行更改(如,增加一個(gè)整數(shù)的值),那么請調(diào)用GetPointerToVarName函數(shù),更改變量值,然后調(diào)用ReleasePointerToVarName函數(shù)。在同一個(gè)線程中,可以多次調(diào)用GetPointerToVarName函數(shù)(在后續(xù)的調(diào)用中不會(huì)發(fā)生阻塞),但是必須調(diào)用相同次數(shù)的ReleasePointerToVarName函數(shù)與GetPointerToVarName一一對應(yīng)。如果在相同的線程中,調(diào)用了ReleasePointerToVarName函數(shù),而前面沒有與之相匹配的GetPointerToVarName調(diào)用,那么ReleasePointerToVarName將會(huì)報(bào)告一個(gè)run-time error錯(cuò)誤。

如果需要對變量值進(jìn)行設(shè)定而不需要考慮其當(dāng)前值,那么請調(diào)用SetVarName函數(shù)。如果需要獲得變量的當(dāng)前值,請調(diào)用GetVarName函數(shù)。需要了解的一點(diǎn)是,在GetVarName從內(nèi)存中讀出變量值后而在其將變量值返回給你前,變量的值是有可能改變的。

下面的代碼顯示了如何使用線程安全變量作為前面例子中提到的計(jì)數(shù)變量。

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;
}

使用數(shù)組作為線程安全變量


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

將多個(gè)變量結(jié)合成單個(gè)線程安全變量

如果有多個(gè)彼此相關(guān)的變量,那么必須禁止兩個(gè)線程同時(shí)對這些變量進(jìn)行修改。例如,有一個(gè)數(shù)組和記錄數(shù)組中有效數(shù)據(jù)數(shù)目的count變量。如果一個(gè)線程需要?jiǎng)h除數(shù)組中的數(shù)據(jù),那么在另一個(gè)線程訪問數(shù)據(jù)前,必須對數(shù)組和變量count值進(jìn)行更新。雖然可以使用單個(gè)LabWindows/CVI Utility Library線程鎖來對這兩種數(shù)據(jù)的訪問保護(hù),但是更安全的做法是定義一個(gè)結(jié)構(gòu)體,然后使用這個(gè)結(jié)構(gòu)體作為線程安全變量。下面的例子顯示了如何使用線程安全變量來安全地向數(shù)組中填加一個(gè)數(shù)據(jù)。

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的不匹配調(diào)用

可以通過DefineThreadSafeScalarVar和DefineThreadSafeArrayVar的最后一個(gè)參數(shù)(maxGetPointerNestingLevel),來指定最大數(shù)目的嵌套調(diào)用。通??梢园堰@個(gè)參數(shù)設(shè)為0,這樣GetPointerToVarName在檢測到同一線程中對GetPointerToVarName的兩次連續(xù)調(diào)用而中間沒有對ReleasePointerToVarName進(jìn)行調(diào)用時(shí),就會(huì)報(bào)出一個(gè)運(yùn)行錯(cuò)誤。例如,下面的代碼在第二次執(zhí)行的時(shí)候會(huì)報(bào)出run-time error的錯(cuò)誤,因?yàn)樗浟苏{(diào)用ReleasePointerToCount函數(shù)。

int IncrementCount (void)
{
int *countPtr;

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

如果代碼中必須對GetPointerToVarName進(jìn)行嵌套調(diào)用時(shí),那么可將maxGetPointerNestingLevel參數(shù)設(shè)為一個(gè)大于零的整數(shù)。例如,下面的代碼將maxGetPointerNestingLevel參數(shù)設(shè)定為1,因此它允許對GetPointerToVarName進(jìn)行一級嵌套調(diào)用。

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函數(shù)的不匹配調(diào)用檢查。

線程安全隊(duì)列

使用LabWindows/CVI Utility Library的線程安全隊(duì)列,可以在線程間安全地傳遞數(shù)據(jù)。當(dāng)需要用一個(gè)線程來采集數(shù)據(jù)而用另一個(gè)線程來處理數(shù)據(jù)時(shí),這種技術(shù)非常有用。線程安全隊(duì)列在其內(nèi)部處理所有的數(shù)據(jù)鎖定。通常說來,應(yīng)用程序中的輔助線程獲取數(shù)據(jù),而主線程在數(shù)據(jù)可用時(shí)讀取數(shù)據(jù)然后分析并/或顯示數(shù)據(jù)。下面的代碼顯示了線程如何使用線程安全隊(duì)列將數(shù)據(jù)傳遞到另外一個(gè)線程。在數(shù)據(jù)可用時(shí),主線程利用回調(diào)函數(shù)來讀取數(shù)據(jù)。

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);
}

6. 避免死鎖

當(dāng)兩個(gè)線程同時(shí)等待對方持有的線程鎖定對象時(shí),代碼就不能繼續(xù)運(yùn)行了。這種狀況被稱為死鎖。如果用戶界面線程發(fā)生死鎖,那么它就不能響應(yīng)用戶的輸入。用戶必須非正常地結(jié)束程序。下面的例子解釋了死鎖是如何發(fā)生的。

線程1:調(diào)用函數(shù)來獲取線程鎖A(線程1:無線程鎖,線程2:無線程鎖)。

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

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

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

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

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

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

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

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

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

與不對數(shù)據(jù)進(jìn)行保護(hù)時(shí)產(chǎn)生的錯(cuò)誤相似,由于程序運(yùn)行的情況不同導(dǎo)致線程切換的時(shí)序不同,死鎖錯(cuò)誤間歇性地發(fā)生。例如,如果直到線程1持有線程鎖A和B后才切換到線程2,那么線程1就可以完成工作而釋放掉這些線程鎖,讓線程2在晚些時(shí)候獲取到。就像上面所說的那樣,死鎖現(xiàn)象只有在線程同時(shí)獲取線程鎖時(shí)才會(huì)發(fā)生。所以你可以使用簡單的規(guī)則來避免這種死鎖。當(dāng)需要獲取多個(gè)線程鎖對象時(shí),程序中的每個(gè)線程都需要按照相同的順序來獲取線程鎖對象。下面的LabWindows/CVI Utility Library函數(shù)獲取線程鎖對象,并且返回時(shí)并不釋放這些對象。

  • CmtGetLock
  • CmtGetTSQReadPtr
  • CmtGetTSQWritePtr

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

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

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

7. 監(jiān)視和控制輔助線程

在把一個(gè)函數(shù)調(diào)度到獨(dú)立的線程中運(yùn)行時(shí),需要對被調(diào)度函數(shù)的運(yùn)行狀態(tài)進(jìn)行監(jiān)視。為了獲得被調(diào)度函數(shù)的運(yùn)行狀態(tài),調(diào)用CmtGetThreadPoolFunctionAttribute來獲得ATTR_TP_FUNCTION_EXECUTION_STATUS屬性的值。也可以注冊一個(gè)回調(diào)函數(shù),線程池調(diào)用之后立即運(yùn)行被調(diào)度的函數(shù)和/或開始運(yùn)行后立即由線程池調(diào)用。如果需要注冊這樣的回調(diào)函數(shù),必須使用CmtScheduleThreadFunctionAdv來對函數(shù)進(jìn)行調(diào)度。

通常說來,輔助進(jìn)程需要在主線程結(jié)束程序前完成。如果主線程在輔助線程完成之前結(jié)束,那么輔助線程將不能夠?qū)⒎峙涞降馁Y源清理掉。同時(shí),可能導(dǎo)致這些輔助線程所使用的庫函數(shù)也不能被正確清除。

可以調(diào)用CmtWaitForThreadPoolFunctionCompletion函數(shù)來安全地等待輔助線程結(jié)束運(yùn)行,然后允許主線程結(jié)束。

在一些例子中,輔助線程函數(shù)必須持續(xù)完成一些工作直到主線程讓它停止下來。在這類情況下,輔助線程通常在while循環(huán)中完成任務(wù)。while循環(huán)的條件是主線程中設(shè)定的整數(shù)變量,當(dāng)主線程需要告知輔助線程停止運(yùn)行時(shí),將其設(shè)為非零整數(shù)。下面的代碼顯示了如何使用while循環(huán)來控制輔助線程何時(shí)結(jié)束執(zhí)行。

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;
}

注意事項(xiàng):如果使用volatile關(guān)鍵字,這段代碼在經(jīng)過優(yōu)化的編譯器(如Microsoft Visual C++)后功能是正常的。優(yōu)化的編譯器確定while循環(huán)中的代碼不會(huì)修改quit變量的值。因此,作為優(yōu)化,編譯器可能只使用quit變量在while循環(huán)條件中的初始值。使用volatile關(guān)鍵字是告知編譯器另一個(gè)線程可能會(huì)改變quit變量的值。這樣,編譯器在每次循環(huán)運(yùn)行時(shí)都使用更新過后的quit變量值。

有些時(shí)候,當(dāng)主線程進(jìn)行其他任務(wù)的時(shí)候需要暫停輔助線程的運(yùn)行。如果你暫停正在運(yùn)行操作系統(tǒng)代碼的線程,可能會(huì)使得操作系統(tǒng)處于非法狀態(tài)。因此,在需要暫停的線程中需要始終調(diào)用Windows SDK的SuspendThreadfunction函數(shù)。這樣,可以確保線程在運(yùn)行關(guān)鍵代碼時(shí)不被暫停。在另一個(gè)線程中調(diào)用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;
}

8. 進(jìn)程和線程優(yōu)先級

在Windows操作系統(tǒng)中,可以指定每個(gè)進(jìn)程和線程工作的相對重要性(被稱為優(yōu)先級)。如果給予進(jìn)程或線程以較高的優(yōu)先級,那么它們將獲得比優(yōu)先級較低的線程更好的優(yōu)先選擇。這意味著當(dāng)多個(gè)線程需要運(yùn)行的時(shí)候,具有最高優(yōu)先級的線程首先運(yùn)行。

Windows將優(yōu)先級分類。同一進(jìn)程中的所有線程擁有相同的優(yōu)先級類別。同一進(jìn)程中的每個(gè)線程都有著與進(jìn)程優(yōu)先級類別相關(guān)的優(yōu)先級。可以調(diào)用Windows SDK中的SetProcessPriorityClass函數(shù)來設(shè)定系統(tǒng)中線程的優(yōu)先級。

NI公司不推薦用戶將線程的優(yōu)先級設(shè)為實(shí)時(shí)優(yōu)先級,除非只在很短時(shí)間內(nèi)這樣做。當(dāng)進(jìn)程被設(shè)為實(shí)時(shí)優(yōu)先級時(shí),它運(yùn)行時(shí)系統(tǒng)中斷會(huì)被阻塞。這會(huì)造成鼠標(biāo)、鍵盤、硬盤及其它至關(guān)重要的系統(tǒng)特性不能工作,并很可能造成系統(tǒng)被鎖定。

如果你是使用CmtScheduleThreadFunctionAdv函數(shù)來將函數(shù)調(diào)度到線程池中運(yùn)行,那么還可以指定執(zhí)行所調(diào)度函數(shù)的線程的優(yōu)先級。線程池在運(yùn)行被調(diào)度的函數(shù)前會(huì)改變線程優(yōu)先級。在函數(shù)結(jié)束運(yùn)行后,線程池會(huì)將線程優(yōu)先級恢復(fù)到原來的優(yōu)先級。可使用CmtScheduleThreadFunctionAdv函數(shù)來在默認(rèn)的和自定義的線程池中指定線程的優(yōu)先級。

在創(chuàng)建自定義的LabWindows/CVI Utility Library線程池(調(diào)用CmtNewThreadPool函數(shù))時(shí),可以設(shè)定池中各線程的默認(rèn)優(yōu)先級。

9. 消息處理

每個(gè)創(chuàng)建了窗口的線程必須對Windows消息進(jìn)行處理以避免系統(tǒng)鎖定。用戶界面庫中的RunUserInterfacefunction函數(shù)包含了處理LabWindows/CVI用戶界面事件和Windows消息的循環(huán)。用戶界面庫中的GetUserEvent和ProcessSystemEventsfunctions函數(shù)在每次被調(diào)用時(shí)對Windows消息進(jìn)行處理。如果下列情況中的之一被滿足,那么程序中的每個(gè)線程都需要調(diào)用GetUserEventor和ProcessSystemEventsregularly函數(shù)來處理Windows消息。

  • 線程創(chuàng)建了窗口但沒有調(diào)用RunUserInterface函數(shù)。
  • 線程創(chuàng)建了窗口并調(diào)用了RunUserInterface函數(shù),但是在返回到RunUserInterface循環(huán)前需要運(yùn)行的回調(diào)函數(shù)占用了大量時(shí)間(多于幾百毫秒)。

但是,在代碼中的某些地方不適合用于處理Windows消息。在LabWindows/CVI的用戶界面線程中調(diào)用了GetUserEvent、ProcessSystemEvents或RunUserInterface函數(shù)時(shí),線程可以調(diào)用一個(gè)用戶界面回調(diào)函數(shù)。如果在用戶界面回調(diào)函數(shù)中調(diào)用這些函數(shù)之一,那么線程將調(diào)用另外一個(gè)回調(diào)函數(shù)。除非需要這樣做,否則這種事件將產(chǎn)生不可預(yù)知的行為。

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

有的時(shí)候,線程對窗口的創(chuàng)建不是那么顯而易見的。用戶界面庫函數(shù)如LoadPanel、CreatePanel和FileSelectPopup等都創(chuàng)建了用于顯示和丟棄的窗口。這些函數(shù)還為每個(gè)調(diào)用它們的線程創(chuàng)建了隱藏的窗口。在銷毀可見的窗口時(shí),這個(gè)隱藏的窗口并沒有被銷毀。除了這些用戶界面庫函數(shù)外,各種其它的LabWindows/CVI庫函數(shù)和Windows API函數(shù)創(chuàng)建了隱藏的背景窗口。為了避免系統(tǒng)的鎖定,必須在線程中對使用這兩種方法創(chuàng)建的窗口的Windows消息進(jìn)行處理。

10. 使用線程局部變量

線程局部變量與全局變量相似,可以在任意線程中對它們進(jìn)行訪問。但是,全局變量對于所有線程只保存一個(gè)值,而線程局部變量為每個(gè)訪問的線程保存一個(gè)獨(dú)立的值。當(dāng)程序中需要同時(shí)在多個(gè)上下文中進(jìn)行相同的任務(wù),而其中每個(gè)上下文都對應(yīng)一個(gè)獨(dú)立的線程時(shí),通常需要使用到線程局部變量。例如,你編寫了一個(gè)并行的測試程序,其中的每個(gè)線程處理一個(gè)待測單元,那么你可能需要使用線程局部變量來保存每個(gè)單元的特定信息(例如序列號(hào))。

Windows API提供了用于創(chuàng)建和訪問線程局部變量的機(jī)制,但是該機(jī)制對每個(gè)進(jìn)程中可用的線程局部變量的數(shù)目進(jìn)行了限定。LabWindows/CVI Utility Library中的線程局部變量函數(shù)沒有這種限制。下面的代碼展示了如何創(chuàng)建和訪問一個(gè)保存了整數(shù)的線程局部變量。

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;
}

11. 在線程局部變量中存儲(chǔ)動(dòng)態(tài)分配的數(shù)據(jù)

如果你使用線程局部變量來存儲(chǔ)動(dòng)態(tài)分配到的資源,那么你需要釋放掉分配的資源的每一個(gè)拷貝。也就是說,你需要釋放掉每個(gè)線程中分配到的資源拷貝。使用LabWindows/CVI的線程局部變量,你可以指定用于銷毀線程局部變量的回調(diào)函數(shù)。當(dāng)你銷毀線程局部變量時(shí),每個(gè)訪問過變量的線程都會(huì)調(diào)用指定的回調(diào)函數(shù)。下面的代碼展示了如何創(chuàng)建和訪問保存了動(dòng)態(tài)分配的字符串的線程局部變量。

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: %sn", *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);
}

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

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);
}

12. 在獨(dú)立線程中運(yùn)行的回調(diào)函數(shù)

使用LabWindows/CVI中的一些庫,你可以在系統(tǒng)創(chuàng)建的線程中接收回調(diào)函數(shù)。因?yàn)檫@些庫會(huì)自動(dòng)創(chuàng)建執(zhí)行回調(diào)函數(shù)的線程,所以你不需要?jiǎng)?chuàng)建線程或者將函數(shù)調(diào)度到單獨(dú)的線程中執(zhí)行。在程序中,你仍然需要對這些線程和其它線程間共享的數(shù)據(jù)進(jìn)行保護(hù)。這些回調(diào)函數(shù)的實(shí)現(xiàn)通常被稱為是異步事件。

LabWindows/CVI的GPIB/GPIB 488.2庫中,可以調(diào)用ibnotify來注冊事件發(fā)生時(shí)GPIB/GPIB 488.2庫調(diào)用的回調(diào)函數(shù)。你可以為每一個(gè)電路板或器件指定一個(gè)回調(diào)函數(shù)。可以為事件指定調(diào)用的回調(diào)函數(shù)。GPIB/GPIB 488.2庫會(huì)創(chuàng)建用于執(zhí)行回調(diào)函數(shù)的線程。

在LabWindows/CVI的虛擬儀器軟件構(gòu)架 (VISA) 庫中,你可以調(diào)用viInstallHandler函數(shù)來注冊多個(gè)事件句柄(回調(diào)函數(shù))用于在特定的ViSession中接收VISA事件(I/O完成、服務(wù)請求等等)類型。VISA庫通常創(chuàng)建獨(dú)立的線程來執(zhí)行回調(diào)函數(shù)。VISA可能會(huì)對一個(gè)進(jìn)程中的所有回調(diào)函數(shù)使用同一個(gè)線程,或者對每個(gè)ViSession使用單獨(dú)的線程。你需要為某個(gè)指定的事件類型調(diào)用viEnableEvent函數(shù)以通知VISA庫調(diào)用已注冊的事件句柄。

在LabWindows/CVI VXI庫中,每個(gè)中斷或回調(diào)函數(shù)類型都有自己的回調(diào)注冊和使能函數(shù)。例如,為了接收NI-VXI中斷,你必須調(diào)用SetVXIintHandler和EnableVXIint函數(shù)。VXI庫使用自己創(chuàng)建的獨(dú)立線程來執(zhí)行回調(diào)函數(shù)。對于同一進(jìn)程中所有的回調(diào)函數(shù),VXI都使用相同的線程。

13. 為線程設(shè)定首選的處理器

可以使用平臺(tái)SDK中的SetThreadIdealProcessor函數(shù)來指定執(zhí)行某一線程的處理器。這個(gè)函數(shù)的第一個(gè)參數(shù)是線程句柄。第二個(gè)參數(shù)是以零為索引起始的處理器??梢哉{(diào)用LabWindows/CVI Utility Library中的CmtGetThreadPoolFunctionAttribute函數(shù),使用ATTR_TP_FUNCTION_THREAD_HANDLE屬性來獲取線程池線程的句柄??梢哉{(diào)用LabWindows/CVI Utility Library中的CmtGetNumberOfProcessors函數(shù)來通過程序來確定運(yùn)行該程序的計(jì)算機(jī)上處理器的數(shù)量。

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



關(guān)鍵詞: LabWindowsCVI多線程技

評論


技術(shù)專區(qū)

關(guān)閉