From b15e92d55d7866918dbaac3b7d7080dbf43bc8e7 Mon Sep 17 00:00:00 2001 From: Volodymyr Dombrovskyi <5788605+dombrovsky@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:38:46 -0600 Subject: [PATCH] docs: add choosing TaskFlow decision guide --- README.md | 1 + docs/choosing-taskflow.md | 78 ++++++++++++++++++++++++++++++++++ docs/execution-models.md | 2 + docs/getting-started.md | 1 + docs/index.md | 1 + docs/recipes.md | 2 + docs/semantics-and-pitfalls.md | 2 + 7 files changed, 87 insertions(+) create mode 100644 docs/choosing-taskflow.md diff --git a/README.md b/README.md index 1099c84..cc68fe4 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Read [Concepts and lifecycle](https://dombrovsky.github.io/TaskFlow/concepts-and ## Documentation +- [Choosing TaskFlow](https://dombrovsky.github.io/TaskFlow/choosing-taskflow/) - [Getting started](https://dombrovsky.github.io/TaskFlow/getting-started/) - [Recipes](https://dombrovsky.github.io/TaskFlow/recipes/) - [Extensions](https://dombrovsky.github.io/TaskFlow/extensions/) diff --git a/docs/choosing-taskflow.md b/docs/choosing-taskflow.md new file mode 100644 index 0000000..206c2cb --- /dev/null +++ b/docs/choosing-taskflow.md @@ -0,0 +1,78 @@ +--- +layout: page +title: Choosing TaskFlow +permalink: /choosing-taskflow/ +--- + +# Choosing TaskFlow + +Use this page when you are deciding between TaskFlow and common .NET primitives such as `lock`, `SemaphoreSlim`, `Channel`, or custom task chaining. + +TaskFlow is an owned FIFO execution lane for submitted operations. Each submission gets its own returned task while the lane controls serialization and lifetime. + +## Comparison at a glance + +| Option | Best fit | What it does not give by default | +|---|---|---| +| `lock` | Very short synchronous critical sections | No `await` in the critical section, no per-submission task model, no queue ownership | +| `SemaphoreSlim` | Async mutual exclusion around one code path | You build ordering/lifecycle/error-observation conventions yourself | +| `Channel` | Producer-consumer data exchange with buffering/backpressure | No per-caller operation task unless you add correlation and completion plumbing | +| Custom task chaining | Full control with explicit tradeoffs | Easy to regress ordering/cancellation/disposal semantics over time | +| TaskFlow | Owned sequential operation lane with per-call tasks and composable policies | Not a parallel work queue and not hard preemption | + +## Use `lock` when + +- The work is synchronous and short. +- You only need in-process mutual exclusion. +- You do not need per-caller asynchronous completion tasks. + +Use TaskFlow instead when callers are asynchronous and you must serialize submitted operations without blocking threads. + +## Use `SemaphoreSlim` when + +- You simply need async mutual exclusion around a specific operation. +- You can define and maintain your own conventions for lifetime and shutdown. +- You do not need a first-class lane abstraction. + +Use TaskFlow instead when you need lane ownership, consistent per-submission outcomes, and policy composition (`WithTimeout`, `OnError`, cancellation scopes, latest-request-wins). + +## Use `Channel` when + +- You have producers and consumers exchanging data. +- Buffering, bounded capacity, and backpressure are the central design concerns. +- Consumers can pull and process messages independently. + +Use TaskFlow instead when callers submit operations and each call must receive an individual task representing that operation's outcome. + +## Use custom task chaining when + +- You have niche scheduling semantics and accept implementation/maintenance cost. +- Existing primitives do not fit your constraints. + +Use TaskFlow instead when you want established sequential-lane semantics without repeatedly rebuilding ordering, cancellation, disposal, and failure behavior. + +## Tradeoffs you accept with TaskFlow + +- Cancellation is cooperative. Delegates must observe the provided token. +- Built-in flows invoke accepted queued delegates even if already canceled, so the lane progresses deterministically. +- One lane is intentionally sequential, so long operations can create head-of-line blocking. + +These are behavior contracts, not incidental implementation details. See [Semantics and pitfalls](semantics-and-pitfalls.md) for details. + +## Typical scenarios where TaskFlow is a strong fit + +- Serialize access to a non-thread-safe resource while preserving async caller APIs. +- Convert synchronous callbacks into ordered asynchronous processing. +- Implement latest-request-wins behavior with cooperative cancellation. +- Bind operation processing to a component or dependency-injection scope lifetime. + +See concrete examples in [Recipes](recipes.md). + +## Related guides + +- [Getting started](getting-started.md) +- [Concepts and lifecycle](concepts-and-lifecycle.md) +- [Execution models](execution-models.md) +- [Semantics and pitfalls](semantics-and-pitfalls.md) +- [Extensions](extensions/index.md) +- [Dependency injection](dependency-injection.md) diff --git a/docs/execution-models.md b/docs/execution-models.md index 272703f..5629d85 100644 --- a/docs/execution-models.md +++ b/docs/execution-models.md @@ -8,6 +8,8 @@ permalink: /execution-models/ TaskFlow separates the scheduling contract from the lifetime-owning execution lane. Choose the smallest model that supplies the ordering and thread behavior your component actually needs. +If you are still deciding whether TaskFlow is the right primitive at all, start with [Choosing TaskFlow](choosing-taskflow.md). + ## Standard TaskFlow `TaskFlow` is the default choice. It serializes operations in FIFO order and schedules them through `TaskFlowOptions.TaskScheduler`, which defaults to `TaskScheduler.Default`. diff --git a/docs/getting-started.md b/docs/getting-started.md index 9e17c5b..22d1eb6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -79,6 +79,7 @@ For built-in flows, cancellation does not remove a queued delegate. When it reac ## Next steps +- Decide whether TaskFlow fits your case in [Choosing TaskFlow](choosing-taskflow.md). - Start from a complete application pattern in [Recipes](recipes.md). - Add cancellation and reliability policies through [Extensions](extensions/index.md). - Select a different thread or scheduler in [Execution models](execution-models.md). diff --git a/docs/index.md b/docs/index.md index e86fc60..62164e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,6 +36,7 @@ Use TaskFlow to: | Goal | Read | |---|---| +| Decide whether TaskFlow is the right primitive for this problem | [Choosing TaskFlow](choosing-taskflow.md) | | Create and dispose a first FIFO lane | [Getting started](getting-started.md) | | Understand operation completion and ownership | [Concepts and lifecycle](concepts-and-lifecycle.md) | | Avoid cancellation, timeout, and disposal surprises | [Semantics and pitfalls](semantics-and-pitfalls.md) | diff --git a/docs/recipes.md b/docs/recipes.md index a8179ed..2990e0b 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -8,6 +8,8 @@ permalink: /recipes/ These examples use both caller-observed operation tasks and deliberate fire-and-forget submissions whose lifetime is owned by a flow. +If you are choosing between TaskFlow and other primitives, read [Choosing TaskFlow](choosing-taskflow.md) first. + ## Serialize a non-thread-safe resource ```csharp diff --git a/docs/semantics-and-pitfalls.md b/docs/semantics-and-pitfalls.md index 4b7adcb..e230b98 100644 --- a/docs/semantics-and-pitfalls.md +++ b/docs/semantics-and-pitfalls.md @@ -6,6 +6,8 @@ permalink: /semantics-and-pitfalls/ # Semantics and pitfalls +For a first-pass decision on whether these tradeoffs match your scenario, see [Choosing TaskFlow](choosing-taskflow.md). + ## Canceled queued delegates are still invoked The built-in `TaskFlow`, `DedicatedThreadTaskFlow`, and `CurrentThreadTaskFlow` implementations preserve queue progression by invoking every accepted delegate after its predecessor finishes. If cancellation happened while the operation waited, the delegate receives an already-canceled token.