C# 正在等待连续的UI后台轮询任务

C# 正在等待连续的UI后台轮询任务,c#,multithreading,task-parallel-library,C#,Multithreading,Task Parallel Library,我对并行编程C#有点陌生(当我开始我的项目时,我研究了TPL的MSDN示例),如果您能提供以下示例代码,我将不胜感激。 这是几个后台工作任务之一。此特定任务将状态消息推送到日志中 var uiCts = new CancellationTokenSource(); var globalMsgQueue = new ConcurrentQueue<string>(); var backgroundUiTask = new Task( () => { while (!ui

我对并行编程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);
var uiCts=new CancellationTokenSource();
var globalMsgQueue=新的ConcurrentQueue();
var backgroundUiTask=新任务(
() =>
{
而(!uiCts.iscancellationrequest)
{
而(globalMsgQueue.Count>0)
ConsumerMsgQueue();
线程。睡眠(backgroundUiTimeOut);
}
},
uiCts.Token);
//完全在别的地方
backgroundUiTask.Start();
Task.WaitAll(backgroundUiTask);
我在阅读了一些主题后,要求专业人士提供意见,比如

这提示我使用Task.Delay而不是Thread.Sleep作为第一步,并引入TaskCreationOptions.LongRunning


但我想知道我可能还遗漏了什么?正在轮询MsgQueue。是否计数代码气味?更好的版本会依赖于事件吗?

当然
任务。延迟
优于
线程。睡眠
,因为您不会阻塞池上的线程,并且在等待期间池上的线程将可用于处理其他任务。然后,您不需要让任务长期运行。长时间运行的任务在专用线程中运行,然后
Task。延迟
是没有意义的


相反,我将推荐一种不同的方法。使用它,让你的生活变得简单。计时器是在线程池上运行回调的内核对象,您不必担心延迟或睡眠。

首先,没有理由使用
任务。启动
或使用任务构造函数。任务不是线程,它们不会自行运行。它们是一种承诺,即某些事情将在未来完成,可能产生也可能不会产生任何结果。其中一些将在线程池线程上运行。使用
Task.Run
可在需要时一步创建并运行任务

我假设实际的问题是如何创建缓冲的后台工作程序。NET已经提供了可以做到这一点的类

动作块

这个类已经实现了这一点,还有很多其他功能——它允许您指定输入缓冲区有多大,有多少任务将同时处理传入消息,支持取消和异步完成

日志记录块可以如此简单:

_logBlock=new ActionBlock<string>(msg=>File.AppendAllText("myLog.txt",msg));
完成后,我们可以告诉块
Complete()
,并等待它处理任何剩余的消息:

_block.Complete();
await _block.Completion;
频道

一个较新的、较低级别的选项是使用。您可以将通道视为一种异步队列,尽管它们可以用于实现复杂的处理管道。如果ActionBlock是今天编写的,它将在内部使用通道

对于通道,您需要自己提供“工作者”任务。不过,不需要轮询,因为ChannelReader类允许异步读取消息,甚至可以使用
wait-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);
完成后,我们可以在编写器上调用
Complete()
TryComplete()

_logWriter.TryComplete();
线路

.ContinueWith(t=>writer.TryComplete(t.Exception);
需要确保通道关闭,即使发生异常或发出取消令牌信号

一开始这似乎太麻烦了。通道允许我们轻松地运行初始化代码或将状态从一条消息传送到下一条消息。我们可以在循环开始之前打开一个流并使用它,而不是每次调用
文件时重新打开文件

_logWriter.TryWrite(someMessage);
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;
}
公共ChannelWriter登录(字符串路径,CancellationToken=默认) { var channel=channel.CreateUnbounded(); var writer=channel.writer; _=Task.Run(异步()=>{ //*****无法使用ActionBlock执行此操作**** 使用(var writer=File.AppendText(somePath)) { 等待foreach(channel.Reader.ReadAllAsync(令牌)中的var msg) { writer.WriteLine(msg); //或 //等待writer.WriteLineAsync(msg); } } }(t=>writer.TryComplete(t.Exception); 返回作者; }
是这类工作的首选工具。它可以非常轻松地构建高效的生产者-消费者对,也可以构建更复杂的管道,同时提供一套完整的配置选项。在您的情况下,使用一个就足够了

一个更简单的解决方案是使用A。它的优点是不需要安装任何包(因为它是内置的)。,而且它也更容易学习。除了方法

Add
completedadding
getconsumineGenumerable
之外,您不必学习更多的内容。它还支持取消。缺点是它是一个阻塞集合,因此在等待新消息到达时会阻塞使用者线程,而生产者则会执行以下操作:d在等待内部缓冲区中的可用空间时(仅当您在构造函数中指定
boundedCapacity
时)

var uiCts=new CancellationTokenSource();
var globalMsgQueue=new BlockingCollection();
var backgroundUiTask=新任务(()=>
{
foreach(globalMsgQueue.GetConsumingEnumerable(uiCts.Token)中的var项)
{
ConsumeMsgQueueItem(项目);
}
},uiCts.Token);

BlockingCollection
在内部使用一个
ConcurrentQueue
作为缓冲区。

//完全在其他地方…backgroundUiTask.Start()
这是一个bug。任务不是线程,它们代表的内容可能在线程池中的线程上运行,也可能不在线程上运行。很少有任何理由使用任务构造函数,也绝对没有理由在公共代码中使用它。使用Task.run代替。而不是MsgQueue.Count,除此之外,您还可以访问什么
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;
}
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);