Asynchronous APIs | Ignite Documentation

Ignite Summit 2024 — Call For Speakers Now Open — Learn more

Edit

Asynchronous APIs

Overview

Many Ignite APIs have asynchronous variants, for example, void ICache.Put and Task ICache.PutAsync. Async APIs allow us to write efficient non-blocking code:

ICache<int, string> cache = ignite.GetOrCreateCache<int, string>("name");

// Sync, blocks thread on every call.
cache.Put(1, "Hello");
string hello = cache.Get(1);

// Async, does not block threads.
await cache.PutAsync(1, "Hello");
string hello = await cache.GetAsync(1);

With async APIs, current thread is not blocked while we wait for the cache operation to complete; the thread is returned to the thread pool and can perform other work.

When the async operation completes, our method resumes execution - either on the same thread, or on a different one - depending on the environment and the configuration. This is called "async continuation".

Async Continuations

Unless specified otherwise, Ignite executes async continuations on the .NET Thread Pool, which is safe and does not require any special care.

Thin Client

All thin client async APIs use .NET Thread Pool. for async continuations.

Thick Cache

Callbacks for asynchronous cache operations on server and thick client nodes are invoked by using Java ForkJoinPool#commonPool, unless a different executor is configured with IgniteConfiguration.AsyncContinuationExecutor.

  • This default executor is safe for any operations inside the callback.

  • Default behavior was changed in Ignite 2.11. Before that, async cache operation callbacks were called from an Ignite system pool (so-called "striped pool").

  • To restore the previous behavior, use IgniteConfiguration.AsyncContinuationExecutor = AsyncContinuationExecutor.UnsafeSynchronous.

    • Previous behavior can provide a small performance improvement, because callbacks are executed without any indirection or scheduling.

    • UNSAFE: cache operations cannot proceed while system threads execute callbacks, and deadlocks are possible if other cache operations are invoked from the callback.

Important

Ignite 2.10 and before: possibility of deadlocks and system pool starvation

In Ignite versions 2.10 and before, system pool is used to run async continuations, which means that GetAsync call in the code above is executed by the system thread.

This can lead to deadlocks if user code blocks the thread, or cause starvation because system thread is busy running user code instead of performing cache operations.

To enable safe behavior, move continuations to .NET Thread Pool manually:

await cache.PutAsync(1, "Hello").ContinueWith(
                t => {},
                CancellationToken.None,
                TaskContinuationOptions.None,
                TaskScheduler.Default);

string hello = await cache.GetAsync(1).ContinueWith(
                t => t.Result,
                CancellationToken.None,
                TaskContinuationOptions.None,
                TaskScheduler.Default);

Tip: use an extension method to reduce verbosity.

Compute

Ignite 2.11 and later: all ICompute async APIs use .NET Thread Pool to run async continuations.

Ignite 2.10 and before: Compute async continuations are executed on Ignite public pool. To reduce the load on the public pool, it is recommended to use the same ContinueWith approach as above:

await compute.CallAsync(new MyAction()).ContinueWith(
                t => t.Result,
                CancellationToken.None,
                TaskContinuationOptions.None,
                TaskScheduler.Default);

This will move the continuation from Ignite public pool (reserved for Compute functionality) to the .NET thread pool (TaskScheduler.Default).

ConfigureAwait

Task.ConfigureAwait method can be used as usual with all Ignite async APIs.

See ConfigureAwait FAQ for more details.