AsyncHelper Class

A helper class to implement CPU-bound operations with adjustable parallelization, cancellation and progress reporting, allowing a single shared implementation for both sync and async overloads where the latter can be either IAsyncResult or Task (.NET Framework 4.0 and later) returning methods.

Definition

Namespace: KGySoft.Threading
Assembly: KGySoft.CoreLibraries (in KGySoft.CoreLibraries.dll) Version: 9.0.0-preview.1
C#
public static class AsyncHelper
Inheritance
Object    AsyncHelper

Example

The following example demonstrates how to use the AsyncHelper class to create sync/async versions of a method sharing the common implementation in a single method.
C#
#nullable enable

using System;
using System.Threading;
using System.Threading.Tasks;

using KGySoft;
using KGySoft.CoreLibraries;
using KGySoft.Reflection;
using KGySoft.Security.Cryptography;
using KGySoft.Threading;

public static class Example
{
    // The sync version. This method is blocking, cannot be canceled, does not report progress and auto adjusts parallelization.
    public static double[] GenerateRandomValues(int count, double min, double max)
    {
        ValidateArguments(count, min, max);

        // Just to demonstrate some immediate return (which is quite trivial in this overload).
        if (count == 0)
            return Reflector.EmptyArray<double>(); // same as Array.Empty but available also for older targets

        // The actual processing is called directly from this overload with DefaultContext. The result is never null from here.
        return DoGenerateRandomValues(AsyncHelper.DefaultContext, count, min, max)!;
    }

    // Another sync overload. This is still blocking but allows cancellation, reporting progress and adjusting parallelization.
    // The result can be null if the operation is canceled and config.ThrowIfCanceled was false.
    public static double[]? GenerateRandomValues(int count, double min, double max, ParallelConfig? config)
    {
        ValidateArguments(count, min, max);

        // For immediate return use AsyncHelper.FromResult, which handles throwing possible OperationCanceledException
        // or returning null if config.ThrowIfCanceled was false. For void methods use AsyncHelper.HandleCompleted instead.
        if (count == 0)
            return AsyncHelper.FromResult(Reflector.EmptyArray<double>(), config);

        // Even though this is a synchronous call, use AsyncHelper to take care of the context and handle cancellation.
        return AsyncHelper.DoOperationSynchronously(context => DoGenerateRandomValues(context, count, min, max), config);
    }

    // The Task-returning version. Requires .NET Framework 4.0 or later and can be awaited in .NET Framework 4.5 or later.
    public static Task<double[]?> GenerateRandomValuesAsync(int count, double min, double max, TaskConfig? asyncConfig = null)
    {
        ValidateArguments(count, min, max);

        // Use AsyncHelper.FromResult for immediate return. It handles asyncConfig.ThrowIfCanceled properly.
        // To return a Task without a result use AsyncHelper.FromCompleted instead.
        if (count == 0)
            return AsyncHelper.FromResult(Reflector.EmptyArray<double>(), asyncConfig);

        // The actual processing for Task returning async methods.
        return AsyncHelper.DoOperationAsync(context => DoGenerateRandomValues(context, count, min, max), asyncConfig);
    }

    // The old-style Begin/End methods that work even in .NET Framework 3.5. Can be omitted if not needed.
    public static IAsyncResult BeginGenerateRandomValues(int count, double min, double max, AsyncConfig? asyncConfig = null)
    {
        ValidateArguments(count, min, max);

        // Use AsyncHelper.FromResult for immediate return. It handles asyncConfig.ThrowIfCanceled and
        // sets IAsyncResult.CompletedSynchronously. Use AsyncHelper.FromCompleted if the End method has no return value.
        if (count == 0)
            return AsyncHelper.FromResult(Reflector.EmptyArray<double>(), asyncConfig);

        // The actual processing for IAsyncResult returning async methods.
        return AsyncHelper.BeginOperation(context => DoGenerateRandomValues(context, count, min, max), asyncConfig);
    }

