誰も私にマップとmapAsync w.r.t AKKAストリームの違いを教えてもらえますか? ドキュメント内 と言われています
外部の非ストリームベースのサービスを含むストリーム変換および副作用は、mapAsyncまたはmapAsyncUnorderedで実行できます
なぜここで単純にマッピングできないのですか? Flow、Source、Sinkはすべて本質的にモナディックであり、したがってこれらの性質の遅延でマップは正常に機能するはずだと思います。
署名
違いは signatures で最もよく強調されています。Flow.map
はT
型を返す関数を受け取り、Flow.mapAsync
はFuture[T]
型を返す関数を受け取ります。
実際の例
例として、ユーザーIDに基づいてユーザーのフルネームをデータベースに照会する関数があるとします。
type UserID = String
type FullName = String
val databaseLookup : UserID => FullName = ??? //implementation unimportant
Source
値のakkaストリームUserID
を指定すると、ストリーム内でFlow.map
を使用してデータベースをクエリし、フルネームをコンソールに出力できます。
val userIDSource : Source[UserID, _] = ???
val stream =
userIDSource.via(Flow[UserID].map(databaseLookup))
.to(Sink.foreach[FullName](println))
.run()
このアプローチの1つの制限は、このストリームが一度に1 dbクエリしか作成しないことです。このシリアルクエリは「ボトルネック」となり、ストリームの最大スループットを妨げる可能性があります。
Future
を使用して同時クエリを実行することで、パフォーマンスを向上させることができます。
def concurrentDBLookup(userID : UserID) : Future[FullName] =
Future { databaseLookup(userID) }
val concurrentStream =
userIDSource.via(Flow[UserID].map(concurrentDBLookup))
.to(Sink.foreach[Future[FullName]](_ foreach println))
.run()
この単純化された補遺の問題は、バックプレッシャーを効果的に排除したことです。
Sinkは、Futureを導入し、foreach println
を追加しているだけです。これは、データベースクエリに比べて比較的高速です。ストリームは継続的にデマンドをソースに伝播し、Flow.map
の内部により多くのFutureを生成します。したがって、同時に実行されるdatabaseLookup
の数に制限はありません。制限のない並列クエリにより、最終的にデータベースが過負荷になる可能性があります。
Flow.mapAsync
が助けになりました。同時ルックアップの数を制限しながら、同時にdbアクセスが可能です。
val maxLookupCount = 10
val maxLookupConcurrentStream =
userIDSource.via(Flow[UserID].mapAsync(maxLookupCount)(concurrentDBLookup))
.to(Sink.foreach[FullName](println))
.run()
また、Sink.foreach
が単純になり、Future[FullName]
ではなく、FullName
だけが取り込まれるようになりました。
順序付けされていない非同期マップ
UserIDからFullNamesへの順番を維持する必要がない場合は、Flow.mapAsyncUnordered
を使用できます。例:すべての名前をコンソールに出力する必要があるだけで、それらが出力された順序は関係ありません。