Zero-allocation, extremely fast in-memory messaging library for .NET and Unity.
English | 日本語
Zero Messengerは.NET/Unity向けのハイパフォーマンスなメッセージングライブラリです。購読/購読解除が容易なイベントとしてMessageBroker<T>が利用可能なほか、IMessagePublisher<T>/IMessageSubscriber<T>を用いたPub/Subパターンの実装をサポートします。
Zero Messengerはパフォーマンスを重視して実装されており、Publish()はMessagePipeやVitalRouterなどのライブラリよりもさらに高速に動作します。もちろん、Publish時のアロケーションは一切ありません。
また、メッセージのパイプラインを構築する際のアロケーションも他のライブラリと比べて少なくなるように設計されています。以下はSubscribe/Disposeを10000回実行するベンチマークの結果です。
Zero Messengerを利用するには.NET Standard2.1以上が必要です。パッケージはNuGetから入手できます。
dotnet add package ZeroMessengerInstall-Package ZeroMessengerNugetForUnityを用いることでUnityでZero Messengerを利用可能です。詳細はUnityの項目を参照してください。
MessageBroker<T>.Defaultを用いることでグローバルなPub/Subの実装を簡単に行うことができます。
using System;
using ZeroMessenger;
// メッセージを購読
var subscription = MessageBroker<Message>.Default.Subscribe(x =>
{
Console.WriteLine(x.Text);
});
// メッセージを発行
MessageBroker<Message>.Default.Publish(new Message("Hello!"));
// 購読を解除
subscription.Dispose();
// メッセージに使用する型
public record struct Message(string Text) { }また、MessageBroker<T>のインスタンスはeventやRxのSubject<T>のように利用することも可能です。
var broker = new MessageBroker<int>();
broker.Subscribe(x =>
{
Console.WriteLine(x);
});
broker.Publish(10);
broker.Dispose();DIコンテナ上にZero Messengerを追加することで、サービス間でのPub/Subを簡単に行うことができます。
Zero MessengerはMicrosoft.Extensions.DependencyInjection上でのPub/Subをサポートしています。これにはZeroMessenger.DependencyInjectionパッケージが必要です。
dotnet add package ZeroMessenger.DependencyInjectionInstall-Package ZeroMessenger.DependencyInjectionservices.AddZeroMessenger()を追加することでIServiceCollectionにZero Messengerを登録できます。以下のコードは、Generic Host上でPub/Subを実装するサンプルです。
using ZeroMessenger;
using ZeroMessenger.DependencyInjection;
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Zero Messengerを追加する
services.AddZeroMessenger();
services.AddSingleton<ServiceA>();
services.AddSingleton<ServiceB>();
})
.Build()
.Run();
public record struct Message(string Text) { }
public class ServiceA
{
IMessagePublisher<Message> publisher;
public ServiceA(IMessagePublisher<Message> publisher)
{
this.publisher = publisher;
}
public Task SendAsync(CancellationToken cancellationToken = default)
{
publisher.Publish(new Message("Hello!"));
}
}
public class ServiceB : IDisposable
{
IDisposable subscription;
public ServiceB(IMessageSubscriber<Message> subscriber)
{
subscription = subscriber.Subscribe(x =>
{
Console.WriteLine(x);
});
}
public void Dispose()
{
subscription.Dispose();
}
}Pub/Subに利用されるインターフェースはIMessagePublisher<T>とIMessageSubscriber<T>です。MessageBroker<T>はこの両方を実装しています。
public interface IMessagePublisher<T>
{
void Publish(T message, CancellationToken cancellationToken = default);
ValueTask PublishAsync(T message, AsyncPublishStrategy publishStrategy = AsyncPublishStrategy.Parallel, CancellationToken cancellationToken = default);
}
public interface IMessageSubscriber<T>
{
IDisposable Subscribe(MessageHandler<T> handler);
IDisposable SubscribeAwait(AsyncMessageHandler<T> handler, AsyncSubscribeStrategy subscribeStrategy = AsyncSubscribeStrategy.Sequential);
}IMessagePublisher<T>はメッセージの発行を行うためのインターフェースです。Publish()でメッセージを発行できるほか、PublishAsync()を用いることですべての処理が終わるまで待機することが可能です。
IMessagePublisher<Message> publisher;
// メッセージを発行する (Fire-and-forget)
publisher.Publish(new Message("Foo!"));
// メッセージを発行し、すべてのSubscriberの処理が終わるまで待機する
await publisher.PublishAsync(new Message("Bar!"), AsyncPublishStrategy.Parallel, cancellationToken);AsyncPublishStrategyを指定することで非同期メッセージハンドラの扱いを変更できます。
AsyncPublishStrategy |
- |
|---|---|
AsyncPublishStrategy.Parallel |
非同期メッセージハンドラは全て並列に実行される。 |
AsyncPublishStrategy.Sequential |
非同期メッセージハンドラはキューに追加され、追加順に一つづつ実行される。 |
IMessageSubscriber<T>はメッセージの購読を行うためのインターフェースです。Action<T>を受け取るSubscribe()拡張メソッドが提供されているため、ラムダ式を用いて簡単に購読を行うことが可能です。また、戻り値のIDisposableのDispose()を呼び出すことで購読を解除することができます。
IMessageSubscriber<Message> subscriber;
// メッセージを購読する
var subscription = subscriber.Subscribe(x =>
{
Console.WriteLine(x.Text);
});
// 購読を解除する
subscription.Dispose();また、SubscribeAwait()を用いることで購読内で非同期処理を行うことも可能です。
var subscription = subscriber.SubscribeAwait((x, ct) =>
{
await FooAsync(x, ct);
}, AsyncSubscribeStrategy.Sequential);AsyncSubscribeStrategyを指定することで、処理の実行中にメッセージを受け取ったときの扱いを変更できます。
AsyncSubscribeStrategy |
- |
|---|---|
AsyncSubscribeStrategy.Sequential |
メッセージはキューに追加され、順番に実行されます。 |
AsyncSubscribeStrategy.Parallel |
並列に実行されます。 |
AsyncSubscribeStrategy.Switch |
実行中の処理をキャンセルし、新しいメッセージの処理を実行します。 |
AsyncSubscribeStrategy.Drop |
実行中は新たなメッセージを無視します。 |
Filterを利用することでメッセージの前後に処理を追加することができるようになります。
新たなFilterを作成するには、IMessageFilter<T>を実装したクラスを定義します。
public class NopFilter<T> : IMessageFilter<T>
{
public async ValueTask InvokeAsync(T message, CancellationToken cancellationToken, Func<T, CancellationToken, ValueTask> next)
{
try
{
// 継続処理を呼び出す
await next(context, cancellationToken);
}
catch
{
throw;
}
finally
{
}
}
}IMessageFilter<T>の定義はASP.NET Coreのミドルウェアなどでも用いられているasync decoratorパターンを採用しています。
以下は処理の前後にログ出力を追加するFilterのサンプルです。
public class LoggingFilter<T> : IMessageFilter<T>
{
public async ValueTask InvokeAsync(T message, CancellationToken cancellationToken, Func<T, CancellationToken, ValueTask> next)
{
Console.WriteLine("Before");
await next(message, cancellationToken);
Console.WriteLine("After");
}
}作成したFilterを追加するにはいくつかの方法があります。
MessageBroker<T>に直接追加する場合はAddFilter<T>()を利用します。フィルターの適用順序は追加順と同じになります。
var broker = new MessageBroker<int>();
// フィルターを追加
broker.AddFilter<LoggingFilter<int>>();DIコンテナ上のPublisherにグローバルなFilterを追加するには、AddZeroMessenger()メソッド内で構成を行います。
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddZeroMessenger(messenger =>
{
// 型を指定して追加
messenger.AddFilter<LoggingFilter<Message>>();
// open genericsで追加
messenger.AddFilter(typeof(LoggingFilter<>));
});
})
.Build()
.Run();Subscribe時に個別のフィルターを追加するにはWithFilter<T>() / WithFilters()拡張メソッドが利用可能です。
IMessageSubscriber<Message> subscriber;
subscriber
.WithFilter<LoggingFilter<Message>>()
.Subscribe(x =>
{
});Zero Messengerでは組み込みのFilterとしてPredicateFilter<T>が提供されています。AddFilter<T>()やWithFilter<T>()などの引数にPredicate<T>を渡すと、それを元に作成したPredicateFilter<T>が自動で追加されます。
public record struct FooMessage(int Value);
IMessageSubscriber<FooMessage> subscriber;
subscriber
.WithFilter(x => x.Value >= 0) // 0未満の値は除外
.Subscribe(x =>
{
});Zero MessengerはCysharp/R3との連携をサポートしています。この機能を有効化するには、ZeroMessenger.R3パッケージを追加します。
dotnet add package ZeroMessenger.R3Install-Package ZeroMessenger.R3ZeroMessenger.R3を追加することで、IMessageSubscriber<T>からObservable<T>への変換や、Observable<T>をIMessagePublisher<T>に接続するオペレータが利用できるようになります。
// IMessageSubscriber<T>からObservable<T>へ変換
subscriber.ToObservable()
.Subscribe(x => { });
// Observable<T>をSubscribeし、IMessagePublisher<T>のPublish()に変換
observable.SubscribeToPublish(publisher);
// Observable<T>をSubscribeAwaitし、IMessagePublisher<T>のPublishAsync()に変換
observable.SubscribeAwaitToPublish(publisher, AwaitOperation.Sequential, AsyncPublishStrategy.Parallel);NugetForUnityでNuGetパッケージをインストールすることで、Zero MessengerをUnityで利用できるようになります。
- Unity 2021.3 以上
-
NugetForUnityをインストールします。
-
NuGet > Manage NuGet PackagesからNuGetウィンドウを開き、ZeroMessengerパッケージを検索してインストールします。
さらに、VContainerのDIコンテナ上でZero Messengerを扱うための拡張パッケージが用意されています。
ZeroMessenger.VContainerをインストールするには、Window > Package ManagerからPackage Managerウィンドウを開き、[+] > Add package from git URLから以下のURLを入力します。
https://github.com/AnnulusGames/ZeroMessenger.git?path=src/ZeroMessenger.Unity/Assets/ZeroMessenger.VContainer
ZeroMessenger.VContainerを導入するとIContainerBuilderにAddZeroMessenger()拡張メソッドが追加されます。これを呼び出すことでDIコンテナにZero Messengerが追加され、IMessagePublisher<T>/IMessageSubscriber<T>がInjectされるようになります。
using VContainer;
using VContainer.Unity;
using ZeroMessenger.VContainer;
public class ExampleLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// Zero Messengerを追加
builder.AddZeroMessenger();
}
}Note
AddZeroMessenger()はOpen Genericsを利用した登録を行います。これはUnity 2022.1より前のバージョンのIL2CPPで動作しない可能性があります。
このライブラリはMITライセンスの下に公開されています。


