AsyncEventHandlers 3.0.1

dotnet add package AsyncEventHandlers --version 3.0.1                
NuGet\Install-Package AsyncEventHandlers -Version 3.0.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AsyncEventHandlers" Version="3.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add AsyncEventHandlers --version 3.0.1                
#r "nuget: AsyncEventHandlers, 3.0.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install AsyncEventHandlers as a Cake Addin
#addin nuget:?package=AsyncEventHandlers&version=3.0.1

// Install AsyncEventHandlers as a Cake Tool
#tool nuget:?package=AsyncEventHandlers&version=3.0.1                

AsyncEventHandlers

This library provides faster and true thread-safe asynchronus event handlers for .NET.

Using the AsyncEventHandler struct for events is about ~96% faster than using the built-in event handlers. For more details check out the Benchmarks section.

Setup

Just install the latest release from NuGet or from the Packages tab. Alternatively you can download it from the Releases tab aswell.

Usage

Note that when registering/unregistering a lot from different threads and using many events, consider using AsyncEventHandler instead of AsyncEventHandlerDelegate as the first one is about ~96% faster than the latter and doesn't use the built-in non thread-safe registering/unregistering mechanism.

Using the AsyncEventHandler struct

The AsyncEventHandler type is completely thread-safe, no matter how many events are registered/unregistered from different threads. It can almost be used as the delegate described before apart from the declaration.

Declaring an event:

private readonly AsyncEventHandler myEvent = new AsyncEventHandler();
public event AsyncEvent MyEvent
{
    add { myEvent.Register(value); }
    remove { myEvent.Unregister(value); }
}

You can also just expose the AsyncEventHandler struct directly but that way it is not treated as an C# event and subscribers outside of your class can even invoke your event.

public AsyncEventHandler MyEvent = new AsyncEventHandler();
public AsyncEventHandler AnotherEvent;  // You can even do this!

// Or with custom event args:
public class MyType
{
    public string? Message { get; set; }
}

public AsyncEventHandler<MyType> MyCustomEvent = new AsyncEventHandler<MyType>();

Registering/Unregistering to the event:

class WebsocketServer
{
    public AsyncEventHandler<MyType> MyExposedEvent = new AsyncEventHandler<MyType>();

    private readonly AsyncEventHandler<MyType> myEvent = new AsyncEventHandler<MyType>();
    public event AsyncEvent<MyType> MyEvent
    {
        add { myEvent.Register(value); }
        remove { myEvent.Unregister(value); }
    }
}

var ws = new WebsocketServer();
// Event property
ws.MyEvent += Ws_MyEvent;   // Register
ws.MyEvent -= Ws_MyEvent;   // Unregister
// Using the struct directly
ws.MyExposedEvent += Ws_MyEvent;   // Register
ws.MyExposedEvent -= Ws_MyEvent;   // Unregister
// Or use the methods
ws.MyExposedEvent.Register(Ws_MyEvent);
ws.MyExposedEvent.Unregister(Ws_MyEvent);

static Task Ws_MyEvent(MyType data, CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        Console.WriteLine("The cancellation token was cancelled!");
    }

    return Task.CompletedTask;
}

Invoking the event:
Note: Always call InvokeAsync on the AsyncEventHandler struct itself, not on the event!

try
{
    await myEvent.InvokeAsync(new MyType());
    // or with a cancellation token
    var cts = new CancellationTokenSource();
    await myEvent.InvokeAsync(new MyType(), cts.Token);
}
catch (OperationCanceledException)
{
    // Cancellation token was cancelled
}
catch (ObjectDisposedException)
{
    // Cancellation token was disposed
}
catch (Exception)
{
    // Some registered event(s) have thrown an exception
}

The AsyncEventHandlerDelegate delegate is basically being used like the built-in EventHandler type apart from the invocation. See below.

Declaring an event:

public event AsyncEventHandlerDelegate? MyEvent;

// Or with custom event args:
public class MyAsyncEventArgs : AsyncEventArgs
{
    public string? Message { get; set; }
}

public event AsyncEventHandlerDelegate<MyAsyncEventArgs>? MyCustomEvent;

Registering/Unregistering to the event:

class WebsocketServer
{
    public event AsyncEventHandlerDelegate? MyEvent;
}

var ws = new WebsocketServer();
ws.MyEvent += Ws_MyEvent;   // Register
ws.MyEvent -= Ws_MyEvent;   // Unregister

static Task Ws_MyEvent(object? sender, AsyncEventArgs e)
{
    if (e.CancellationToken.IsCancellationRequested)
    {
        Console.WriteLine("The cancellation token was cancelled!");
    }

    return Task.CompletedTask;
}

Invoking the event:

try
{
    await MyEvent.InvokeAsync(this, new AsyncEventArgs());
    // or with a cancellation token
    var cts = new CancellationTokenSource();
    await MyEvent.InvokeAsync(this, new AsyncEventArgs(), cts.Token);
}
catch (OperationCanceledException)
{
    // Cancellation token was cancelled
}
catch (ObjectDisposedException)
{
    // Cancellation token was disposed
}
catch (Exception)
{
    // Some registered event(s) have thrown an exception
}

Sample projects

More samples can be found in the Samples directory.

Benchmarks

The benchmarks can be found in the AsyncEventHandlers.Benchmarks directory.

Data

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1645 (21H1/May2021Update)
Intel Core i7-7700K CPU 4.20GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100
  [Host]     : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT  [AttachedDebugger]
  DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT


|            Method |         Mean |       Error |      StdDev |
|------------------ |-------------:|------------:|------------:|
|     Struct_Call_1 |     107.6 ns |     1.20 ns |     1.12 ns |
|   Delegate_Call_1 |   3,242.8 ns |    34.12 ns |    30.25 ns |
|   Struct_Call_100 |   9,466.6 ns |   114.86 ns |   101.82 ns |
| Delegate_Call_100 | 344,683.2 ns | 2,627.63 ns | 2,457.89 ns |

Explanation

The Struct_Call_1 and Struct_Call_100 methods use the AsyncEventHandler struct for async events. The other methods use the delegate approach.

Call_1 invokes all 100 registered events async, Call_100 does the same with the only difference being that the method calls InvokeAsync 100 times.

Speed improvements

Struct_Call_1 vs. Delegate_Call_1:
100 - ((107.6/3242.8) * 100) = 96.6818799 = 96.68%

Struct_Call_100 vs. Delegate_Call_100:
100 - ((9466.6/344683.2) * 100) = 97.253536 = 97,25%

As you can see, the struct approach is about 96%-97% faster!

License

AsyncEventHandlers is licensed under The Unlicense, see LICENSE.txt for more information.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on AsyncEventHandlers:

Package Downloads
ZCStudio.CoreLib.IO.Ports

CoreLib

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
3.0.1 5,352 12/3/2022
2.0.0 566 5/3/2022
1.0.1 468 3/22/2022
1.0.0 439 3/22/2022