如何延迟C#中的Action,类似于QTimer::singleShot?

6

Qt具有使用Lambda执行定时操作的简洁功能。

仅需一行代码即可在延迟后执行操作:

    QTimer::singleShot(10, [=](){
        // do some stuff
    });

虽然我还没有在C#中找到相应的等效物。


我找到的最接近的是

Timer timer = new Timer();
timer.Interval = 10;
timer.Elapsed += (tsender, args) => { 
  // do some stuff 
  timer.Stop();
};
timer.Start();

但它的外观并不够干净。

有没有更好的方法来实现这个?

使用场景是通过串行线发送数据到一些硬件,在点击按钮或执行操作时,通常需要发送命令,然后在几毫秒后发送一个数据包。


使用帮助函数的解决方案:

    public void DelayTask(int timeMs, Action lambda)
    {
        System.Timers.Timer timer = new System.Timers.Timer();
        timer.Interval = timeMs;
        timer.Elapsed += (tsender, args) => { lambda.Invoke(); };
        timer.AutoReset = false;
        timer.Start();
    }

调用方

DelayTask(10, () => /* doSomeStuff...*/ );

1
你写一个函数怎么样?我怀疑 QTimer::singleShot 在内部看起来更干净。 - tkausl
1
如果 timer.AutoReset 为 false(默认值为 false),则无需停止计时器。 - Maximilian Ast
2
延迟执行任务可能是一个选项。 - tkausl
@tkausl 这是否是线程安全的? - Damien
@Damien,你可以使用System.Threading.Timer代替System.Timers.Timer,并在构造函数中将其配置为仅触发一次,例如:new Timer(_=>lambda(),null,timeMS,Timeout.Infinite); - Panagiotis Kanavos
显示剩余7条评论
3个回答

5
我想到的最接近的东西可能就是像你建议的那样的帮助函数:
public static class DelayedAction
{
    public static Task RunAsync(TimeSpan delay, Action action)
    {
       return Task.Delay(delay).ContinueWith(t => action(), TaskScheduler.FromCurrentSynchronizationContext());
    }
}

这个类的使用方法与你对Qt的理解非常相近:

await DelayedAction.RunAsync(TimeSpan.FromSeconds(10), () => /* do stuff */);

更新

如同一个已存在的SO 问题中所提到的,ContinueWith默认情况下不会保留同步上下文。

在当前问题中,lambda表达式正在更新一些UI控件,因此必须在UI线程上运行。

为了实现这一点,在调用方法ContinueWithTaskScheduler.FromCurrentSynchronizationContext())时,调度程序必须指定同步上下文,以确保此类更新是可能的。


@PanagiotisKanavos 已经修复了 :) - Kzryzstof
@Damien 哪个解决方案覆盖了 lambda?这两个解决方案(Panagiotis' 和这个)似乎都能正常工作,是吗? - Kzryzstof
在这种情况下,等待任务完成并不是必要的,所以我认为这不是一个问题(甚至可能被编译器删除 - 我不确定)。关于使用哪个线程,这里使用的是默认调度程序,这意味着它将在ThreadPool线程上运行。 - Kzryzstof
@Kzrystof 我有这段代码将数据发送到串口,但如果串口没有打开,则会捕获异常,并使用异常消息更新UI上的标签。这导致了一个线程异常。 - Damien
让我们在聊天中继续这个讨论 - Kzryzstof
显示剩余5条评论

5

您应该使用 System.Threading.Timer 而不是 System.Timers.Timer。 System.Timers.Timer 是一个多线程定时器,旨在与桌面应用程序一起使用,这就是为什么它继承自 Component 并需要通过属性进行配置。

然而,使用 System.Threading.Timer,您可以通过单个构造函数调用创建一个单次触发定时器:

var timer= new Timer(_=>lambda(),null,timeMS,Timeout.Infinite);

这个简单粗暴的控制台应用程序:

static void Main(string[] args)
{
    var timeMS = 1000;
    var timer = new Timer(_ => Console.WriteLine("Peekaboo"), null, timeMS, Timeout.Infinite);
    Console.ReadKey();
}

即使主线程被ReadKey();阻塞,也会在1秒后打印出Peekaboo


2
使用微软的反应式框架(NuGet“System.Reactive”),您可以这样做:
IDisposable subscription =
    Observable
        .Timer(TimeSpan.FromMilliseconds(10.0))
        .Subscribe(_ => { /* Do Stuff Here */ });

IDisposable 允许您通过调用 subscription.Dispose(); 在触发之前取消订阅。


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