向计时器构造函数传递异步回调函数

61

我有一个异步回调函数,它被传递到Timer(来自System.Threading)的构造函数中:

private async Task HandleTimerCallback(object state)
{
    if (timer == null) return;

    if (asynTaskCallback != null)
    {
        await HandleAsyncTaskTimerCallback(state);
    }
    else
    {
        HandleSyncTimerCallback(state);
    }
}

定时器:

timer = new Timer(async o => await HandleTimerCallback(o), state, CommonConstants.InfiniteTimespan,
            CommonConstants.InfiniteTimespan);

有没有办法在Lambda中省略掉“o”参数?因为对于非异步情况,我可以将我的“handler”作为委托传递。
 timer = new Timer(HandleTimerCallback, state, CommonConstants.InfiniteTimespan,
            CommonConstants.InfiniteTimespan);

2
在 Timer 和 async/await 混合使用时,这确实没有意义。如果你想在 async/await 环境中实现定时器行为,只需在循环体中加入 await Task.Delay(someValue),并在循环体中调用你的方法即可。 - spender
1
当然这是有意义的。也许你想要计时器的操作异步进行,这样就不会阻塞其他动作了。 - hal9000
5个回答

120

有没有办法在Lambda中省略那个o参数?

当然可以,只需将您的事件处理程序方法定义为async void

private async void HandleTimerCallback(object state)

你的意思是没有方法体吗?为什么不用 Task 而是用 void - demo
2
@demo: 因为TimerCallback的返回类型是void - Stephen Cleary
20
在使用线程定时器时,使用async void是可以的吗?在你的很多文章中,你提到永远不要使用async void,因为异常无法得到处理(除非该方法由一个特殊的调用者调用,可以处理这些情况,例如:WinForm事件)。 - Andy
43
@Andy:我总是建议避免使用async void,因为它是为事件处理程序而设计的。这是一个事件处理程序。使用async void时的异常处理语义是为了与事件相匹配的:在这种情况下,如果方法中出现异常,它将在线程池线程上引发,就像方法是同步的一样。 - Stephen Cleary
2
@Vitalii 这与异步 void 没有任何关系,而与计时器有关。如果您不希望处理程序调用重叠,则将周期参数设置为 Timeout.Infinite。如果您想要一个重复的计时器,那么为了避免重叠调用,您必须使用锁来同步处理程序调用,或在每次处理程序调用期间启动/停止计时器。 - Neurion
显示剩余2条评论

10
你可以使用一个包装方法,如David Fowler在这里推荐的方法:这里
public class Pinger
{
    private readonly Timer _timer;
    private readonly HttpClient _client;
    
    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new Timer(Heartbeat, null, 1000, 1000);
    }

    public void Heartbeat(object state)
    {
        // Discard the result
        _ = DoAsyncPing();
    }

    private async Task DoAsyncPing()
    {
        await _client.GetAsync("http://mybackend/api/ping");
    }
}

只有在您可以放心“点火并忘记任务”(即忽略可能出现的错误)的情况下,才能保证效果。 - Theodor Zoulias
@TheodorZoulias 但是接受的答案中的async void方法不也是一个"点火并忘记"的方式吗? - undefined
1
@Alex 不,async void 是一种“发射并崩溃”的方式,而不是“发射并忘记”。在 async void 方法中的任何错误都会在捕获的同步上下文中重新抛出,通常会终止进程。 - undefined

7
我只是想提一下,.NET 6引入了一个新的计时器类,叫做PeriodicTimer,它是异步优先的,并且完全避免了回调函数。

https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer

使用起来可能有点奇怪(因为它是一个无限循环),但由于它是异步的,所以不会阻塞其他线程的执行。
public async Task DoStuffPeriodically()
{
    var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));

    while (await timer.WaitForNextTickAsync())
    {
        //do stuff
    }
}

它完全避免了回调,使用更简单的代码,并且是后台服务的完美候选。

基本上你会得到一个“永不停止的任务”来执行一些操作。

要启动任务,只需调用,例如_ = DoStuffPeriodically()并使用丢弃操作符(但在方法内部添加try-catch以防止后台任务崩溃),或通过Task.Run来启动此任务。

Nick Chapsas有一个很好的视频解释了用法:https://www.youtube.com/watch?v=J4JL4zR_l-0(包括如何使用CancellationToken来中止计时器)。


-4
我使用了像Vlad上面建议的包装器,但是我使用了Task.Wait来确保异步进程完成:
private void ProcessWork(object state)
{
    Task.WaitAll(ProcessScheduledWorkAsync());
}

protected async Task ProcessWorkAsync()
{
    
}

1
为什么要使用Task.WaitAll(ProcessScheduledWorkAsync());而不是ProcessScheduledWorkAsync().Wait();?无论哪种情况,阻塞线程有什么好处? - Theodor Zoulias
@TheodorZoulias,这两者远非相同,第二个很可能会导致线程死锁。 - Erik Philips
据我所知,这两者完全等价。您能否提供一个最小的可重现示例,以演示 task.Wait()Task.WaitAll(task) 之间的区别? - Theodor Zoulias
@TheodorZoulias 我知道你认为它是如何工作的。我建议阅读A Tour of Task, Part 5: Waiting,并查看庞大的SO问题How to call asynchronous method from synchronous method in C#?。祝你好运! - Erik Philips
@TheodorZoulias,你要么不在意阅读然而,大多数情况下,Task.Wait是危险的,因为它可能会导致死锁,要么你就是来搞事情的。编写代码时可以随意。 - Erik Philips
显示剩余3条评论

-4
如果您不想使用包装类,可以在TimerCallback函数中执行以下操作:
private void TimerFunction(object state)
{
    _ = Task.Run(async () =>
    {
        await this.SomeFunctionAsync().ConfigureAwait(false);
    });
}

1
这个做的和包装器一样。 - Kieran Foot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接