Async and Batching APIs

  • how-to
    +
    The Couchbase .NET SDK uses the Task-based Asynchronous Pattern (TAP) using types in the System.Threading.Tasks namespace to represent asynchronous operations against the Couchbase Server which can be awaited via the await keyword. There is no separate synchronous API, however, all tasks can be run synchronously in a blocking fashion using the Task.Result method. Batching may be done with Task.WhenAll.

    A couple of points to consider: All operations are composed of a Task or a Task<IResult> depending upon whether or not the task return void or a TResult. Tasks are evaluated asynchronously using the familiar await keyword, and run in a blocking method by calling the Task.Result() method. If a task is awaited, the method awaiting the task must have the async keyword in its signature. Tasks can be run concurrently using any of the System.Threading.Tasks combinators: Task.Run, Task.WhenAll, Task.WhenAny, etc. More information can be found in Microsoft’s documentation here: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern#combinators.

    Note: All examples on this page start with initiating a Cluster object and then opening a Bucket and Collection:

    var cluster = new Cluster("couchbase://your-ip", new ClusterOptions()
                     .WithConnectionString("couchbase://your-ip")
                     .WithCredentials(username: "user", password: "password")
                     .WithBuckets("travel-sample")
                );
    
    var bucket = await cluster.BucketAsync("travel-sample");
    var scope = await bucket.ScopeAsync("inventory");
    var collection = await scope.CollectionAsync("hotel");

    Asynchronous Programming using await

    This is the most common and basic ways for consuming Couchbase operations asynchronously via Tasks:

    var upsertResult = await collection.UpsertAsync("doc1",new {Name = "Ted", Age = 80});
    using (var getResult = await collection.GetAsync("doc1"))
    {
        var person = getResult.ContentAs<dynamic>();
    }

    In this way, every single operation will be fired off on a System.Threading.Threadpool thread separately from the main application thread. Note that in the UpsertAsync method above, an exception will be thrown if the operation fails; if it succeeds then the result will be an IMutationResult that contains the CAS value for reuse, otherwise it can be ignored. GetAsync returns a GetResult if it succeeds, you’ll then have to use ContentAs to read the returned value.

    Synchronous Programming using Task.Result

    The same methods above can also be called synchronously, blocking the calling thread by using the Result property:

    var upsertResult = collection.UpsertAsync("doc1",new {Name = "Ted", Age = 80}).Result;
    using (var getResult = collection.GetAsync("doc1").Result)
    {
        var person = getResult.ContentAs<dynamic>();
    }

    Another way of doing this is by calling the awaiter explicitly Task.GetAwaiter().GetResult():

    var upsertResult = collection.UpsertAsync("doc1",new {Name = "Ted", Age = 80}).GetAwaiter().GetResult();
    using (var getResult = collection.GetAsync("doc1").GetAwaiter().GetResult())
    {
        var person = getResult.ContentAs<dynamic>();
    }

    This is a slightly more verbose way of achieving the same goal: calling the Task synchronously in a blocking fashion. Note that Couchbase suggests using the await keyword and running the Task asynchronously, and not blocking the calling thread!

    Concurrently executing lots of Tasks using Task.WhenAll

    In certain situations, it may be desirable to execute a large number of Tasks concurrently. The way to batch like this is via the Task.WhenAll combinator:

    var tasks = new List<Task<IGetResult>>
    {
        collection.GetAsync("doc1"),
        collection.GetAsync("doc2"),
        collection.GetAsync("doc3"),
        collection.GetAsync("doc4")
    };
    
    var results = await Task.WhenAll(tasks);
    foreach (var getResult in results)
    {
        var doc = getResult.ContentAs<dynamic>();
        //work with the doc returned
    }

    In this example we will fetch four documents asynchronously while not blocking the main thread, suspending the state until the results are returned. Then will loop through and work with each document in a synchronous manner.

    What about Task.Wait, Task.WaitAll

    Task.Wait and Task.WaitAll and other Wait methods will block the main thread synchronously while the Task is run; we do not suggest using either of these methods in most cases. An example of when you would use one of these methods would be a console app where you do not want the main thread to run through the main method before getting the results back.

    Avoiding DeadLocks

    When a Task runs, it suspends the current continuation context, executes the Task, and then attempts to continue back where the continuation context suspended. In ASP.NET this is the request context; however, in ASP.NET it is not tied to a specific thread and the context only allows one thread to run at a time. The top level method is blocked by the context and when the continuation context calls back it deadlocks because its already blocked.

    The best way to avoid deadlocks such as this, is to avoid blocking on Tasks, which means avoiding any of the Wait methods. Another handy way of avoiding deadlocks is to not use a synchronization context:

    var result = collection.GetAsync("TheKey").ConfigureAwait(false);

    This will configure the Task to not use the synchronization context even if it exists.

    Batching

    Asynchronous clients inherently batch operations: because the application receives the response at a later stage in the application, batching will be the result of issuing many requests in sequence.

    Batching in .NET using TAP is relively simple. await Task.WhenAll() will group together tasks and wait until they are complete before running, useful where you do not want the main thread to run through the main method before getting the results back.

    // collection of things that will complete in the future
    var tasks = new List<Task>();
    
    // create tasks to be executed concurrently
    // NOTE: these tasks have not yet been scheduled
    for (var i = 0; i <100; i++)
    {
        var task = collection.GetAsync($"mykey-{i}");
        tasks.Add(task);
    }
    
    // Waits until all of the tasks have completed
    await Task.WhenAll(tasks);
    
    // can iterate task list to get results
    foreach (var task in tasks)
    {
        var result = tasks.Result;
    }