王越,夏京川,祁正興,陸小龍
(四川大學(xué) 制造科學(xué)與工程學(xué)院,四川 成都 610065)
測控系統(tǒng)軟件設(shè)計質(zhì)量直接影響著測控系統(tǒng)穩(wěn)定運行。其中,多個方面的因素決定了并發(fā)編程在現(xiàn)代測控系統(tǒng)軟件中的必要性:1) 數(shù)據(jù)采集量大,并需要進行復(fù)雜算法處理,要求系統(tǒng)具有一定的可靠性、穩(wěn)定性和實時性;2) 系統(tǒng)復(fù)雜度與集成度的不斷提高增加了用戶與應(yīng)用程序交互的頻率,要求軟件具有良好的響應(yīng)能力;3) 現(xiàn)代測控系統(tǒng)網(wǎng)絡(luò)化發(fā)展趨勢以及云計算的需求,軟件中信息的傳輸速率需要進一步提高[1-2]。本文描述了利用Windows系統(tǒng).Net Framework中的并發(fā)技術(shù)來設(shè)計測控系統(tǒng)軟件,增強系統(tǒng)的整體性能。
Windows系統(tǒng)中并發(fā)編程通常采用多線程模式,線程是運行在一個單一進程上下文中的邏輯流,由內(nèi)核進行調(diào)度,其共享所在進程的整個虛擬地址空間。每一個線程都包括線程內(nèi)核對象、線程環(huán)境塊、用戶模式棧、內(nèi)核模式棧等內(nèi)容[3]。進程在創(chuàng)建或終止任一線程時,會調(diào)用進程中加載的所有非托管DLL(dynamic linking library,動態(tài)鏈接庫)的DllMain方法。
使用Thread編程模型,開發(fā)者能夠直接創(chuàng)建與操縱操作系統(tǒng)級別的線程[1]。然而,線程的每一次創(chuàng)建、終止過程都會產(chǎn)生巨大的開銷;同時,一個CPU任一時刻只能運行一個線程,過多的線程會導(dǎo)致頻繁的線程上下文切換,造成更為嚴重的性能損耗。通常程序開發(fā)者無法了解計算機運行時特性,一味地增加線程必然會導(dǎo)致計算機資源的極大浪費。
Thread Pool模型則將線程管理工作移交到了操作系統(tǒng)本身,采取復(fù)用的方式(工作項在線程中排隊,線程池線程執(zhí)行完當(dāng)前工作項后,繼續(xù)執(zhí)行下一個工作項)來確保計算機中線程數(shù)量維持在一個合理的值。Thread Pool使用特定的探索方式來決定是否添加新的線程,以及保證線程池中最大線程數(shù)量。在較新的Net版本(4.0及以上)中,線程數(shù)量最大值由可利用內(nèi)存空間決定,這能充分利用現(xiàn)代計算機的多核CPU硬件資源[4]。
TPL(task parallel library,任務(wù)并行庫)是基于Thread Pool工作原理提出的一種新的并發(fā)編程模型,其提供了一個規(guī)范、更易理解的并發(fā)編程結(jié)構(gòu)。
Task是一種抽象概念,代表并發(fā)工作的基本單元,即是指被創(chuàng)建活動運行的同時,主(創(chuàng)建)線程仍然在持續(xù)執(zhí)行[5]。Task可指定為工作線程或I/O線程,由Thread Pool進行調(diào)度,內(nèi)置為后臺線程執(zhí)行并且不可改變。通過配置TaskCreationOptions與TaskScheduler選項可調(diào)整運行時Task參數(shù),以適應(yīng)不同的工作需求。
異步的核心理念是異步操作,啟動了的操作將會在一段時間后完成,該操作不會阻塞原來的線程,其結(jié)束時會通知原有線程操作已完成[6]。編譯器后臺采用FSM(有限狀態(tài)機)原理,實現(xiàn)future模式或回調(diào)機制,以避免產(chǎn)生不必要的線程。實現(xiàn)異步的通用方式為創(chuàng)建一個Task并指定ContinueWith()方法,通過配置同步上下文(SynchronizationContext),可將完成后的延續(xù)任務(wù)(continuation)調(diào)回到原有線程中執(zhí)行。現(xiàn)代的異步.Net程序則主要使用兩個關(guān)鍵字:async和await(.Net 4.5及以上)以簡化異步操作的調(diào)用。
同一進程的多個線程將共享絕大多數(shù)運行資源與數(shù)據(jù)。當(dāng)多個線程想同時獲得同一個資源時,將產(chǎn)生競爭;當(dāng)多段并發(fā)代碼想同時訪問一個數(shù)據(jù),并且至少有一段代碼在修改數(shù)據(jù)時,數(shù)據(jù)將變得不安全或產(chǎn)生錯誤。在以上兩種情況下,都需要使用同步技術(shù)來協(xié)調(diào)多線程的執(zhí)行,保證線程間的調(diào)用安全。
Windows提供了用戶與內(nèi)核模式構(gòu)造線程同步機制。用戶模式使用特殊CPU指令來協(xié)調(diào)線程,以確保數(shù)據(jù)變量呈現(xiàn)為原子性;而內(nèi)核模式由Windows自身提供,可根據(jù)線程運行情況切換至等待、就緒、休眠等狀態(tài),有利于系統(tǒng)監(jiān)測與整體管理[2]。為在實現(xiàn)系統(tǒng)可進行整體管理的同時最大化優(yōu)化線程同步性能,更合理的方式是混合使用用戶模式與內(nèi)核模式,其實現(xiàn)形式包括Monitor、Lock、ManualResetEventSlim、ReaderWriterLockSlim等。
當(dāng)程序并發(fā)運行時,用戶常常因為某些原因中止并發(fā)線程。早期技術(shù)如Abort()方法將強制退出線程,可能使進程中共享資源處于無效狀態(tài)而導(dǎo)致后續(xù)的錯誤,Interrupt()方法僅在線程處于WaitSleepJoin狀態(tài)下完成,該狀態(tài)由于線程原因可能永遠無法達到從而造成死鎖。在.Net 4.0中,提供CancellationTokenSource與CancellationToken來處理取消問題。其實現(xiàn)機制主要為協(xié)調(diào)取消請求與并發(fā)操作,在操作內(nèi)部定義可安全取消位置,僅當(dāng)程序運行在安全位置(通常采用輪詢方式)時拋出異常并取消線程。
由上述可見,多線程的主要優(yōu)勢在于構(gòu)建性能良好、可伸縮、響應(yīng)靈敏的應(yīng)用程序。其核心理念在于將線程與任務(wù)分開,線程由框架根據(jù)操作系統(tǒng)特性創(chuàng)建并維護,開發(fā)者僅需要創(chuàng)建可劃分為小塊的任務(wù),框架將自動地將任務(wù)調(diào)度到合適的線程并執(zhí)行。
為實現(xiàn)以上目標(biāo),關(guān)鍵在于兩點:1) 不手動創(chuàng)建額外的線程;2) 已創(chuàng)建的線程不應(yīng)該被阻塞以便更好地復(fù)用。
在某測控系統(tǒng)設(shè)計中,程序應(yīng)用劃分為多個模塊,其硬件結(jié)構(gòu)與軟件框架如圖1所示。
圖1 系統(tǒng)體系結(jié)構(gòu)圖
圖中每個實線矩形框表示一個軟件模塊和功能。為達到最佳應(yīng)用效果,各軟件模塊通常需要同時工作,程序應(yīng)運行在多線程模式。本文以下內(nèi)容將著重介紹各個模塊并發(fā)狀態(tài)下的應(yīng)用技術(shù)與實現(xiàn)方法。
數(shù)據(jù)對于測控系統(tǒng)至關(guān)重要。通常而言,被測物理量(信號)通過傳感器采集,并在采集設(shè)備中對信號進行處理(硬件濾波、數(shù)模轉(zhuǎn)換等);計算機通過軟件驅(qū)動采集設(shè)備,獲取信號進行再次加工(如軟件濾波、數(shù)值轉(zhuǎn)換),得到控制信號后輸出到執(zhí)行機構(gòu)或視圖界面,完成對系統(tǒng)的控制或展現(xiàn)給用戶。
為保證控制系統(tǒng)的有效可靠,必然要求采集模塊具有較高的實時性和穩(wěn)定性。通過任務(wù)和異步,能夠保證計算機在調(diào)度采集設(shè)備的同時,并行執(zhí)行其他的業(yè)務(wù)邏輯。
首先,對采集設(shè)備API進行包裝,如:
public class SomeDevice {
SemaphoreSlim slim=new SemaphoreSlim(1,1);
…
public async Task
return await Task.Run( async () => {
...
await slim.WaitAsync();
...//讀取數(shù)據(jù)
slim.Release();
return datas;//返回數(shù)據(jù)
});
}
}
代碼中使用了SemaphoreSlim類型的一個信號量對象來實現(xiàn)資源同步,這是目前.Net平臺中唯一支持異步訪問的同步構(gòu)造。當(dāng)SemaphoreSlim最大計數(shù)為1時,可對其保護的資源進行互斥訪問。在應(yīng)用程序的其他業(yè)務(wù)中,使用如下方式來得到采集數(shù)據(jù):
vardevice = new SomeDevice();
var datas = await device.GetAcquisitionAsync(null);
當(dāng)程序主線程執(zhí)行方法遇到await關(guān)鍵字后,會啟動另一個線程池線程執(zhí)行await后所帶的異步方法,而主線程無需阻塞,直接返回以執(zhí)行其它工作。當(dāng)異步方法結(jié)束,若之后還有操作,將會被包裝為一個Task返回至主線程的任務(wù)隊列,并通知主線程盡快執(zhí)行,主線程會在合適的時間對后續(xù)Task進行調(diào)度。
目前大多數(shù)的計算機與IO設(shè)備都能夠通過DMA(direct memory access,直接內(nèi)存存取)的方式來進行數(shù)據(jù)傳輸,此時不需要消耗CPU資源,硬件會自動進行讀寫數(shù)據(jù)并與內(nèi)存交換[7]。然而,若使用同步I/O,CPU通知硬件設(shè)備后,線程將會阻塞,直到硬件設(shè)備完成I/O操作,Windows再喚醒線程調(diào)度給CPU繼續(xù)執(zhí)行。在這個過程中線程處于休眠狀態(tài),若程序想完成其他工作,需要創(chuàng)建新的線程。當(dāng)休眠狀態(tài)被喚醒后,因為加入了新線程,運行線程數(shù)量可能已超過最合適的值,計算機將會執(zhí)行的線程時間片切換,造成巨大的性能損耗。
使用異步操作可避免以上情況。在.Net常用的IO操作,如文件讀取、寫入,Stream的應(yīng)用,數(shù)據(jù)庫Command的執(zhí)行中,有些已經(jīng)提供了異步方法,有些則提供了早期版本的BeginXxx/EndXxx方法與IAsyncResult接口,需要使用以下方式進行包裝:
var io = new IODevice(...);
await Task.Factory.FromAsync(io.BeginXxx, io.EndXxx);
對于未提供異步方法的IO操作,可手動進行包裝。以下代碼提供了序列化操作的異步接口與序列化到xml文件的實現(xiàn):
public interface IAsyncSerializer{ Task WriteAsync
public class XmlSerializer : IAsyncSerializer{
...
public async Task WriteAsync
return await Task.Run(()=>{
using(var stream = File.Open(path, FileMode.OpenOrCreate)){
var serializer = new XmlSerializer(obj.GetType());
serializer.Serialize(stream, obj);
}
});
}
}
隨著測控系統(tǒng)軟件綜合化程度的提高,與操作者的交互頻率也將不可避免地增加。通常GUI框架(如MFC、WinForm、WPF等)的控件與創(chuàng)建者線程具有“線程相關(guān)性”,其他線程永遠無法直接訪問用戶界面控件,這為基于多線程的GUI應(yīng)用程序帶來了困擾。通過異步的方式能夠解決該問題,并進一步提高程序的響應(yīng)能力。
可直接將異步操作注冊到控件(如Button)的事件中,如:
CancellationTokenSource ctSource;
private async void ButtonStartClick(object sender, EventArgs e){
var model = new Experiment();
this.textBox.Text = await model.Execute(ctSource.Token);
}
private void ButtonStopClick(object sender, EventArgs e){
ctSource.Cancel();
}
在代碼中,Execute是一個返回Task
并且異步調(diào)用在Execute執(zhí)行的同時,UI線程能夠響應(yīng)用戶的其他操作,不至于卡頓。此外,代碼在UI線程中聲明了CancellationTokenSource對象,它的Token屬性將被傳到Execute方法中。若Execute是一個費時的操作,用戶可操作CancellationTokenSource(如這里的點擊Stop按鈕)取消Execute的執(zhí)行。任務(wù)將在一個安全的、可控制的位置停止,這是開發(fā)者可以操控的。此時Execute大致的結(jié)構(gòu)將如下:
public async Task
return await Task.Run(()=>{
int totalTimes=10000;
for(int times = 0;times ...//其他操作 token.ThrowIfCancellationRequest();//拋出異常以停止 } return "Completed"; },token); } 測控系統(tǒng)通常包含大量的數(shù)據(jù)處理,包括濾波、數(shù)值轉(zhuǎn)換、計算結(jié)果、圖形圖像處理等。這些過程通常為CPU密集型操作,創(chuàng)建多于CPU內(nèi)核數(shù)的線程將嚴重影響計算機性能。使用TPL中Parallel的For、ForEach方法,開發(fā)者將復(fù)雜的線程調(diào)度過程委托給操作系統(tǒng)執(zhí)行,能夠更關(guān)注于應(yīng)用邏輯并提升并行性能。 IEnumerable object lockObject = new object(); double result = 0; Parallel.ForEach(data, () => 0, (item, state, returnValue) => ExecuteCompute(item), returnValue =>{Monitor.Enter(lockObject); result += returnValue; Monitor.Exit(lockObject); }); 以上代碼對集合datas中的每一項都進行ExecuteCompute運算,再將運算結(jié)果統(tǒng)計至局部變量result中。其中,每一小塊都被劃分為一個任務(wù),被動態(tài)分配到空閑線程的任務(wù)隊列中以并行方式執(zhí)行,而操作系統(tǒng)則會根據(jù)硬件特性委派給不同的CPU,并根據(jù)當(dāng)前任務(wù)與線程數(shù)量決定是否增加新的線程。 任何一項任務(wù)完成后,將會繼續(xù)執(zhí)行求和操作,涉及到的result是一個共享變量,需要進行同步。此處使用Monitor構(gòu)造能得到較好性能。 本文利用.Net Framework下的現(xiàn)代并發(fā)編程API,實現(xiàn)了某測控系統(tǒng)中軟件重要模塊的編寫。其中,異步方式有利于構(gòu)建IO設(shè)備模塊和實現(xiàn)反應(yīng)迅速的GUI交互界面;而并行則適用于需要CPU執(zhí)行的計算密集性操作,以保證充分利用當(dāng)代多核多處理器的硬件性能。以上技術(shù)的實現(xiàn)為構(gòu)造一個性能良好、可伸縮、響應(yīng)靈敏的應(yīng)用程序奠定了堅實的基礎(chǔ)。2.5 數(shù)據(jù)處理
3 結(jié)語