Codebelt
v10.5.5

Cuemon.Threading

Concurrent for and foreach loops with synchronous and asynchronous delegates for long-running parallel workloads.

.NET 10.0 / .NET 9.0 / .NET Standard 2.0 MIT 358,099 downloads

Cuemon.Threading

Overview

Cuemon.Threading provides parallel loop primitives that extend System.Threading with first-class support for both synchronous and asynchronous delegates. The central type is ParallelFactory, a static class that offers For, ForAsync, ForEach, ForEachAsync, and result-collecting variants (ForResult, ForResultAsync, ForEachResult, ForEachResultAsync). All result-collecting variants return an IReadOnlyCollection<TResult> in the same sequential order as the loop index or source sequence, regardless of actual execution order.

For scenarios that require custom iteration rules, AdvancedParallelFactory exposes low-level control over loop variable, step, and condition through ForLoopRuleset<TOperand>, as well as While and WhileResult variants for predicate-driven queue workloads. The package also includes AsyncPatterns.SafeInvokeAsync, which manages CA2000 lifetimes for disposable async resources.

Key APIs

ParallelFactory is the main consumer entry point. Its For and ForAsync overloads iterate an index range (int or long) across multiple tasks, invoking a worker delegate once per iteration. The ForEach and ForEachAsync overloads iterate any IEnumerable<TSource>. The ForResult/ForResultAsync and ForEachResult/ForEachResultAsync variants collect worker return values into an IReadOnlyCollection<TResult> in sequential loop order. All overloads support up to five extra typed parameters passed through to the worker delegate without boxing via MutableTuple.

AdvancedParallelFactory provides lower-level parallel loop control. Its For, ForAsync, ForResult, and ForResultAsync overloads accept a ForLoopRuleset<TOperand> that encodes start, end, and step. The While, WhileAsync, WhileResult, and WhileResultAsync overloads drive iteration from a state object, a predicate, and a value provider, making them suitable for queue-based workloads. Use AdvancedParallelFactory.Iterator<T> and AdvancedParallelFactory.Condition<T> to construct loop control with explicit AssignmentOperator and RelationalOperator values.

ForLoopRuleset<TOperand> encapsulates the three components of a counted for-loop: initial value, repeat count (end), and step. The type parameter must be a value type that implements IComparable<TOperand>, IEquatable<TOperand>, and IConvertible. ParallelFactory always constructs a default ForLoopRuleset<int> or ForLoopRuleset<long> with a step of 1; use AdvancedParallelFactory directly when a different step or operator is required.

AsyncWorkloadOptions configures asynchronous parallel methods. It inherits from AsyncOptions (from Cuemon.Core) and exposes CancellationToken and PartitionSize. The PartitionSize controls how many tasks are started per batch; leaving it unset uses a system-determined default. Pass an Action<AsyncWorkloadOptions> as the optional setup argument to the async overloads.

AsyncTaskFactoryOptions configures synchronous parallel methods in the same way. It also provides PartitionSize and CreationOptions (TaskCreationOptions) to fine-tune how tasks are created.

AsyncPatterns contains SafeInvokeAsync static overloads for CA2000-compliant disposal of async resources. Each overload accepts an initializer delegate, a tester delegate, an optional catcher delegate, and a CancellationToken. The return value is Task<TResult> where TResult : class, IDisposable. Use this type when an async pipeline creates a disposable resource whose lifetime must be managed correctly regardless of exceptions or cancellation.

Basic usage

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit;
using Cuemon.Threading;
using Xunit;

namespace MyProject.Tests;

public class ParallelFactoryExampleTest : Test
{
    public ParallelFactoryExampleTest(ITestOutputHelper output) : base(output)
    {
    }

    [Fact]
    public async Task ForResultAsync_CollectsResultsInLoopOrder()
    {
        const int count = 8;

        IReadOnlyCollection<int> squares = await ParallelFactory.ForResultAsync(0, count, async (i, ct) =>
        {
            await Task.Delay(5, ct);
            return i * i;
        });

        Assert.Equal(count, squares.Count);
        Assert.True(squares.SequenceEqual(Enumerable.Range(0, count).Select(i => i * i)));

        TestOutput.WriteLine($"Computed {squares.Count} squares in parallel: {string.Join(", ", squares)}");
    }
}

Use this pattern when you need to fan out an index-based computation across multiple concurrent tasks and collect results in a deterministic order without managing Task arrays or ConcurrentDictionary indices manually. The result collection preserves loop index order regardless of task completion order, which makes downstream processing straightforward.

Installation

dotnet add package Cuemon.Threading

Usage guidance

Reach for ParallelFactory when you have a CPU- or I/O-bound loop whose iterations are independent and the total iteration count or source sequence is known before the loop starts. Use AdvancedParallelFactory when you need a non-unit step, a custom loop variable type, or a predicate-driven While loop over a shared queue or stack. When your workload is already expressed as a Parallel.For or Parallel.ForEach call and you do not need async worker delegates or index-ordered result collection, the built-in System.Threading.Tasks.Parallel class is sufficient and adds no dependency.

Family packages