异步操作循环调用的模式
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 控制台应用程序,并在几分钟内记录了以下性能计数器,老实说,我没有看到太大的区别:
- \Process(Events)% 处理器时间(接近 2 ~20%)
- \Process(Events)\Private Bytes(几乎相等,稍微接近 2
较低)
- \Process(Events)\Thread Count(方法降低 2 ~ 25%)
- .NET CLR LocksAndThreads(Events) 当前逻辑线程数
(几乎相等,接近 2 略高)
- .NET CLR LocksAndThreads(Events) 当前物理线程数
(几乎相等,接近 2 略高)
- .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 秒,则会立即触发下一次执行。
我想定期做一些基于 I/O 的异步操作。 它不应该 运行 尽可能快,而是在周期之间有一个可配置的延迟。
到目前为止,我提出了两种不同的方法,我想知道哪种方法在资源消耗方面更好。
使用 Task.Run()
的方法 1internal 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 控制台应用程序,并在几分钟内记录了以下性能计数器,老实说,我没有看到太大的区别:
- \Process(Events)% 处理器时间(接近 2 ~20%)
- \Process(Events)\Private Bytes(几乎相等,稍微接近 2 较低)
- \Process(Events)\Thread Count(方法降低 2 ~ 25%)
- .NET CLR LocksAndThreads(Events) 当前逻辑线程数 (几乎相等,接近 2 略高)
- .NET CLR LocksAndThreads(Events) 当前物理线程数 (几乎相等,接近 2 略高)
- .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 秒,则会立即触发下一次执行。