    // Note that the name of "BeginGenerateRandomValues" is explicitly specified here.
    // Older compilers need it also for AsyncContext.BeginOperation.
    public static double[]? EndGenerateRandomValues(IAsyncResult asyncResult)
        => AsyncHelper.EndOperation<double[]?>(asyncResult, nameof(BeginGenerateRandomValues));

    // The method of the actual processing has the same parameters as the sync version after an IAsyncContext parameter.
    // The result can be null if the operation is canceled (but see also the next comment)
    private static double[]? DoGenerateRandomValues(IAsyncContext context, int count, double min, double max)
    {
        // Not throwing OperationCanceledException explicitly: it will be thrown by the caller
        // if the asyncConfig.ThrowIfCanceled was true in the async overloads.
        // Actually we could call context.ThrowIfCancellationRequested() that would be caught conditionally by the caller
        // but use that only if really needed because using exceptions as control flow is really ineffective.
        if (context.IsCancellationRequested)
            return null;

        // New progress without max value: in a UI this can be displayed with some indeterminate progress bar/circle
        context.Progress?.New("Initializing");
        Thread.Sleep(100); // imitating some really slow initialization, blocking the current thread
        var result = new double[count];
        using var rnd = new SecureRandom(); // just because it's slow

        // We should periodically check after longer steps whether the processing has already been canceled
        if (context.IsCancellationRequested)
            return null;

        // Possible shortcut: ParallelHelper has a For overload that can accept an already created context from
        // implementations like this one. It will call IAsyncProgress.New and IAsyncProgress.Increment implicitly.
        ParallelHelper.For(context, "Generating values", 0, count,
            body: i => result[i] = rnd.NextDouble(min, max, FloatScale.ForceLogarithmic)); // some slow number generation

        // Actually the previous ParallelHelper.For call returns false if the operation was canceled
        if (context.IsCancellationRequested)
            return null;

        // Alternative version with Parallel.For (only in .NET Framework 4.0 and above)
        context.Progress?.New("Generating values (alternative way)", maximumValue: count);
        Parallel.For(0, count,
            // for auto-adjusting parallelism ParallelOptions strictly requires -1, whereas context allows <= 0
            new ParallelOptions { MaxDegreeOfParallelism = context.MaxDegreeOfParallelism <= 0 ? -1 : context.MaxDegreeOfParallelism },
            (i, state) =>
            {
                // Breaking the loop on cancellation. Note that we did not set a CancellationToken in ParallelOptions
                // because if context comes from the .NET Framework 3.5-compatible Begin method it cannot even have any.
                // But if you really need a CancellationToken you can pass one to asyncConfig.State from the caller.
                if (context.IsCancellationRequested)
                {
                    state.Stop();
                    return;
                }

                result[i] = rnd.NextDouble(min, max, FloatScale.ForceLogarithmic);
                context.Progress?.Increment();
            });

        return context.IsCancellationRequested ? null : result;
    }

    private static void ValidateArguments(int count, double min, double max)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count), PublicResources.ArgumentMustBeGreaterThanOrEqualTo(0));
        if (Double.IsNaN(min))
            throw new ArgumentOutOfRangeException(nameof(min), PublicResources.ArgumentOutOfRange);
        if (Double.IsNaN(max))
            throw new ArgumentOutOfRangeException(nameof(max), PublicResources.ArgumentOutOfRange);
        if (max < min)
            throw new ArgumentException(PublicResources.MaxValueLessThanMinValue);
    }
}

Properties

DefaultContext Gets a default context for non-async operations that allows to set the number of threads automatically, does not support reporting progress, and is not cancellable.
See the Examples section of the AsyncHelper class for details.
SingleThreadContext Gets a predefined context for non-async operations that forces to run a possibly parallel algorithm on a single thread, does not support reporting progress, and is not cancellable. It actually returns a SimpleContext instance.
See the Examples section of the AsyncHelper class for details about using IAsyncContext.

Methods

