簡體   English   中英

等待持續的 UI 后台輪詢任務

[英]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 到達時向工作人員提供新消息,如果緩沖區已滿等可能會阻止發送者。無需輪詢。

其他代碼可以使用PostSendAsync向塊發送消息:

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

其他代碼可以使用WriteAsyncTryWrite發送消息,例如:

_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.DelayThread.Sleep更好,因為您不會阻塞池中的線程,並且在等待期間池中的線程將可用於處理其他任務。 然后,您不需要讓您的任務長時間運行。 長時間運行的任務在專用線程中運行,然后Task.Delay沒有意義。

相反,我會推薦一種不同的方法。 只需使用System.Threading.Timer ,讓您的生活變得簡單。 計時器是 kernel 對象,它們將在線程池上運行它們的回調,您不必擔心延遲或睡眠。

TPL Dataflow 庫是此類工作的首選工具。 它允許非常容易地構建高效的生產者-消費者對,以及更復雜的管道,同時提供一套完整的配置選項。 在您的情況下,使用單個ActionBlock就足夠了。

您可能考慮的一個更簡單的解決方案是使用BlockingCollection 它的優點是不需要安裝任何package(因為它是內置的),而且學習起來也容易得多。 除了AddCompleteAddingGetConsumingEnumerable方法之外,您無需了解更多信息。 它還支持取消。 缺點是它是一個阻塞集合,因此它在等待新消息到達時阻塞消費者線程,而在等待內部緩沖區中的可用空間時阻塞生產者線程(僅當您在構造函數中指定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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM