wake-up-neo.net

Blockieren und Warten auf ein Ereignis

Manchmal möchte ich meinen Thread blockieren, während auf das Auftreten eines Ereignisses gewartet wird.

Ich mache es normalerweise so:

private AutoResetEvent _autoResetEvent = new AutoResetEvent(false);

private void OnEvent(object sender, EventArgs e){
  _autoResetEvent.Set();
}

// ...
button.Click += OnEvent;
try{
  _autoResetEvent.WaitOne();
}
finally{
  button.Click -= OnEvent;
}

Es scheint jedoch, dass dies etwas sein sollte, das ich einer allgemeinen Klasse entnehmen könnte (oder vielleicht sogar etwas, das bereits im Framework vorhanden ist).

Ich würde gerne so etwas tun können:

EventWaiter ew = new EventWaiter(button.Click);
ew.WaitOne();
EventWaiter ew2 = new EventWaiter(form.Closing);
ew2.WaitOne();

Ich kann jedoch keine Möglichkeit finden, eine solche Klasse zu konstruieren (ich kann keine geeignete Methode finden, um das Ereignis als Argument zu übergeben). Kann jemand helfen?

Um ein Beispiel zu geben, warum dies nützlich sein kann, betrachten Sie Folgendes:

var status = ShowStatusForm();
status.ShowInsertUsbStick();
bool cancelled = WaitForUsbStickOrCancel();
if(!cancelled){
  status.ShowWritingOnUsbStick();
  WriteOnUsbStick();
  status.AskUserToRemoveUsbStick();
  WaitForUsbStickToBeRemoved();
  status.ShowFinished();
}else{
  status.ShowCancelled();
}
status.WaitUntilUserPressesDone();

Dies ist viel prägnanter und lesbarer als der äquivalente Code, der mit der zwischen vielen Methoden verteilten Logik geschrieben wurde. Aber um WaitForUsbStickOrCancel (), WaitForUsbStickToBeRemoved und WaitUntilUserPressesDone () zu implementieren (vorausgesetzt, dass wir ein Ereignis erhalten, wenn USB-Sticks eingefügt oder entfernt werden), muss "EventWaiter" jedes Mal neu implementiert werden. Natürlich muss man darauf achten, dass dies niemals auf dem GUI-Thread ausgeführt wird, aber manchmal lohnt sich dies für den einfacheren Code.

Die Alternative würde ungefähr so ​​aussehen:

var status = ShowStatusForm();
status.ShowInsertUsbStick();
usbHandler.Inserted += OnInserted;
status.Cancel += OnCancel;
//...
void OnInserted(/*..*/){
  usbHandler.Inserted -= OnInserted;
  status.ShowWritingOnUsbStick();
  MethodInvoker mi = () => WriteOnUsbStick();
  mi.BeginInvoke(WritingDone, null);
}
void WritingDone(/*..*/){
  /* EndInvoke */
  status.AskUserToRemoveUsbStick();
  usbHandler.Removed += OnRemoved;
}
void OnRemoved(/*..*/){
  usbHandler.Removed -= OnRemoved;
  status.ShowFinished();
  status.Done += OnDone;
}
/* etc */

Ich finde das viel schwieriger zu lesen. Zugegeben, es ist bei weitem nicht immer so, dass der Fluss so linear ist, aber wenn es so ist, mag ich den ersten Stil.

Es ist vergleichbar mit ShowMessage () und Form.ShowDialog () - sie blockieren auch, bis ein "Ereignis" auftritt (obwohl sie eine Nachrichtenschleife ausführen, wenn sie im gui-thread aufgerufen werden).

35
Rasmus Faber

Bestehen Sie nicht das Ereignis, sondern einen Delegaten, der der Signatur des Ereignishandlers entspricht. Das klingt für mich eigentlich hackig, seien Sie sich also der möglichen Probleme mit dem Deadlock bewusst.

3

Ich habe die Klasse EventWaiter von Dead.Rabit für den Umgang mit EventHandler<T> geändert. Sie können also für das Warten auf alle Ereignistypen den Typ EventHandler<T> verwenden. Dies bedeutet, dass Ihr Delegat so etwas wie delegate void SomeDelegate(object sender, T EventsArgs) ist.

 public class EventWaiter<T>
{

    private AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
    private EventInfo _event = null;
    private object _eventContainer = null;

    public EventWaiter(object eventContainer, string eventName)
    {
        _eventContainer = eventContainer;
        _event = eventContainer.GetType().GetEvent(eventName);
    }