BeginOperation(ActionIAsyncContext, AsyncConfig, String) Exposes the specified operation with no return value as an IAsyncResult-returning async operation. The operation can be completed by calling the EndOperation method.
See the Examples section of the AsyncHelper class for details.
BeginOperationTResult(FuncIAsyncContext, TResult, AsyncConfig, String) Exposes the specified operation with a return value as an IAsyncResult-returning async operation. To obtain the result the EndOperation method must be called.
See the Examples section of the AsyncHelper class for details.
BeginOperationTResult(FuncIAsyncContext, TResult, TResult, AsyncConfig, String) Exposes the specified operation with a return value as an IAsyncResult-returning async operation. To obtain the result the EndOperation method must be called.
See the Examples section of the AsyncHelper class for details.
DoOperationAsync(ActionIAsyncContext, TaskConfig) Executes the specified operation asynchronously.
See the Examples section of the AsyncHelper class for details.
DoOperationAsyncTResult(FuncIAsyncContext, TResult, TaskConfig) Executes the specified operation asynchronously.
See the Examples section of the AsyncHelper class for details.
DoOperationAsyncTResult(FuncIAsyncContext, TResult, TResult, TaskConfig) Executes the specified operation asynchronously.
See the Examples section of the AsyncHelper class for details.
DoOperationSynchronously(ActionIAsyncContext, ParallelConfig) Executes the specified operation synchronously, in which some sub-operations may run in parallel.
See the Examples section of the AsyncHelper class for details.
DoOperationSynchronouslyTResult(FuncIAsyncContext, TResult, ParallelConfig) Executes the specified operation synchronously, in which some sub-operations may run in parallel.
See the Examples section of the AsyncHelper class for details.
DoOperationSynchronouslyTResult(FuncIAsyncContext, TResult, TResult, ParallelConfig) Executes the specified operation synchronously, in which some sub-operations may run in parallel.
See the Examples section of the AsyncHelper class for details.
EndOperation(IAsyncResult, String) Waits for the completion of an operation started by a corresponding BeginOperation, FromResult or FromCompleted call. If the operation is still running, then this method blocks the caller and waits for the completion. The possibly occurred exceptions are also thrown then this method is called.
See the Examples section of the AsyncHelper class for details.
EndOperationTResult(IAsyncResult, String) Waits for the completion of an operation started by a corresponding BeginOperation or FromResult call. If the operation is still running, then this method blocks the caller and waits for the completion. The possibly occurred exceptions are also thrown then this method is called.
See the Examples section of the AsyncHelper class for details.
FromCompleted(TaskConfig) Returns a task that represents an already completed operation without a result.
See the Examples section of the AsyncHelper class for details. The example uses the similarly working FromResult method.
FromCompleted(AsyncConfig, String) Returns an IAsyncResult instance that represents an already completed operation without a result. The EndOperation method still must be called with the result of this method.
See the Examples section of the AsyncHelper class for details. The example uses the similarly working FromResult method.
FromResultTResult(TResult, ParallelConfig) This method can be used to immediately return from a synchronous operation that has a return value.
See the Examples section of the AsyncHelper class for details.
FromResultTResult(TResult, TaskConfig) Returns a task that represents an already completed operation with a result.
See the Examples section of the AsyncHelper class for details.
FromResultTResult(TResult, TResult, ParallelConfig) This method can be used to immediately return from a synchronous operation that has a return value.
See the Examples section of the AsyncHelper class for details.
FromResultTResult(TResult, AsyncConfig, String) Returns an IAsyncResult instance that represents an already completed operation with a result. To obtain the result the EndOperation method must be called.
See the Examples section of the AsyncHelper class for details.
FromResultTResult(TResult, TResult, TaskConfig) Returns a task that represents an already completed operation with a result.
See the Examples section of the AsyncHelper class for details.
FromResultTResult(TResult, TResult, AsyncConfig, String) Returns an IAsyncResult instance that represents an already completed operation with a result. To obtain the result the EndOperation method must be called.
See the Examples section of the AsyncHelper class for details.
HandleCompleted This method can be used to immediately finish a synchronous operation that does not have a return value.
See the Examples section of the AsyncHelper class for details. The example uses the similarly working FromResult method.

See Also