How do I make an eventhandler run asynchronously?
Asked Answered
O

7

53

I am writing a Visual C# program that executes a continuous loop of operations on a secondary thread. Occasionally when that thread finishes a task I want it to trigger an eventhandler. My program does that but the when the event handler is triggered, the secondary thread waits until the event handler is finished before continuing the thread. How do I make it continue? Here is the way I currently have it structured...

class TestClass 
{
  private Thread SecondaryThread;
  public event EventHandler OperationFinished;

  public void StartMethod()
  {
    ...
    SecondaryThread.Start();      //start the secondary thread
  }

  private void SecondaryThreadMethod()
  {
    ...
    OperationFinished(null, new EventArgs());
    ...  //This is where the program waits for whatever operations take
         //place when OperationFinished is triggered.
  }

}

This code is part of an API for one of my devices. When the OperationFinished event is triggered I want the client application to be able to do whatever it needs to (i.e. update the GUI accordingly) without haulting the API operation.

Also, if I do not want to pass any parameters to the event handler is my syntax correct by using OperationFinished(null, new EventArgs()) ?

Onehorse answered 16/12, 2009 at 17:3 Comment(1)
Which thread do you want the OperationFinished event to be raised on? It cannot be your secondary thread, since you explicitly require not to block it. Does it have to be the primary thread, then, or are you fine with it being raised on a different thread newly created just for the purpose of async callback?Druid
E
69

So you want to raise the event in a manner that prevents the listeners from blocking the background thread? Gimme a couple minutes to whip up an example; it's pretty simple :-)

Here we go: first an important note! Whenever you call BeginInvoke you must call the corresponding EndInvoke, otherwise if the invoked method threw an exception or returned a value then the ThreadPool thread will never be released back to the pool, resulting in a thread-leak!

class TestHarness
{

    static void Main(string[] args)
    {
        var raiser = new SomeClass();

        // Emulate some event listeners
        raiser.SomeEvent += (sender, e) => { Console.WriteLine("   Received event"); };
        raiser.SomeEvent += (sender, e) =>
        {
            // Bad listener!
            Console.WriteLine("   Blocking event");
            System.Threading.Thread.Sleep(5000);
            Console.WriteLine("   Finished blocking event");
        };

        // Listener who throws an exception
        raiser.SomeEvent += (sender, e) =>
        {
            Console.WriteLine("   Received event, time to die!");
            throw new Exception();
        };

        // Raise the event, see the effects
        raiser.DoSomething();

        Console.ReadLine();
    }
}

class SomeClass
{
    public event EventHandler SomeEvent;

    public void DoSomething()
    {
        OnSomeEvent();
    }

    private void OnSomeEvent()
    {
        if (SomeEvent != null)
        {
            var eventListeners = SomeEvent.GetInvocationList();

            Console.WriteLine("Raising Event");
            for (int index = 0; index < eventListeners.Count(); index++)
            {
                var methodToInvoke = (EventHandler)eventListeners[index];
                methodToInvoke.BeginInvoke(this, EventArgs.Empty, EndAsyncEvent, null);
            }
            Console.WriteLine("Done Raising Event");
        }
    }

    private void EndAsyncEvent(IAsyncResult iar)
    {
        var ar = (System.Runtime.Remoting.Messaging.AsyncResult)iar;
        var invokedMethod = (EventHandler)ar.AsyncDelegate;

        try
        {
            invokedMethod.EndInvoke(iar);
        }
        catch
        {
            // Handle any exceptions that were thrown by the invoked method
            Console.WriteLine("An event listener went kaboom!");
        }
    }
}
Escalera answered 16/12, 2009 at 17:26 Comment(9)
Why not just call the multicast delegate directly, rather than using GetInvocationList?Feeler
How would you call the event listeners asynchronously just using that? Granted, you could call all listeners on a seperate single thread--my solution does take it to the level of calling each listener on their own thread--so I could see it being overkill.Escalera
The way I had originally written mine, if there was no method to handle the event in the client app (no listeners) the client app would throw an exception. Do you prevent that from happening by using that for loop that loops through the eventListeners?Onehorse
Alright, I tried this approach and it works great! Thanks for the help!Onehorse
@ Yoooder: Can you please explain what EndAsyncEvent actually does. If I have multiple events that are being triggered this way can I use the same EndAsyncEvent for all of them or does each event need it's on corresponding EndAsyncEvent method????Onehorse
@Jordan: Read what I wrote above the code sample; EndAsyncEvent() exists entirely to ensure that EndInvoke() is called. Not calling EndInvoke() can cause headaches like ThreadPoolEscalera
@Jordan: sorry for not answering the second part of your question. The example above will work for all void delegates since Delegate.EndInvoke() will not return a value. For delegates with a return type then there will need to be 1 EndAsyncEvent() method per return type.Escalera
OP mentions updating the GUI; That will need something like " System.Windows.Application.Current.Dispatcher.BeginInvoke(methodToInvoke, null, EventArgs.Empty);" to fire the function on the GUI thread. That also means EndAsyncEvent() is not used, right?Caves
what about in .net core ? is there alternative ? System.PlatformNotSupportedException: Operation is not supported on this platform.Misuse
W
22

