Как можно запустить синхронизированную последовательность событий в потоке GUI в C #? - PullRequest
6 голосов
/ 23 марта 2012

У меня есть приложение, которое должно выполнять следующие действия, предпочтительно в потоке графического интерфейса, поскольку именно там происходит большая часть действия, а длительных операций нет:

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 ]

10 голосов
/ 24 марта 2012
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();
}
8 голосов
/ 23 марта 2012

Вот эскиз этого в 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

Это в консольном приложении, но вы можете просто вызвать runOps в потоке GUI. Смотрите также этот блог .

Если вы используете VS11 / NetFx45 / C # 5, вы можете сделать то же самое с C # async / await и List из Tuple из Action делегатов.

5 голосов
/ 23 марта 2012

с использованием асинхронного CTP или .NET 4.5 (C # 5) ДЕЙСТВИТЕЛЬНО легко с помощью асинхронного метода и оператора await. Это можно вызвать непосредственно в потоке пользовательского интерфейса, и оно будет работать как положено.

    public async void ExecuteStuff()
    {
        await TaskEx.Delay(1000);
        FuncA();
        await TaskEx.Delay(2000);
        FuncB();
        await TaskEx.Delay(1000);
        FuncC();
    }
1 голос
/ 18 апреля 2012

Вот способ объединить «доходность» и реактивную структуру, чтобы дать вам «асинхронность бедняков».В основном позволяет вам «ждать» любой 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() {}
}

Это небольшая модификация идеи AsyncIOPipe Даниэля Эрвикера: http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/

1 голос
/ 24 марта 2012

Интересно все разные ответы. Вот простой вариант DIY, который не зависит от каких-либо других библиотек и не требует ненужных ресурсов потоков.

Обычно для каждого действия в вашем списке создается функция onTick, которая выполняет это действие, а затем рекурсивно вызывает DoThings с оставшимися действиями и задержками.

Здесь ITimer - это простая оболочка вокруг DispatcherTimer (но она также будет работать с SWF-таймером или фиктивным таймером для модульного тестирования), а DelayedAction - это просто кортеж с int Delay и Action 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; }
    }
}
0 голосов
/ 24 марта 2012

Если вы можете использовать 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 (), но имейте в виду, что это другой элемент управления)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...