Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
78 changes: 78 additions & 0 deletions docs/choosing-taskflow.md
Original file line number Diff line number Diff line change
@@ -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<T>`, 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<T>` | 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<T>` 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)
2 changes: 2 additions & 0 deletions docs/execution-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
2 changes: 2 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/semantics-and-pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading