如何在C#中阻塞直到事件被触发

78

在提出这个问题之后,我想知道是否可能等待事件被触发,然后获取事件数据并返回其中的一部分。就像这样:

private event MyEventHandler event;
public string ReadLine(){ return event.waitForValue().Message; }
...
event("My String");
...elsewhere...
var resp = ReadLine();
请确保您提供的任何解决方案直接返回值,而不是从其他地方获取它。我想知道上面的方法是否可用。我知道Auto/ManuelResetEvent,但我不知道它们像我上面做的那样直接返回值。
更新:我使用MyEventHandler(其中包含一个Message字段)声明了一个事件。我有另一个线程中的一个名为ReadLine的方法在等待事件触发。当事件触发时,WaitForValue方法(事件处理场景的一部分)将返回事件参数,其中包含消息。然后ReadLine将消息返回给调用它的任何内容。 问题被接受的答案就是我所做的,但感觉不太对劲。几乎感觉数据可能会在ManuelResetEvent触发和程序检索数据并返回它之间发生变化。
更新:Auto/ManualResetEvent的主要问题是它太容易受攻击。线程可以等待事件,然后在其他人获取它之前不给足够时间来更改它为其他东西。有没有一种使用锁或其他东西的方式?也许使用get和set语句。

12
这是“积极等待”,但这是一个很糟糕的解决方案。 - george.zakaryan
可能是在方法内等待事件被捕获的重复问题。 - Matyas
3
除了这个问题比那个更早之外,没有其他不同 :) - Arlen Beiler
1
@Vahid,自从我提出这个问题以来,我又有了7年的编程经验,我想说,如果你必须这样做,那么你可能做错了。最好指定一个回调函数。如果你正在使用不允许其他任何东西的旧系统,比如COM,那么只需让代码等待ManualResetEvent并设置第二个事件来保护写入,直到所有线程都完成读取即可。 - Arlen Beiler
1
我也建议您查看http://reactivex.io/。 - Arlen Beiler
显示剩余7条评论
5个回答

67

如果当前方法是异步的,那么您可以使用TaskCompletionSource。创建一个字段,使事件处理程序和当前方法可以访问该字段。

    TaskCompletionSource<bool> tcs = null;

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        tcs = new TaskCompletionSource<bool>();
        await tcs.Task;
        WelcomeTitle.Text = "Finished work";
    }

    private void Button_Click2(object sender, RoutedEventArgs e)
    {
        tcs?.TrySetResult(true);
    }

这个例子使用了一个表单,其中有一个名为WelcomeTitle的文本块和两个按钮。当点击第一个按钮时,它会开始执行点击事件,但会停留在await行。当点击第二个按钮时,任务完成并更新WelcomeTitle文本。如果你想要添加超时功能,可以进行更改。

await tcs.Task;

await Task.WhenAny(tcs.Task, Task.Delay(25000));
if (tcs.Task.IsCompleted)
    WelcomeTitle.Text = "Task Completed";
else
    WelcomeTitle.Text = "Task Timed Out";

3
不需要使用async就可以使用TaskCompletionSource。 - Felix Keil
2
没错,但如果你想在不阻塞用户界面的情况下等待任务完成,就需要它。 - Adam
1
@KyleDelaney 不是的。在我的例子中,您将使用新实例替换tcs。 - Adam
@Adam 谢谢,这太棒了。还要感谢你的等待策略。 将任务分配给完成源是否类似于订阅事件? - user3625699
在ButtonClick中分配新的TaskCompletionSource<bool>()而不是直接在字段中分配的目的是什么? - user3625699
显示剩余3条评论

47

你可以使用ManualResetEvent。在触发辅助线程之前重置事件,然后使用WaitOne()方法阻止当前线程。然后,辅助线程可以设置ManualResetEvent,这将使主线程继续。代码示例:

ManualResetEvent oSignalEvent = new ManualResetEvent(false);

void SecondThread(){
    //DoStuff
    oSignalEvent.Set();
}

void Main(){
    //DoStuff
    //Call second thread
    System.Threading.Thread oSecondThread = new System.Threading.Thread(SecondThread);
    oSecondThread.Start();

    oSignalEvent.WaitOne(); //This thread will block here until the reset event is sent.
    oSignalEvent.Reset();
    //Do more stuff
}

这不会直接返回值。我已经在另一个问题中尝试过了。难道没有像我所问的那样的东西吗? - Arlen Beiler

4
一个非常简单的事件类型,你可以等待的是ManualResetEvent,甚至更好的是ManualResetEventSlim。它们都有一个WaitOne()方法,可以完美地实现这个功能。你可以一直等待,或者设置超时时间,或者"取消令牌",这是您决定停止等待事件的一种方式(if you want to cancel your work, or your app is asked to exit)。你通过调用Set()方法来触发它们。点击这里查看文档。

1
但是这并不返回任何类型的值。 - Arlen Beiler
1
你能否编辑一下问题并添加不同组件及其交互的概述?这将有助于我们更好地了解您想要实现的目标,并希望能够导致改进的设计。 - Mike

1
如果您愿意使用Microsoft Reactive Extensions,那么这个可以很好地工作:
public class Foo
{
    public delegate void MyEventHandler(object source, MessageEventArgs args);
    public event MyEventHandler _event;
    public string ReadLine()
    {
        return Observable
            .FromEventPattern<MyEventHandler, MessageEventArgs>(
                h => this._event += h,
                h => this._event -= h)
            .Select(ep => ep.EventArgs.Message)
            .First();
    }
    public void SendLine(string message)
    {
        _event(this, new MessageEventArgs() { Message = message });
    }
}

public class MessageEventArgs : EventArgs
{
    public string Message;
}

我可以这样使用它:
var foo = new Foo();

ThreadPoolScheduler.Instance
    .Schedule(
        TimeSpan.FromSeconds(5.0),
        () => foo.SendLine("Bar!"));

var resp = foo.ReadLine();

Console.WriteLine(resp);

我需要在不锁定的情况下在不同的线程上调用SendLine消息,但这段代码表明它按预期工作。


-1
您可以设置 "e.Handled = true"。
private void TextBox1_KeyDown(object sender, KeyEventArgs e){
e.Handled = true; 

//your code here;

}

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