![](/img/trans.png)
[英]Running a background task and waiting for its result without blocking the UI thread
[英]Waiting on a continuous UI background polling task
我對並行編程 C# 有點陌生(當我開始我的項目時,我研究了 TPL 的 MSDN 示例)並且希望對以下示例代碼提供一些輸入。 它是幾個后台工作任務之一。 此特定任務將狀態消息推送到日志。
var uiCts = new CancellationTokenSource();
var globalMsgQueue = new ConcurrentQueue<string>();
var backgroundUiTask = new Task(
() =>
{
while (!uiCts.IsCancellationRequested)
{
while (globalMsgQueue.Count > 0)
ConsumeMsgQueue();
Thread.Sleep(backgroundUiTimeOut);
}
},
uiCts.Token);
// Somewhere else entirely
backgroundUiTask.Start();
Task.WaitAll(backgroundUiTask);
在閱讀了諸如使用 Thread.Sleep 等待的替代方案、 使用 Thread.Sleep() 總是不好的幾個主題之后,我要求提供專業意見? , 什么時候用Task.Delay,什么時候用Thread.Sleep? , 使用 Tasks 連續輪詢
這促使我首先使用 Task.Delay 而不是 Thread.Sleep 並引入 TaskCreationOptions.LongRunning。
但我想知道我可能會遺漏哪些其他警告? 輪詢 MsgQueue.Count 是否有代碼異味? 一個更好的版本會依賴於一個事件嗎?
首先,沒有理由使用Task.Start
或使用 Task 構造函數。 任務不是線程,它們不會自行運行。 它們是promise ,將來會完成某些事情,可能會或可能不會產生任何結果。 其中一些將在線程池線程上運行。 需要時,使用Task.Run
在一個步驟中創建和運行任務。
我認為實際的問題是如何創建緩沖的后台工作人員。 .NET 已經提供了可以做到這一點的類。
動作塊<T>
ActionBlock class 已經實現了這一點以及更多功能 - 它允許您指定輸入緩沖區的大小、同時處理傳入消息的任務數量、支持取消和異步完成。
一個日志塊可以像這樣簡單:
_logBlock=new ActionBlock<string>(msg=>File.AppendAllText("myLog.txt",msg));
ActionBlock class 本身負責緩沖輸入,在工作人員 function 到達時向工作人員提供新消息,如果緩沖區已滿等可能會阻止發送者。無需輪詢。
其他代碼可以使用Post
或SendAsync
向塊發送消息:
_block.Post("some message");
完成后,我們可以告訴塊Complete()
並等待它處理任何剩余的消息:
_block.Complete();
await _block.Completion;
頻道
一個更新的、較低級別的選項是使用Channels 。 您可以將通道視為一種異步隊列,盡管它們可用於實現復雜的處理管道。 如果 ActionBlock 是今天編寫的,它將在內部使用 Channels。
使用頻道,您需要自己提供“工人”任務。 不過不需要輪詢,因為 ChannelReader class 允許您異步讀取消息,甚至可以使用await foreach
。
writer 方法可能如下所示:
public ChannelWriter<string> LogIt(string path,CancellationToken token=default)
{
var channel=Channel.CreateUnbounded<string>();
var writer=channel.Writer;
_=Task.Run(async ()=>{
await foreach(var msg in channel.Reader.ReadAllAsync(token))
{
File.AppendAllText(path,msg);
}
},token).ContinueWith(t=>writer.TryComplete(t.Exception);
return writer;
}
....
_logWriter=LogIt(somePath);
其他代碼可以使用WriteAsync
或TryWrite
發送消息,例如:
_logWriter.TryWrite(someMessage);
完成后,我們可以在 writer 上調用Complete()
或TryComplete()
:
_logWriter.TryComplete();
線
.ContinueWith(t=>writer.TryComplete(t.Exception);
即使發生異常或發出取消令牌信號,也需要確保通道關閉。
起初這可能看起來太麻煩了。 通道使我們能夠輕松地運行初始化代碼或將 state 從一條消息傳遞到另一條消息。 我們可以在循環開始之前打開一個 stream 並使用它而不是每次調用File.AppendAllText
時都重新打開文件,例如:
public ChannelWriter<string> LogIt(string path,CancellationToken token=default)
{
var channel=Channel.CreateUnbounded<string>();
var writer=channel.Writer;
_=Task.Run(async ()=>{
//***** Can't do this with an ActionBlock ****
using(var writer=File.AppendText(somePath))
{
await foreach(var msg in channel.Reader.ReadAllAsync(token))
{
writer.WriteLine(msg);
//Or
//await writer.WriteLineAsync(msg);
}
}
},token).ContinueWith(t=>writer.TryComplete(t.Exception);
return writer;
}
絕對Task.Delay
比Thread.Sleep
更好,因為您不會阻塞池中的線程,並且在等待期間池中的線程將可用於處理其他任務。 然后,您不需要讓您的任務長時間運行。 長時間運行的任務在專用線程中運行,然后Task.Delay
沒有意義。
相反,我會推薦一種不同的方法。 只需使用System.Threading.Timer ,讓您的生活變得簡單。 計時器是 kernel 對象,它們將在線程池上運行它們的回調,您不必擔心延遲或睡眠。
TPL Dataflow 庫是此類工作的首選工具。 它允許非常容易地構建高效的生產者-消費者對,以及更復雜的管道,同時提供一套完整的配置選項。 在您的情況下,使用單個ActionBlock
就足夠了。
您可能考慮的一個更簡單的解決方案是使用BlockingCollection
。 它的優點是不需要安裝任何package(因為它是內置的),而且學習起來也容易得多。 除了Add
、 CompleteAdding
和GetConsumingEnumerable
方法之外,您無需了解更多信息。 它還支持取消。 缺點是它是一個阻塞集合,因此它在等待新消息到達時阻塞消費者線程,而在等待內部緩沖區中的可用空間時阻塞生產者線程(僅當您在構造函數中指定boundedCapacity
時)。
var uiCts = new CancellationTokenSource();
var globalMsgQueue = new BlockingCollection<string>();
var backgroundUiTask = new Task(() =>
{
foreach (var item in globalMsgQueue.GetConsumingEnumerable(uiCts.Token))
{
ConsumeMsgQueueItem(item);
}
}, uiCts.Token);
BlockingCollection
在內部使用ConcurrentQueue
作為緩沖區。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.