Yes, they are synchronous.
To answer your questions:
- Raising an event does block the thread if the event handlers are all implemented synchronously.
- The event handlers are executed sequentially, one after another, in the order they are subscribed to the event.
I too was curious about the internal mechanism of event
and its related operations. So I wrote a simple program and used ildasm
to poke around its implementation.
The short answer is
- there's no asynchronous operation involved in subscribing or invoking the events.
- event is implemented with a backing delegate field of the same delegate type
- subscribing is done with
Delegate.Combine()
- unsubscribing is done with
Delegate.Remove()
- Invoking is done by simply invoking the final combined delegate
Here's what I did. The program I used:
public class Foo
{
// cool, it can return a value! which value it returns if there're multiple
// subscribers? answer (by trying): the last subscriber.
public event Func<int, string> OnCall;
private int val = 1;
public void Do()
{
if (OnCall != null)
{
var res = OnCall(val++);
Console.WriteLine($"publisher got back a {res}");
}
}
}
public class Program
{
static void Main(string[] args)
{
var foo = new Foo();
foo.OnCall += i =>
{
Console.WriteLine($"sub2: I've got a {i}");
return "sub2";
};
foo.OnCall += i =>
{
Console.WriteLine($"sub1: I've got a {i}");
return "sub1";
};
foo.Do();
foo.Do();
}
}
Here's Foo's implementation:
Note that there is a field OnCall
and an event OnCall
. The field OnCall
is obviously the backing property. And it's merely a Func<int, string>
, nothing fancy here.
Now the interesting parts are:
add_OnCall(Func<int, string>)
remove_OnCall(Func<int, string>)
- and how
OnCall
is invoked in Do()
How is Subscribing and Unsubscribing Implemented?
Here's the abbreviated add_OnCall
implementation in CIL. The interesting part is it uses Delegate.Combine
to concatenate two delegates.
.method public hidebysig specialname instance void
add_OnCall(class [mscorlib]System.Func`2<int32,string> 'value') cil managed
{
// ...
.locals init (class [mscorlib]System.Func`2<int32,string> V_0,
class [mscorlib]System.Func`2<int32,string> V_1,
class [mscorlib]System.Func`2<int32,string> V_2)
IL_0000: ldarg.0
IL_0001: ldfld class [mscorlib]System.Func`2<int32,string> ConsoleApp1.Foo::OnCall
// ...
IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
// ...
} // end of method Foo::add_OnCall
Likewise, Delegate.Remove
is used in remove_OnCall
.
How is an event invoked?
To invoke OnCall
in Do()
, it simply calls the final concatenated delegate after loading the arg:
IL_0026: callvirt instance !1 class [mscorlib]System.Func`2<int32,string>::Invoke(!0)
How exactly does a subscriber subscribe to an event?
And finally, in Main
, not suprisingly, subscribing to the OnCall
event is done by calling add_OnCall
method on the Foo
instance.