乐趣区

关于c#:实现常驻任务除了避免昙花线程还需要避免重返线程池

后面咱们应用简略的例子演示了 Task 和 Thread 的两种制作昙花线程的形式。那么除了防止昙花线程,在实现常驻工作的时候,还须要防止重返线程池。本文将介绍如何防止重返线程池。

常驻工作

常驻工作十分常见,比方:

  1. 咱们正在编写一个日志文件库,咱们心愿在后盾一直的将日志写入文件,尽可能不影响业务线程的执行。因而,须要一个写文件的常驻工作。
  2. 咱们对接了一个近程 TCP 服务,对方要求咱们每隔一段时间发送一个心跳包,以放弃连贯。因而,须要一个发送心跳包的常驻工作。
  3. 咱们编写了一个简略的内存缓存,通过一个后台任务来定期清理过期的缓存。因而,须要一个清理缓存的常驻工作。

相似的场景还有很多。因而,咱们须要一个可能实现常驻工作的办法。

而实现常驻工作的次要要点是:

  1. 常驻工作必须防止影响业务线程的执行,因而须要在后盾执行。
  2. 常驻工作不能被业务线程影响,无论以后业务如许忙碌,常驻工作都必须可能失常执行。否则会呈现日志不落盘,心跳包不发送,缓存不清理等问题。

实现常驻工作的伎俩有很多。本文将围绕如何应用常驻繁多线程来实现常驻工作。

所谓常驻繁多线程,就是指始终应用一个线程来执行常驻工作。从而达到:

  1. 防止频繁的创立和销毁线程,从而防止频繁的线程切换。
  2. 更容易的解决背压问题。
  3. 更容易的解决线程平安问题。

评测主体

咱们将采纳如下状况来评测如何编写常驻工作的正确性。

private int _count = 0;

private void ProcessTest(Action<CancellationToken> action, [CallerMemberName] string methodName = "")
{var cts = new CancellationTokenSource();
    // 启动常驻线程
    action.Invoke(cts.Token);
    // 严架给压力
    YanjiaIsComing(cts.Token);

    // 期待一段时间
    Thread.Sleep(TimeSpan.FromSeconds(5));
    cts.Cancel();

    // 输入后果
    Console.WriteLine($"{methodName}: count = {_count}");
}

private void YanjiaIsComing(CancellationToken token)
{Parallel.ForEachAsync(Enumerable.Range(0, 1_000_000), token, (i, c) =>
    {while (true)
        {
            // do something
            c.ThrowIfCancellationRequested();}
    });
}

这里咱们定义了一个 ProcessTest 办法,用于评测常驻工作的正确性。咱们将在这个办法中启动常驻工作,而后执行一个严架给压力的办法,来模仿十分忙碌的业务操作。最初咱们将输入常驻工作中的计数器的值。

能够初步看一下严架带来的压力有多大:

而后咱们无妨假如,咱们的常驻工作是心愿每秒进行一次计数。那么最终在控制台输入的后果应该是 5 或者 6。但如果小于 5,那么就阐明咱们的常驻工作有问题。

比方上面这样:

[Test]
public void TestTaskRun_Error()
{
    ProcessTest(token =>
    {Task.Run(async () =>
        {while (true)
            {
                _count++;
                await Task.Delay(TimeSpan.FromSeconds(1), token);
            }
        }, token);
    });
    // TestTaskRun_Error: count = 1
}

在该测试中,咱们心愿应用 Task.Run 来执行咱们期待的循环,进行每秒加一的操作。然而,咱们发现,最终输入的后果是 1。这是因为:

  1. Task.Run 会将咱们的工作放入 Task Default Scheduler 线程池中执行。
  2. 然而因为迫于严架给压力,咱们的业务线程会始终处于忙碌状态,因而线程池中的线程也会始终处于忙碌状态。
  3. 从而日导致咱们的常驻工作无奈失常执行。

这里咱们能够看到,Task.Run 并不是一种正确的实现常驻工作的办法。当然实际上这也不是常驻繁多线程,因为这样实质是应用了线程池。

欢送关注作者的微信公众号“newbe 技术专栏”,获取更多技术内容。

全同步过程

联合咱们之前提到的 TaskCreationOptions.LongRunning 以及 Thread 很容易在全同步的状况下实现常驻繁多线程。

[Test]
public void TestSyncTaskLongRunning_Success()
{
    ProcessTest(token =>
    {Task.Factory.StartNew(() =>
        {while (true)
            {
                _count++;
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
        }, token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
    });
    // TestSyncTaskLongRunning_Success: count = 6
}


[Test]
public void TestThread_Success()
{
    ProcessTest(token =>
    {new Thread(() =>
        {while (true)
            {
                _count++;
                Thread.Sleep(TimeSpan.FromSeconds(1));
                if (token.IsCancellationRequested)
                {return;}
            }
        })
        {IsBackground = true,}.Start();});
    // TestThread_Success: count = 6
}

这两种正确的写法都实现了常驻繁多线程,因而咱们能够看到,最终输入的后果都是 6。

昙花线程

那么天然,咱们也能够晓得,如果混合了昙花线程,那么就会呈现问题。

[Test]
public void TestAsyncTaskLongRunning_Error()
{
    ProcessTest(token =>
    {Task.Factory.StartNew(async () =>
        {while (true)
            {
                _count++;
                await Task.Delay(TimeSpan.FromSeconds(1), token);
            }
        }, token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
    });
    // TestAsyncTaskLongRunning_Error: count = 1
}

[Test]
public void TestThreadWithAsync_Error()
{
    ProcessTest(token =>
    {Task CountUp(CancellationToken c)
        {
            _count++;
            return Task.CompletedTask;
        }

        new Thread(async () =>
        {while (true)
            {
                try
                {await CountUp(token);
                    await Task.Delay(TimeSpan.FromSeconds(1), token);
                    token.ThrowIfCancellationRequested();}
                catch (OperationCanceledException e)
                {return;}
            }
        })
        {IsBackground = true,}.Start();});
    // TestThreadWithAsync_Error: count = 1
}

这两种谬误的写法都无奈实现常驻繁多线程,因而咱们能够看到,最终输入的后果都是 1。

不是有 Task 就是异步的

尽管不是本篇的要害内容,然而还是额定补充两个 case 作为比照:

[Test]
public void TestThreadWithTask_Success()
{
    ProcessTest(token =>
    {Task CountUp(CancellationToken c)
        {
            _count++;
            return Task.CompletedTask;
        }

        new Thread(() =>
        {while (true)
            {
                try
                {CountUp(token).Wait(token);
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
                catch (OperationCanceledException e)
                {return;}
            }
        })
        {IsBackground = true,}.Start();});
    // TestThreadWithTask_Success: count = 6
}

[Test]
public void TestThreadWithDelayTask_Error()
{
    ProcessTest(token =>
    {Task CountUp(CancellationToken c)
        {
            _count++;
            return Task.Delay(TimeSpan.FromSeconds(1), c);
        }

        new Thread(() =>
        {while (true)
            {
                try
                {CountUp(token).Wait(token);
                    token.ThrowIfCancellationRequested();}
                catch (OperationCanceledException e)
                {return;}
            }
        })
        {IsBackground = true,}.Start();});
    // TestThreadWithDelayTask_Error: count = 1
}

在这两个 case 但中,尽管在 while 中蕴含了 wait Task,然而因为 Task.CompletedTask 实际上是一种同步代码,所以并不会进入到线程池当中。因而也就不会呈现谬误的状况。

然而这种谬误的起因不是因为昙花线程,是因为咱们在 Thread 中进行了 Wait,然而被调用的 Task 如果的确是一个异步的 Task,那么因为线程池忙碌,咱们的 Task 就会被提早执行,因而就会呈现谬误的状况。

总结

  1. 在全同步的状况下,咱们能够应用 TaskCreationOptions.LongRunning 或者 Thread 来实现常驻繁多线程。从而实现稳固的常驻工作。
  2. 留神 async/await 可能会导致线程池的应用,从而防止常驻繁多线程被毁坏。
  3. 咱们暂未给出带有异步代码的状况下如何实现稳固的常驻工作,咱们将在后续探讨。

测试代码:https://github.com/newbe36524/Newbe.Demo/tree/main/src/BlogDemos/Newbe.LongRunningJob

参考

  • .NET Task 揭秘(2):Task 的回调执行与 await1
  • Task2
  • TaskCreationOptions3
  • 这样在 C# 应用 LongRunningTask 是错的 4
  • async 与 Thread 的谬误联合 5

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

欢送关注作者的微信公众号“newbe 技术专栏”,获取更多技术内容。

  • 本文作者:newbe36524
  • 本文链接:https://www.newbe.pro/Others/0x028-avoid-return-to-threadpool-in-longrunning-task/
  • 版权申明:本博客所有文章除特地申明外,均采纳 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… ↩
  4. https://www.newbe.pro/Others/0x026-This-is-the-wrong-way-to-u… ↩
  5. https://www.newbe.pro/Others/0x027-error-when-using-async-wit… ↩
退出移动版