With the Task Parallel Library it is now possible to do the following:

Task.Factory.FromAsync( ( asyncCallback, @object ) => this.OperationFinished.BeginInvoke( this, EventArgs.Empty, asyncCallback, @object ), this.OperationFinished.EndInvoke, null );
Wil answered 2/5, 2013 at 11:0 Comment(3)
Works great, thanks for reminding the TPL's FromAsync method!Doyledoyley
@FactorMytic Do you know where I could read more about why it doesn't work in that case?Hadden
@Hadden A bit late to the party, but BeginInvoke throws when invoked upon multicast delegate: stackoverflow.com/questions/4731061/…Cilla
I
12

Also, if I do not want to pass any parameters to the event handler is my syntax correct by using OperationFinished(null, new EventArgs()) ?

No. Typically, you would call it as:

OperationFinished(this, EventArgs.Empty);

You should always pass an object as a sender - it's expected in the pattern (although typically ignored). EventArgs.Empty is better than new EventArgs(), as well.

In order to fire this in a separate thread, the easiest option is probably to just use the thread pool:

private void RaiseOperationFinished()
{
       ThreadPool.QueueUserWorkItem( new WaitCallback( (s) =>
           {
              if (this.OperationFinished != null)
                   this.OperationFinished(this, EventArgs.Empty);
           }));
}

That being said, raising an event on a separate thread is something that should be thoroughly documented, as it will potentially cause unexpected behavior.

Iphigenia answered 16/12, 2009 at 17:13 Comment(2)
@beruic Agreed. This was written in 2009 ;)Iphigenia
I know this is an old answer, but curious as to the benefit to using Task.Run over QueueUserWorkItem? Also, if one wanted to squeeze the most performance possible out of it, UnsafeQueueUserWorkItemis faster and the only thing we lose, if I understand it correctly, is CAS (Code Access Security) (see a great answer by Hans Passant here regarding UnsafeQueueUserWorkItem), which further decreases the time between the event being raised and the time your event handler actually runsGibert
F
6

Try the BeginInvoke and EndInvoke methods on the event delegate - these return immediately, and allow you to use polling, a wait handle or a callback function to notify you when the method has completed. See here for an overview; in your example, the event is the delegate you'll be using

Feeler answered 16/12, 2009 at 17:17 Comment(1)
I'm not sure this is a naming issue (what you mean with "the event delegate"), but DONT use BeginInvoke on the event field. You cannot call BeginInvoke on multicast delegates. Ie: BeginInvoke is not the asynchronous Invoke sub.Clothesline
C
4

Maybe Method2 or Method3 below can help :)

public partial class Form1 : Form
{
    private Thread SecondaryThread;

    public Form1()
    {
        InitializeComponent();

        OperationFinished += callback1;
        OperationFinished += callback2;
        OperationFinished += callback3;
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        SecondaryThread = new Thread(new ThreadStart(SecondaryThreadMethod));
        SecondaryThread.Start();
    }

     private void SecondaryThreadMethod()
     {
        Stopwatch sw = new Stopwatch();
        sw.Restart();

        OnOperationFinished(new MessageEventArg("test1"));
        OnOperationFinished(new MessageEventArg("test2"));
        OnOperationFinished(new MessageEventArg("test3"));
        //This is where the program waits for whatever operations take
             //place when OperationFinished is triggered.

        sw.Stop();

        Invoke((MethodInvoker)delegate
        {
            richTextBox1.Text += "Time taken (ms): " + sw.ElapsedMilliseconds + "\n";
        });
     }

    void callback1(object sender, MessageEventArg e)
    {
        Thread.Sleep(2000);
        Invoke((MethodInvoker)delegate
        {
            richTextBox1.Text += e.Message + "\n";
        });
    }
    void callback2(object sender, MessageEventArg e)
    {
        Thread.Sleep(2000);
        Invoke((MethodInvoker)delegate
        {
            richTextBox1.Text += e.Message + "\n";
        });
    }

    void callback3(object sender, MessageEventArg e)
    {
        Thread.Sleep(2000);
        Invoke((MethodInvoker)delegate
        {
            richTextBox1.Text += e.Message + "\n";
        });
    }

    public event EventHandler<MessageEventArg> OperationFinished;