    public void WaitForEvent(TimeSpan timeout)
    {
        EventHandler<T> eventHandler = new EventHandler<T>((sender, args) => { _autoResetEvent.Set(); });
        _event.AddEventHandler(_eventContainer, eventHandler);
        _autoResetEvent.WaitOne(timeout);
        _event.RemoveEventHandler(_eventContainer, eventHandler);
    }
}

Und zum Beispiel verwende ich das für das Warten auf Url von HttpNotificationChannel, wenn ich mich beim Windows Push-Benachrichtigungsdienst registriere.

            HttpNotificationChannel pushChannel = new HttpNotificationChannel(channelName);
            //ChannelUriUpdated is event 
            EventWaiter<NotificationChannelUriEventArgs> ew = new EventWaiter<NotificationChannelUriEventArgs>(pushChannel, "ChannelUriUpdated");
            pushChannel.Open();
            ew.WaitForEvent(TimeSpan.FromSeconds(30));

Sie können dies auch versuchen:

class EventWaiter<TEventArgs> where TEventArgs : EventArgs
{
    private readonly Action<EventHandler<TEventArgs>> _unsubHandler;
    private readonly Action<EventHandler<TEventArgs>> _subHandler;

    public EventWaiter(Action<EventHandler<TEventArgs>> subHandler, Action<EventHandler<TEventArgs>> unsubHandler)
    {
        _unsubHandler = unsubHandler;
        _subHandler = subHandler;
    }

    protected void Handler(object sender, TEventArgs args)
    {
        _unsubHandler.Invoke(Handler);
        TaskCompletionSource.SetResult(args);
    }

    public  TEventArgs WaitOnce()
    {
        TaskCompletionSource = new TaskCompletionSource<TEventArgs>();
        _subHandler.Invoke(Handler);
        return TaskCompletionSource.Task.Result;
    }

    protected TaskCompletionSource<TEventArgs> TaskCompletionSource { get; set; } 

}

Verwendungszweck:

EventArgs eventArgs = new EventWaiter<EventArgs>((h) => { button.Click += new EventHandler(h); }, (h) => { button.Click -= new EventHandler(h); }).WaitOnce();
0
theres

Ich habe ein funktionierendes Beispiel in LinqPad mithilfe von Reflektion zusammengestellt und einen Verweis auf das EventInfo-Objekt mit einer Zeichenfolge abgerufen (seien Sie vorsichtig, wenn Sie die Kompilierzeitprüfung verlieren). Das offensichtliche Problem ist, dass es keine Garantie gibt, dass ein Ereignis jemals ausgelöst wird, oder dass das Ereignis, das Sie erwarten, möglicherweise ausgelöst wird, bevor die EventWaiter-Klasse zum Blockieren bereit ist eine Produktions-App.

void Main()
{
    Console.WriteLine( "main thread started" );

    var workerClass = new WorkerClassWithEvent();
    workerClass.PerformWork();

    var waiter = new EventWaiter( workerClass, "WorkCompletedEvent" );
    waiter.WaitForEvent( TimeSpan.FromSeconds( 10 ) );

    Console.WriteLine( "main thread continues after waiting" );
}

public class WorkerClassWithEvent
{
    public void PerformWork()
    {
        var worker = new BackgroundWorker();
        worker.DoWork += ( s, e ) =>
        {
            Console.WriteLine( "threaded work started" );
            Thread.Sleep( 1000 ); // <= the work
            Console.WriteLine( "threaded work complete" );
        };
        worker.RunWorkerCompleted += ( s, e ) =>
        {
            FireWorkCompletedEvent();
            Console.WriteLine( "work complete event fired" );
        };

        worker.RunWorkerAsync();
    }

    public event Action WorkCompletedEvent;
    private void FireWorkCompletedEvent()
    {
        if ( WorkCompletedEvent != null ) WorkCompletedEvent();
    }
}

public class EventWaiter
{
    private AutoResetEvent _autoResetEvent = new AutoResetEvent( false );
    private EventInfo _event               = null;
    private object _eventContainer         = null;

    public EventWaiter( object eventContainer, string eventName )
    {
        _eventContainer = eventContainer;
        _event = eventContainer.GetType().GetEvent( eventName );
    }

    public void WaitForEvent( TimeSpan timeout )
    {
        _event.AddEventHandler( _eventContainer, (Action)delegate { _autoResetEvent.Set(); } );
        _autoResetEvent.WaitOne( timeout );
    }
}

Ausgabe

// main thread started
// threaded work started
// threaded work complete
// work complete event fired
// main thread continues after waiting
0
Dead.Rabit