我有一个应用程序,它必须做以下类型的事情,最好是在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#中的计算表达式,我会假设存在一个,但我找不到
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();
}
这是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。另请参阅此博客。
如果使用的是VS11/NetFx45/C#5,则可以使用C#async
/await
和Tuple
的List
的Action
委托执行类似的操作。
使用异步CTP或.NET 4.5(C#5),使用异步方法和等待运算符非常容易。这可以直接在UI线程上调用,它将按预期工作。
public async void ExecuteStuff()
{
await TaskEx.Delay(1000);
FuncA();
await TaskEx.Delay(2000);
FuncB();
await TaskEx.Delay(1000);
FuncC();
}
这里有一种将"yield return"和反应式框架结合起来的方法,为您提供一个"穷人的异步"。基本上可以让您"等待"任何可观察的对象。在这里,我只是把它用于计时器,因为这是你感兴趣的,但在继续下一件事之前,你可以让它"等待"按钮点击(使用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/
所有不同的反应都很有趣。这里有一个简单的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; }
}
}
如果您可以使用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 += "Firstn"),
new TimedEvent(5000, () => textBox1.Text += "Secondn"),
new TimedEvent(2000, () => textBox1.Text += "Thirdn")
);
}
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(),但请记住这是一个不同的控件)