异步操作循环调用的模式

Pattern for cyclic calls of async operations

我想定期做一些基于 I/O 的异步操作。 它不应该 运行 尽可能快,而是在周期之间有一个可配置的延迟。

到目前为止,我提出了两种不同的方法,我想知道哪种方法在资源消耗方面更好。

使用 Task.Run()

的方法 1
internal class Program
    {
        private static void Main(string[] args)
        {
            for (var i = 0; i < 80; i++)
            {
                var handler = new CommunicationService();
                handler.Start();
            }


            Console.ReadLine();
        }
    }

    internal class CommunicationService
    {
        private readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler());

        public void Start()
        {
            Run();
        }

        private void Run()
        {
            Task.Run(async () =>
            {
                try
                {
                    var result = await _httpClient.GetAsync(someUri);
                    await Task.Delay(TimeSpan.FromSeconds(configurableValue));
                }
                catch (Exception ex)
                {
                    Console.Error.WriteLine(ex);
                    Run();
                }

                Run();
            });
        }
    }

所以async操作被包裹在Task.Run()fire and forget风格,所以它可以无阻塞地启动。

使用 EventHandler 的方法 2

 internal class CommunicationService
    {
        private event EventHandler CommunicationHandler;
        private readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler());

        public void Start()
        {
            CommunicationHandler = (o, events) => Communicate();
            OnCommunicationTriggered();
        }

        private async void Communicate()
        {
            try
            {
                var result = await _httpClient.GetAsync(someUri);
                await Task.Delay(TimeSpan.FromSeconds(configurableValue));

            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
                OnCommunicationTriggered();
            }
            OnCommunicationTriggered();
        }

        private void OnCommunicationTriggered()
        {
            CommunicationHandler.Invoke(this, EventArgs.Empty);
        }
    }

使用这种方法包装在 Task.Run() 中是没有必要的,但是这样更好吗?

我为这两种方法创建了一个 .Net 控制台应用程序,并在几分钟内记录了以下性能计数器,老实说,我没有看到太大的区别:

  1. \Process(Events)% 处理器时间(接近 2 ~20%)
  2. \Process(Events)\Private Bytes(几乎相等,稍微接近 2 较低)
  3. \Process(Events)\Thread Count(方法降低 2 ~ 25%)
  4. .NET CLR LocksAndThreads(Events) 当前逻辑线程数 (几乎相等,接近 2 略高)
  5. .NET CLR LocksAndThreads(Events) 当前物理线程数 (几乎相等,接近 2 略高)
  6. .NET CLR LocksAndThreads(Events)\Contention Rate / sec(方法 2 ~ 高出 50%)

我是不是忽略了这些计数器的重点?

两者实际上在做同样的事情。事件选项似乎增加了不必要的复杂性。资源消耗没有显着差异。

更合适的选择是使用 timer.timer or threading.timer。这使得代码更易于阅读和理解,因为它表达了 intent。在幕后,所有备选方案的结果或多或少是相同的。

您需要考虑如何计算时间。执行时间是否应该包含在计时间隔中?通常间隔比执行时间长很多,所以无所谓。如果这很重要,您可能需要将计时器设置为仅触发一次,并且 reset 计时器在您的操作完成后触发。

根据接受的答案,这是我的新方法:

 internal class EventHandlerService
    {
        private Timer _timer;
        private TimeSpan refreshTime = TimeSpan.FromSeconds(5);

        public void Start()
        {           
            _timer = new Timer(Communicate, null, 0,(int)refreshTime.TotalMilliseconds);
        }

        private void Communicate(object stateInfo)
        {
            Task.Run(async () =>
            {
                _timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); // stop the timer
                Console.WriteLine($"Starting at {DateTime.UtcNow.ToString("O")}");
                var stopWatch=new Stopwatch();
                stopWatch.Start();
                try
                {
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
                catch (Exception ex)
                {
                }
                finally
                {
                    Console.WriteLine($"Finishing at {DateTime.UtcNow.ToString("O")} after: {stopWatch.Elapsed}");
                    var dueTime = refreshTime.Subtract(stopWatch.Elapsed);
                    Console.WriteLine($"Calced dueTime to: {dueTime.TotalSeconds} at {DateTime.UtcNow.ToString("O")}");

                    _timer.Change(Math.Max((int) dueTime.TotalMilliseconds, 0), (int)refreshTime.TotalMilliseconds); // start the timer
                }
            });
        }

      
    }

通过这种方法,我满足了我的需求: 实际 refresh/timer 周期永远不会低于 5 秒,但如果处理程序花费的时间超过 5 秒,则会立即触发下一次执行。