Search code examples
c#taskcompletionsource

TaskCompletionSource usage in IO Async methods


The implementation of the ExecuteNonQueryAsync() method in System.Data.SqlClient.SqlCommand is as follows:

    public override Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken) {

        Bid.CorrelationTrace("<sc.SqlCommand.ExecuteNonQueryAsync|API|Correlation> ObjectID%d#, ActivityID %ls\n", ObjectID);
        SqlConnection.ExecutePermission.Demand();   

        TaskCompletionSource<int> source = new TaskCompletionSource<int>();

        CancellationTokenRegistration registration = new CancellationTokenRegistration();
        if (cancellationToken.CanBeCanceled) {
            if (cancellationToken.IsCancellationRequested) {
                source.SetCanceled();
                return source.Task;
            }
            registration = cancellationToken.Register(CancelIgnoreFailure);
        }

        Task<int> returnedTask = source.Task;
        try {
            RegisterForConnectionCloseNotification(ref returnedTask);

            Task<int>.Factory.FromAsync(BeginExecuteNonQueryAsync, EndExecuteNonQueryAsync, null).ContinueWith((t) => {
                registration.Dispose();
                if (t.IsFaulted) {
                    Exception e = t.Exception.InnerException;
                    source.SetException(e);
                }
                else {
                    if (t.IsCanceled) {
                        source.SetCanceled();
                    }
                    else {
                        source.SetResult(t.Result);
                    }
                }
            }, TaskScheduler.Default);
        } 
        catch (Exception e) {
            source.SetException(e);
        }

        return returnedTask;
    }

Which I would summarize as:

  1. Create TaskCompletionSource<int> source = new TaskCompletionSource<int>();
  2. Create a new task using Task<int>.Factory.FromAsync, using the APM "Begin/End" API
  3. Invoke source.SetResult() when the task finishes.
  4. Return source.Task

What is the point of using TaskCompletionSource here and why not to return the task created by Task<int>.Factory.FromAsync() directly? This task also has the result and exception (if any) wrapped.

In C# in a Nutshell book, in the Asynchronous Programming and Continuations section, it states:

In writing Delay, we used TaskCompletionSource, which is a standard way to implement “bottom-level” I/O-bound asynchronous methods.

For compute-bound methods, we use Task.Run to initiate thread-bound concurrency. Simply by returning the task to the caller, we create an asynchronous method.

Why is it that the compute-bound methods can be implemented using Task.Run(), but not the I/O bound methods?


Solution

  • Note that for a definitive answer, you would have to ask the author of the code. Barring that, we can only speculate. However, I think it's reasonable to make some inferences with reasonable accuracy…

    What is the point of using TaskCompletionSource here and why not to return the task created by Task.Factory.FromAsync() directly?

    In this case, it appears to me that the main reason is to allow the implementation to deregister the registered callback CancelIgnoreFailure() before the task is actually completed. This ensures that by the time the client code receives completion notification, the API itself has completely cleaned up from the operation.

    A secondary reason might be simply to provide a complete abstraction. I.e. to not allow any of the underlying implementation to "leak" from the method, in the form of a Task object that a caller might inspect or (worse) manipulate in a way that interferes with the correct and reliable operation of the task.

    Why is it that the compute-bound methods can be implemented using Task.Run(), but not the I/O bound methods?

    You can implement I/O bound operations using Task.Run(), but why would you? Doing so commits a thread to the operation which, for an operation that would not otherwise require a thread, is wasteful.

    I/O bound operations generally have support from an I/O completion port and the IOCP thread pool (the threads of which handle completions of an arbitrarily large number of IOCPs) and so it is more efficient to simply use the existing asynchronous I/O API, rather than to use Task.Run() to call a synchronous I/O method.