这样在 C# 使用 LongRunnigTask 是错的

Task.Factory.StartNew 有一个重载,是支持 TaskCreationOptions.LongRunning 参数来指定 Task 的特征的。但是可能在没有注意的情况下,你就使用了错误的用法。那么本文我们来简单阐述一下这个参数的作用,和使用的注意要点。

这样其实是错误的

有的时候,你可能会这么写:

Task.Factory.StartNew(async () =>
{
    while (true)
    {
        // do something
        await Task.Delay(1000);
    }
}, TaskCreationOptions.LongRunning);

但其实,这是个错误的写法。

为什么需要 LongRunning

我们通常两种情况下会想到使用 TaskCreationOptions.LongRunning 参数:

  1. 你的任务需要长时间运行,比如一个循环,或者一个死循环。用来从队列中取数据,然后处理数据,或者是一些定时任务。

  2. 你的任务需要占用大量的 CPU 资源,是一个很大的循环,比如要遍历一个很大的数组,并做一些处理。

那么这个时候,我们就需要使用 TaskCreationOptions.LongRunning 参数来指定 Task。

因为我们可能学习到了,Task 默认的 Scheduler 是 ThreadPool,而 ThreadPool 的线程是有限的,如果你的任务需要长时间运行,或者是需要占用大量的 CPU 资源,那么就会导致 ThreadPool 的线程不够用。导致线程饥饿,或者是线程池的线程被占用,导致其他的任务无法执行。

于是我们很聪明的就想到了,我们可以使用 TaskCreationOptions.LongRunning 参数来指定 Task,这样就可以避免线程饥饿。

弄巧成拙

但是实际上,开篇的写法并不能达到我们的目的。

我们可以通过以下代码来验证一下:

var task = Task.Factory.StartNew(async () =>
{
    while (true)
    {
        // do something
        await Task.Delay(1000);
    }
}, TaskCreationOptions.LongRunning);

Thread.Sleep(3000);

Console.WriteLine($"Task Status: {task.Status}");
// Task Status: RanToCompletion

我们可以看到,Task 的状态是并非是 Running,而是 RanToCompletion。

也就是说,我们的任务在 3 秒后就已经执行完了,而不是我们想要的长时间运行。

究其原因,是因为我们采用了异步的方式来执行任务。而异步任务的执行,是通过 ThreadPool 来执行的。也就是说,虽然我们使用了 TaskCreationOptions.LongRunning 参数,来想办法指定线程池单独开一个线程,但是实际上在一个 await 之后,我们的任务还是在 ThreadPool 中执行的。

这会导致,我们的任务实际上后续又回到了 ThreadPool 中,而不是我们想要的单独的线程。起不到单独长期运行的作用。

正确的写法

因此,实际上如果想要保持单独的线程持续的运行,我们需要移除异步的方式,改为同步的方式。

var task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        // do something
        Thread.Sleep(1000);
    }
}, TaskCreationOptions.LongRunning);

Thread.Sleep(3000);

Console.WriteLine($"Task Status: {task.Status}");
// Task Status: Running

这样我们就可以看到,Task 的状态是 Running,而不是 RanToCompletion。我们通过 TaskCreationOptions.LongRunning 参数,单独开启的线程就可以一直运行下去。

实际上还有很多考量

要考量 TaskScheduler 的实现

本文采用的是 aspnetcore 的实现,但是在其他的实现中,可能会有不同的实现。你也完全有可能实现一个 await 之后,不回到 ThreadPool 的实现。

LongRunning 也不是就不能用异步

正如开篇提到的第二种场景,如果你的业务是在第一个 await 之前有大量的同步代码,那么此时单独开启一个线程,也是有意义的。

我就是一个死循环,里面也是异步的怎么办

那么你可以考虑让这个 LongRuning 的 Task,不要 await,而是通过 Wait() 来等待。这样就可以避免 LongRunning 的 Task 直接结束。

总结

本文我们简单阐述了 TaskCreationOptions.LongRunning 参数的作用,和使用的注意要点。

参考

  • .NET Task 揭秘(2):Task 的回调执行与 await1

  • Task2

  • TaskCreationOptions3

感谢您的阅读,如果您觉得本文有用,快点击下方赞同按钮👍,让更多的人看到本文。

欢迎加入Q群 610394020,进入腾讯最新功能类Discord功能:QQ 频道。在 QQ 频道中,你可以:

  • 在技术专区发帖求教

  • 在摸鱼频道畅快遨游

  • 在会议频道一同讨论 让我们一同打造和谐社区。

  • 本文作者:newbe36524

  • 本文链接:https://www.newbe.pro/Others/0x026-This-is-the-wrong-way-to-use-LongRunnigTask-in-csharp/

  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!


  1. https://www.cnblogs.com/eventhorizon/p/15912383.html↩

  2. https://threads.whuanle.cn/3.task/↩

  3. https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0&WT.mc_id=DX-MVP-5003606↩

C#中,可以使用`Task.Factory.StartNew`方法来启动一个后台线程,并使用`Invoke`方法在UI线程上更新`Chart`控件,以避免卡顿UI。以下是一个示例代码: ```csharp using System; using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Forms.DataVisualization.Charting; class Program { static void Main(string[] args) { var chart = new Chart(); chart.Series.Add("Series1"); chart.ChartAreas.Add("ChartArea1"); chart.ChartAreas[0].AxisX.Minimum = 0; chart.ChartAreas[0].AxisX.Maximum = 10; chart.ChartAreas[0].AxisY.Minimum = 0; chart.ChartAreas[0].AxisY.Maximum = 100; var form = new Form(); form.Controls.Add(chart); form.Load += (sender, e) => { Task.Factory.StartNew(() => { var random = new Random(); var i = 0; while (true) { var value = random.Next(0, 100); chart.Invoke((Action)(() => { chart.Series["Series1"].Points.AddXY(i, value); if (chart.Series["Series1"].Points.Count > 10) { chart.ChartAreas[0].AxisX.Minimum = chart.Series["Series1"].Points[chart.Series["Series1"].Points.Count - 10].XValue; chart.ChartAreas[0].AxisX.Maximum = chart.Series["Series1"].Points[chart.Series["Series1"].Points.Count - 1].XValue; } })); Task.Delay(1000).Wait(); // 每隔1秒钟执行一次循环 i++; } }, TaskCreationOptions.LongRunning); }; Application.Run(form); } } ``` 在这个示例中,我们创建了一个`Chart`控件,并在`Form`中显示它。在`Form`的`Load`事件处理程序中,我们使用`Task.Factory.StartNew`方法来启动一个后台线程,并在其中执行循环。在循环中,我们使用`Invoke`方法来在UI线程上更新`Chart`控件。我们还检查了`Chart`中点的数量,以便在添加新点后自动调整`Chart`的`AxisX`范围,以保持最近的10个点可见。最后,我们使用`Application.Run`方法来运行`Form`并显示`Chart`控件。 请注意,在使用`Invoke`方法时,必须将更新UI的代码封装在一个`Action`委托中,以便它可以在UI线程上执行。此外,我们使用了`TaskCreationOptions.LongRunning`选项来告诉.NET运行时,该任务将运行较长时间,并可能在后台线程上使用较多的资源。最后,我们在`Task.Delay`后面调用了`.Wait()`方法来确保在执行下一次循环之前等待1秒钟,以避免在后台线程上卡顿UI。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值