我该如何等待C#事件被触发?

18

我有一个Sender类,它在IChannel上发送一条Message

public class MessageEventArgs : EventArgs {
  public Message Message { get; private set; }
  public MessageEventArgs(Message m) { Message = m; }
}

public interface IChannel {
  public event EventHandler<MessageEventArgs> MessageReceived;
  void Send(Message m);
}

public class Sender {
  public const int MaxWaitInMs = 5000;
  private IChannel _c = ...;

  public Message Send(Message m) {
    _c.Send(m);
    // wait for MaxWaitInMs to get an event from _c.MessageReceived
    // return the message or null if no message was received in response
  }
}

当我们发送消息时,根据所发送的消息类型,IChannel 有时会通过触发 MessageReceived 事件来给予回应。事件参数中包含所需的消息。

我想让 Sender.Send() 方法等待一段时间,以查看是否触发了这个事件。如果是这样,我将返回其 MessageEventArgs.Message 属性。如果没有,我将返回空的 Message

如何以这种方式等待?我希望不必使用 ManualResetEvents 等线程管理工具,因此仅使用常规的 event 对象对我来说最理想。


“……更倾向于不必自己处理线程相关的工作……” - 这是否可以理解为您的应用程序完全在单个线程中运行? - Matt Davis
不,使用工作线程等是可以的,而且它们在应用程序的其他地方也被使用(例如,IChannel实现会生成一个新线程来写入流)。不过,我想继续使用委托和事件语法糖,而不使用更低级别的 System.Threading - Evan Barkley
3
委托和事件不是用于线程的语法糖。事件在同一线程上引发和处理(它是一个对函数调用的语法糖,工作方式相同)。 - Isak Savo
@Isak:抱歉,我表达得不太好。我的问题部分是,如果我坚持使用“事件”和“委托”,是否可以有效地获得ManualResetEvent - Evan Barkley
5个回答

22

使用 AutoResetEvent

等我几分钟,我会提供一个示例。

这是示例:

public class Sender
{
    public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(5000);

    private IChannel _c;
    private AutoResetEvent _messageReceived;

    public Sender()
    {
        // initialize _c
        this._messageReceived = new AutoResetEvent(false);
        this._c.MessageReceived += this.MessageReceived;
    }

    public Message Send(Message m)
    {
        this._c.Send(m);
        // wait for MaxWaitInMs to get an event from _c.MessageReceived
        // return the message or null if no message was received in response


        // This will wait for up to 5000 ms, then throw an exception.
        this._messageReceived.WaitOne(MaxWait);

        return null;
    }

    public void MessageReceived(object sender, MessageEventArgs e)
    {
        //Do whatever you need to do with the message

        this._messageReceived.Set();
    }
}

我知道可以使用ManualResetEvent来完成这个任务,但正如我所说的(“我不想使用ManualResetEvents来完成线程工作”),我希望有另一种选择。 - Evan Barkley
抱歉,在你原始帖子中我漏掉了那个。即使你创建了一个委托并调用了 BeginInvoke,你仍然需要有一个回调方法,并且你仍然需要以某种方式手动同步线程。虽然,IAsyncResult 实例应该为你提供一个 WaitHandle,这会稍微简化一些事情,但并不多。 - Toby
这看起来像是一个解决方案,而且我认为这已经尽可能地简单明了了。谢谢! - Evan Barkley
另外一个问题:使用这种方法,看起来我得让 Sender.MessageReceived 存储一些数据(比如说在实例字段中),以便于之后 Sender.Send 可以返回它。(如果要尝试返回除了 null 外的其他东西,请思考一下该怎么做。)从 Sender.Send 方法内部是否有自包含的方法可以实现呢? - Evan Barkley

2

你有尝试将异步调用函数分配给委托,然后调用mydelegateinstance.BeginInvoke吗?

这是参考链接。

使用下面的示例,只需调用

FillDataSet(ref table, ref dataset);

它会像魔术一样工作。 :)

#region DataSet manipulation
///<summary>Fills a the distance table of a dataset</summary>
private void FillDataSet(ref DistanceDataTableAdapter taD, ref MyDataSet ds) {
  using (var myMRE = new ManualResetEventSlim(false)) {
    ds.EnforceConstraints = false;
    ds.Distance.BeginLoadData();
    Func<DistanceDataTable, int> distanceFill = taD.Fill;
    distanceFill.BeginInvoke(ds.Distance, FillCallback<DistanceDataTable>, new object[] { distanceFill, myMRE });
    WaitHandle.WaitAll(new []{ myMRE.WaitHandle });
    ds.Distance.EndLoadData();
    ds.EnforceConstraints = true;
  }
}
/// <summary>
/// Callback used when filling a table asynchronously.
/// </summary>
/// <param name="result">Represents the status of the asynchronous operation.</param>
private void FillCallback<MyDataTable>(IAsyncResult result) where MyDataTable: DataTable {
  var state = result.AsyncState as object[];
  Debug.Assert((state != null) && (state.Length == 2), "State variable is either null or an invalid number of parameters were passed.");

  var fillFunc = state[0] as Func<MyDataTable, int>;
  var mre = state[1] as ManualResetEventSlim;
  Debug.Assert((mre != null) && (fillFunc != null));
  int rowsAffected = fillFunc.EndInvoke(result);
  Debug.WriteLine(" Rows: " + rowsAffected.ToString());
  mre.Set();
}

1
我不确定我理解这个答案。你能否在这个例子的背景下说明你的意思? - Evan Barkley
嗯...我很感激你提供的具体例子,但对我来说仍然很难理解。而且这个例子似乎直接使用了ManualResetEvents。=/ 这个例子也似乎与我的情况不相似,因为有一个调用EndLoadData的操作,它似乎做了一些隐藏的事情。 - Evan Barkley
我看不到任何绕过重置事件的方法(无论它们是什么类型),你所要求的需要线程处理,因此是不可避免的。没有什么可害怕的,特别是在这个例子中 - 这就是我展示的设计模式的美妙之处 - 它完全自包含,没有线程问题(它将它们隐藏在异步调用后面)! - Geoff
@Geoff:明白了。只是为了明确,您的建议是在我的 Sender.Send 方法中执行以下操作:(1)将 IChannel.Send 调用分配给委托,(2)然后使用 BeginInvoke 异步调用该委托,(3)传递等待句柄和正在调用的委托的引用,(4)然后在等待句柄上调用 WaitOne,(5)一旦在委托中调用 .Set,就继续进行下一步操作? - Evan Barkley
@Evan:但那样行不通,你会调用Set吗?如果你在委托中调用它,如何保证任何等待的时间?如果你总是Sleep(timeout),无论如何都会睡眠,从而破坏所有复杂的线程。 - Ron Warholic
显示剩余4条评论

0
也许你的 MessageReceived 方法应该简单地标记一个值到你的 IChannel 接口的属性上,同时实现 INotifyPropertyChanged 事件处理程序,这样当属性被更改时,你会得到通知。
通过这样做,你的 Sender 类可以循环直到最大等待时间已过或者直到 PropertyChanged 事件处理程序发生,成功中断循环。如果你的循环没有被中断,那么消息将被视为未收到。

那样做是可行的,但需要更改IChannel合同,这似乎不太理想。从设计的角度来看,我认为IChannel不应该改变,因为其实现者使用它的方式没有改变。 - Evan Barkley

0

使用 AutoResetEvent 的有用示例:

    using System;
    using System.Threading;

    class WaitOne
    {
        static AutoResetEvent autoEvent = new AutoResetEvent(false);

        static void Main()
        {
            Console.WriteLine("Main starting.");

            ThreadPool.QueueUserWorkItem(
                new WaitCallback(WorkMethod), autoEvent);

            // Wait for work method to signal.
            autoEvent.WaitOne();
            Console.WriteLine("Work method signaled.\nMain ending.");
        }

        static void WorkMethod(object stateInfo) 
        {
            Console.WriteLine("Work starting.");

            // Simulate time spent working.
            Thread.Sleep(new Random().Next(100, 2000));

            // Signal that work is finished.
            Console.WriteLine("Work ending.");
            ((AutoResetEvent)stateInfo).Set();
        }
    }

-1

WaitOne 真的是这个工作的正确工具。简而言之,您希望在等待 0 至 MaxWaitInMs 毫秒之间完成作业。你真的有两个选择,轮询完成或使用一些可以等待任意时间的结构来同步线程。

由于您非常清楚如何正确处理这个问题,为了记录,我将发布轮询版本:

MessageEventArgs msgArgs = null;
var callback = (object o, MessageEventArgs args) => {
    msgArgs = args;
};

_c.MessageReceived += callback;
_c.Send(m);

int msLeft = MaxWaitInMs;
while (msgArgs == null || msLeft >= 0) {
    Thread.Sleep(100);
    msLeft -= 100; // you should measure this instead with say, Stopwatch
}

_c.MessageRecieved -= callback;

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