【问题标题】:Strange behaviour with async operations inside System.Threading.TimerSystem.Threading.Timer 中异步操作的奇怪行为
【发布时间】:2018-04-22 05:27:05
【问题描述】:

简介: 我有一个 Windows 服务,它监视其他应用程序和服务,如果它们在不同的时间间隔运行。 该服务为每个受监视的应用程序(称为“监视器”)使用一个计时器(System.Threading.Timer)。 不同类型的应用程序需要不同类型的监视器,有些同步工作,有些异步工作(例如,使用 HttpClient 的那些)。

所以我到了需要在计时器中进行异步调用的地步。 我已将代码简化到极限,以便可以将其发布在这里。它可以直接运行到控制台项目中。 我的问题是这段代码有一个非常奇怪的行为,因为引入了更多的计时器 - 它运行得越难,直到它根本没有响应(超过 20 个计时器)。 监控运行时间不正是异步操作中设置的延迟(100ms)吗?

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace TestMain
{
    class TestMain
    {
        private static List<TestTimer> timers = new List<TestTimer>();

        static void Main(string[] args)
        {
            for (int i = 0; i < 20; i++)
            {
                TestMain.timers.Add(new TestTimer(i));
            }
            Console.WriteLine("Press [Enter] to exit.");
            Console.ReadLine();
        }

        public class TestTimer
        {
            public Int32 Id { get; private set; }
            private Timer timer;

            public TestTimer(Int32 id)
            {
                this.Id = id;
                this.timer = new Timer(this.Test, null, 1000, 30 * 1000);
            }

            private void Test(Object state)
            {
                TestWorker t = new TestWorker(this.Id);
                t.Run();
            }
        }

        public class TestWorker
        {
            public Int32 Id { get; private set; }
            private Stopwatch sw = new Stopwatch();

            public TestWorker(Int32 id) { this.Id = id; }

            public void Run()
            {
                this.RunAsync().Wait();
            }
            private async Task RunAsync()
            {
                this.Log(String.Format("Start[{0,2}]", this.Id));
                this.sw.Restart();

                await Task.Run(() => { System.Threading.Thread.Sleep(100); }).ConfigureAwait(false);

                this.sw.Stop();
                this.Log(String.Format("  End[{0,2}]   Duration=[{1}]", this.Id, (Int32)this.sw.ElapsedMilliseconds));
            }
            private void Log(String text)
            {
                Console.WriteLine(String.Format("{0,20}  {1}", DateTime.Now, text));
            }

        }
    }
}

我附加了一个带有运行的打印屏幕。 Console Printscreen

【问题讨论】:

  • 异步操作完成后不再等待部分问题但线程间仍存在瓶颈的解决方案。对我来说正确的解决方案是使用 ThreadPool.SetMinThreads 的解决方案,因为它可以随时间扩展,因为在服务启动时,所有需要监控的应用程序(数量和类型)都是已知的,并且可以设置最小线程数因此。谢谢你的回答。

标签: c# asynchronous timer


【解决方案1】:

这是因为线程池如何管理其线程。线程池具有“最少”数量的线程(您可以使用ThreadPool.GetMinThreads 阅读)。默认情况下(这取决于 .NET 版本,但我们不会使事情复杂化)它与处理器内核的数量有关,例如在我的机器上是 8。当这 8 个线程很忙并且您需要更多时 - 线程池将首先等待一段时间让其中一个繁忙的线程可用(它将等待大约 1 秒),如果没有可用的线程 - 它将向池中添加一个线程。

定时器回调在线程池上执行。因此,当您的所有 20 个计时器同时触发它们的回调时 - 只有 8 个(在我的情况下)回调被执行。其余的排队,大约每秒执行一次(它们从线程池请求线程执行,但每次等待 1 秒,因为此时线程池中的所有线程都很忙)。他们很忙,因为您的计时器回调等待RunAsync 与Wait() 一起完成。所以只有在 12 (20-8) 秒后所有计时器回调都已执行。

当计时器回调执行时 - 它将Start 消息写入控制台并启动秒表。然后您通过执行Task.Run 从线程池请求另一个线程。所有这些请求都在计时器回调之后排队,因此只有在所有计时器启动后,您才会开始接收End 消息。

所以现在您有 20 个线程忙于等待 RunAsync 完成。首先Task.Run 请求另一个线程。该线程等待 100 毫秒,之后它是空闲的并且可以重用,因此任务池不会为每个 Task.Run 创建新线程并将重用这个线程(因为 100 毫秒小于 1 秒,它将等待一个线程可用)。

为了使这种行为更符合预期 - 将线程池中的最小线程设置为 ThreadPool.SetMinThread 到某个更大的值,或者在等待 RunAsync 完成时不要保持计时器回调线程。

【讨论】:

    【解决方案2】:

    System.Threading.Timer 正在使用线程池,所以线程数是有限制的,这就是你的经验。

    监视器运行时间不正是异步操作中设置的延迟(100ms)吗?

    这就是你想要的,但似乎线程忙等待任务完成甚至更长时间,因为里面的任务也想使用线程池中的线程。 p>

    快速解决方法是使用即发即弃的方法 (credits),这样计时器不会等待任何东西,而不是

    public void Run()
    {
        RunAsync().Wait();
    }
    

    做

    public void Run()
    {
        #pragma warning disable 4014
        RunAsync();
        #pragma warning restore 4014
    }
    

    【讨论】:

      猜你喜欢
      • 2013-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-17
      • 2015-12-29
      • 1970-01-01
      • 2020-07-28
      • 2017-09-13
      相关资源
      最近更新 更多