如何在C#中的GUI线程上启动定时事件序列?

时间:2012-03-23 19:19:32

标签: c# asynchronous f# task-parallel-library system.reactive

我有一个应用程序必须执行以下类型的操作,最好是在GUI线程上,因为这是大多数操作发生的地方,并且没有长时间运行的操作:

Wait 1000
FuncA()
Wait 2000
FuncB()
Wait 1000
FuncC()

我意识到我可以使用具有状态机样式OnTick功能的计时器,但这看起来很麻烦:

    int _state;
    void OnTick(object sender, EventArgs e) {
        switch (_state) {
            case 0:
                FuncA();
                _timer.Interval = TimeSpan.FromSeconds(2);
                _state = 1;
                break;
            case 1:
                FuncB();
                _timer.Interval = TimeSpan.FromSeconds(1);
                _state = 2;
                break;
            case 2:
                FuncC();
                _timer.IsEnabled = false;
                _state = 0;
        }
    }

另外,我希望能够使其足够通用以执行类似

的操作
RunSequenceOnGuiThread(new Sequence {
    {1000, FuncA}
    {2000, FuncB}
    {1000, FuncC}};

有没有惯用的方法来做这种事情?鉴于所有的TPL内容,或者Rx,甚至是F#中的计算表达式,我都假设存在,但我找不到它。

6 个答案:

答案 0 :(得分:10)

Observable.Concat(
        Observer.Timer(1000).Select(_ => Func1()),
        Observer.Timer(2000).Select(_ => Func2()),
        Observer.Timer(1000).Select(_ => Func3()))
    .Repeat()
    .Subscribe();

要做到这一点,你唯一需要做的就是确保你的Func返回一个值(即使该值为Unit.Default,即没有)

编辑:以下是如何制作通用版本:

IObservable<Unit> CreateRepeatingTimerSequence(IEnumerable<Tuple<int, Func<Unit>>> actions)
{
    return Observable.Concat(
        actions.Select(x => 
            Observable.Timer(x.Item1).Select(_ => x.Item2())))
        .Repeat();
}

答案 1 :(得分:8)

这是F#中的草图:

let f() = printfn "f"
let g() = printfn "g"
let h() = printfn "h"

let ops = [
    1000, f
    2000, g
    1000, h
    ]

let runOps ops =
    async {
        for time, op in ops do
            do! Async.Sleep(time)
            op()
    } |> Async.StartImmediate 

runOps ops
System.Console.ReadKey() |> ignore

这是在控制台应用程序中,但您可以在GUI线程上调用runOps。另请参阅this blog

如果您使用的是VS11 / NetFx45 / C#5,则可以使用C#async / awaitList Tuple { {1}}代表。

答案 2 :(得分:5)

使用异步CTP或.NET 4.5(C#5),使用异步方法和await运算符非常简单。这可以直接在UI线程上调用,它将按预期工作。

    public async void ExecuteStuff()
    {
        await TaskEx.Delay(1000);
        FuncA();
        await TaskEx.Delay(2000);
        FuncB();
        await TaskEx.Delay(1000);
        FuncC();
    }

答案 3 :(得分:1)

有趣的所有不同的回应。这是一个简单的DIY选项,它不依赖于任何其他库,也不会不必要地占用线程资源。

基本上,对于列表中的每个操作,它会创建一个执行该操作的onTick函数,然后以剩余的操作和延迟递归调用DoThings。

在这里,ITimer只是DispatcherTimer的一个简单包装(但它也可以用于SWF计时器,或用于单元测试的模拟计时器),DelayedAction只是具有int DelayAction action

的元组
public static class TimerEx {
    public static void DoThings(this ITimer timer, IEnumerable<DelayedAction> actions) {
        timer.DoThings(actions.GetEnumerator());
    }

    static void DoThings(this ITimer timer, IEnumerator<DelayedAction> actions) {
        if (!actions.MoveNext())
            return;
        var first = actions.Current;
        Action onTick = null;
        onTick = () => {
            timer.IsEnabled = false;
            first.Action();
            // ReSharper disable AccessToModifiedClosure
            timer.Tick -= onTick;
            // ReSharper restore AccessToModifiedClosure
            onTick = null;
            timer.DoThings(actions);
        };
        timer.Tick += onTick;
        timer.Interval = first.Delay;
        timer.IsEnabled = true;
    }
}

如果您不想深入研究F#或参考Rx或使用.Net 4.5,这是一个简单可行的解决方案。

以下是如何测试它的示例:

[TestClass]
public sealed class TimerExTest {
    [TestMethod]
    public void Delayed_actions_should_be_scheduled_correctly() {
        var timer = new MockTimer();
        var i = 0;
        var action = new DelayedAction(0, () => ++i);
        timer.DoThings(new[] {action, action});
        Assert.AreEqual(0, i);
        timer.OnTick();
        Assert.AreEqual(1, i);
        timer.OnTick();
        Assert.AreEqual(2, i);
        timer.OnTick();
        Assert.AreEqual(2, i);
    }
}

以下是其他要编译的类:

public interface ITimer {
    bool IsEnabled { set; }
    double Interval { set; }
    event Action Tick;
}

public sealed class Timer : ITimer {
    readonly DispatcherTimer _timer;

    public Timer() {
        _timer = new DispatcherTimer();
        _timer.Tick += (sender, e) => OnTick();
    }

    public double Interval {
        set { _timer.Interval = TimeSpan.FromMilliseconds(value); }
    }

    public event Action Tick;

    public bool IsEnabled {
        set { _timer.IsEnabled = value; }
    }

    void OnTick() {
        var handler = Tick;
        if (handler != null) {
            handler();
        }
    }
}

public sealed class MockTimer : ITimer {
    public event Action Tick;

    public bool IsEnabled { private get; set; }

    public double Interval { set { } }

    public void OnTick() {
        if (IsEnabled) {
            var handler = Tick;
            if (handler != null) {
                handler();
            }
        }
    }
}


public sealed class DelayedAction {
    readonly Action _action;
    readonly int _delay;

    public DelayedAction(int delay, Action action) {
        _delay = delay;
        _action = action;
    }

    public Action Action {
        get { return _action; }
    }

    public int Delay {
        get { return _delay; }
    }
}

答案 4 :(得分:1)

这是一种结合“收益率回报”和反应性框架的方法,为您提供“穷人的异步”。基本上让你“等待”任何IObservable。在这里,我只是将它用于计时器,因为这是你感兴趣的,但你可以让它“等待”按钮点击(使用Subject<Unit>)等,然后继续下一件事。

public sealed partial class Form1 : Form {
    readonly Executor _executor = new Executor();

    public Form1() {
        InitializeComponent();
        _executor.Run(CreateAsyncHandler());
    }

    IEnumerable<IObservable<Unit>> CreateAsyncHandler() {
        while (true) {
            var i = 0;
            Text = (++i).ToString();
            yield return WaitTimer(500);
            Text = (++i).ToString();
            yield return WaitTimer(500);
            Text = (++i).ToString();
            yield return WaitTimer(500);
            Text = (++i).ToString();
        }
    }

    IObservable<Unit> WaitTimer(double ms) {
        return Observable.Timer(TimeSpan.FromMilliseconds(ms), new ControlScheduler(this)).Select(_ => Unit.Default);
    }

}

public sealed class Executor {
    IEnumerator<IObservable<Unit>> _observables;
    IDisposable _subscription = new NullDisposable();

    public void Run(IEnumerable<IObservable<Unit>> actions) {
        _observables = (actions ?? new IObservable<Unit>[0]).Concat(new[] {Observable.Never<Unit>()}).GetEnumerator();
        Continue();
    }

    void Continue() {
        _subscription.Dispose();
        _observables.MoveNext();
        _subscription = _observables.Current.Subscribe(_ => Continue());
    }

    public void Stop() {
        Run(null);
    }
}

sealed class NullDisposable : IDisposable {
    public void Dispose() {}
}

这是Daniel Earwicker的AsyncIOPipe想法的略微修改:http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/

答案 5 :(得分:0)

如果您可以使用C#4.5来执行此操作,请使用Firoso帖子:这是在C#中实现此目的的最佳方式,正是Async的构建目标。

但是,如果你不能,可能有一些方法可以做到这一点。我会做一个“简单”的经理来做这件事:

public partial class Form1 : Form
{
    private TimedEventsManager _timedEventsManager;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        _timedEventsManager 
            = new TimedEventsManager(this,
                new TimedEvent(1000, () => textBox1.Text += "First\n"),
                new TimedEvent(5000, () => textBox1.Text += "Second\n"),
                new TimedEvent(2000, () => textBox1.Text += "Third\n")
            );

    }

    private void button1_Click(object sender, EventArgs e)
    {
        _timedEventsManager.Start();
    }
}

public class TimedEvent
{
    public int Interval { get; set; }
    public Action Action { get; set; }

    public TimedEvent(int interval, Action func)
    {
        Interval = interval;
        Action = func;
    }
}

public class TimedEventsManager
{
    private readonly Control _control;
    private readonly Action _chain;

    public TimedEventsManager(Control control, params TimedEvent[] timedEvents)
    {
        _control = control;
        Action current = null;

        // Create a method chain, beginning by the last and attaching it 
        // the previous.
        for (var i = timedEvents.Length - 1; i >= 0; i--)
        {
            var i1 = i;
            var next = current;
            current = () =>
                          {
                              Thread.Sleep(timedEvents[i1].Interval);
                               // MUST run it on the UI thread!
                              _control.Invoke(new Action(() => timedEvents[i1].Action()));
                              if (next != null) next();
                          };
        }

        _chain = current;
    }

    public void Start()
    {
        new Thread(new ThreadStart(_chain)).Start();
    }
}

请注意,此示例特定于Winforms(使用Control.Invoke())。 WPF需要稍微不同的版本,它使用线程调度程序来实现相同的功能。 (如果我的记忆没有让我失望,你也可以使用Control.Dispatcher.Invoke(),但要记住它是一个不同的控件)