    protected void OnOperationFinished(MessageEventArg e)
    {
        //##### Method1 - Event raised on the same thread ##### 
        //EventHandler<MessageEventArg> handler = OperationFinished;

        //if (handler != null)
        //{
        //    handler(this, e);
        //}

        //##### Method2 - Event raised on (the same) separate thread for all listener #####
        //EventHandler<MessageEventArg> handler = OperationFinished;

        //if (handler != null)
        //{
        //    Task.Factory.StartNew(() => handler(this, e));
        //}

        //##### Method3 - Event raised on different threads for each listener #####
        if (OperationFinished != null)
        {
            foreach (EventHandler<MessageEventArg> handler in OperationFinished.GetInvocationList())
            {
                Task.Factory.FromAsync((asyncCallback, @object) => handler.BeginInvoke(this, e, asyncCallback, @object), handler.EndInvoke, null);
            }
        }
    }
}

public class MessageEventArg : EventArgs
{
    public string Message { get; set; }

    public MessageEventArg(string message)
    {
        this.Message = message;
    }
}

}

Crowning answered 24/2, 2015 at 4:36 Comment(0)
P
0

Look at the BackgroundWorker class. I think it does exactly what you are asking for.

EDIT: What I think you are asking is how to fire an event when only a small part of the overall background task is complete. BackgroundWorker provides an event called "ProgressChanged" that allows you to report back to the main thread that some portion of the overall process is complete. Then, when all of the async work is complete, it raises the "RunWorkerCompleted" event.

Potoroo answered 16/12, 2009 at 17:8 Comment(2)
Not sure how BackgroundWorker helps in this situation. Granted, it's a great option for pushing work into a separate thread when you need notifications, but in this case, it's just a simple work item to push the handler into a separate thread...Iphigenia
If i was writing the client application, I could have the method that updates the GUI run in a backgroundworker and that would stop the call to OperationFinished() from blocking, but as I am not writing the client app I cannot do that. Are you saying that my call to OpeartionFinished() should be within a backgroundworker?Onehorse
O
0

I prefer to define a method that I pass to the child thread as a delegate which updates the UI. First define a delegate:

public delegate void ChildCallBackDelegate();

In the child thread define a delegate member:

public ChildCallbackDelegate ChildCallback {get; set;}

In the calling class define the method that updates the UI. You'll need to wrap it in the target control's dispatcher since its being called from a separate thread. Note the BeginInvoke. In this context EndInvoke isn't required:

private void ChildThreadUpdater()
{
  yourControl.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background
    , new System.Threading.ThreadStart(delegate
      {
        // update your control here
      }
    ));
}

Before you launch your child thread, set its ChildCallBack property:

theChild.ChildCallBack = new ChildCallbackDelegate(ChildThreadUpdater);

Then when the child thread wants to update the parent:

ChildCallBack();
Omnibus answered 16/12, 2009 at 23:29 Comment(6)
Can you cite sources to back up that EndInvoke() is not required? My understanding is that it's always good practice to ensure it's called as threading resources are not necessarily freed without the call under specific circumstances. Also, is there a reason you opt to use a ThreadStart rather than the (relatively) performant ThreadPool? Lastly; this solution handles updating the UI, but I don't think the OP's question was limited to that--it doesn't solve the broader issue of raising events asynchronously.Escalera
Jon Skeet said it best: stackoverflow.com/questions/229554/…: "Note that the Windows Forms team has guaranteed that you can use Control.BeginInvoke in a "fire and forget" manner - i.e. without ever calling EndInvoke. This is not true of async calls in general: normally every BeginXXX should have a corresponding EndXXX call, usually in the callback." Also note that at least with WPF, there is no Dispatcher.EndInvoke method.Omnibus
I made my solution update the UI because that's what the OP specified: "When the OperationFinished event is triggered I want the client application to be able to do whatever it needs to (i.e. update the GUI accordingly) without haulting the API operation."Omnibus
ThreadPool is fine if you don't have too many threads, you want to avoid the overhead of spawning a separate thread, the thread life is relatively short and the thread is CPU-intensive. All of my recent work with threads involves lots of simultaneous network connections where the ThreadStart overhead is inconsequential and I want to have lots of threads. I've also never liked the idea of a full threadpool.Omnibus
@ebpower: Ahh! Control.BeginInvoke() is an entirely different animal from Delegate.BeginInvoke() which is where I was getting mixed up. Your solution is solid for just updating UI controls then, but it still doesn't dispatch the event to all listeners asynchronously--instead it just ensures the UI updates on the correct thread.Escalera
Thanks for the comments. But where did he require multiple listeners? He's just updating the UI.Omnibus

© 2022 - 2024 — McMap. All rights reserved.