Introduction
I recently ran into an issue when using async/await with SqlBulkCopy's WriteToServerAsync method and a bespoke implementation of IDataReader, the root cause of which was so surprising that I just had to post about it!
The Problem
The basic process was implemented as follows:
- Set the current thread's culture to Spanish (es-ES)
- Await (configured to preserve the culture) a SqlBulkCopy.WriteToServerAsync call with a custom reader
- Custom reader uses the current thread's culture to decide which resources to use
Expected behaviour:
Due to how async/await and ConfigureAwait preserve the Synchronization Context, it was expected that the custom reader would find the current thread's culture to be Spanish and locate the appropriate Spanish resource files accordingly.
Actual behaviour:
The reader found the current thread's culture to be Spanish, but only sometimes. At varying intervals during a single awaited call to SqlBulkCopy's WriteToServerAsync, the thread on which the reader's Read was executed forgot the culture, reverting to english!
The Investigation
It didn't take too long to identify that the issue was occurring within SqlBulkCopy's WriteToServerAsync method and wasn't anything I had invoked myself, so I popped open the reference source for SqlBulkCopy and through some digging I found this:
private Task WriteRowSourceToServerAsync( int columnCount, CancellationToken ctoken) { Task reconnectTask = _connection._currentReconnectionTask; if (reconnectTask != null && !reconnectTask.IsCompleted) { if (this._isAsyncBulkCopy) { TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(); reconnectTask.ContinueWith((t) => { Task writeTask = WriteRowSourceToServerAsync( columnCount, ctoken); if (writeTask == null) { tcs.SetResult(null); } else { AsyncHelper.ContinueTask( writeTask, tcs, () => tcs.SetResult(null)); } }, ctoken); return tcs.Task; } else { // Trimmed for brevity, check the reference source // for the full method if interested.
Looking at this code, the question immediately became:
Is async/await's SynchronizationContext implicitly
preserved by TPL's ContinueWith?
The Answer
Evidently, the answer is no, TPL's ContinueWith does not implicitly preserve async/await's SynchronizationContext.
Where async/await uses SynchronizationContext, TPL uses TaskScheduler. The only way to preserve the SynchronizationContext with ContinueWith is to pass a TaskScheduler copied from the current SynchronizationContext when calling it:
await Task.Delay(TimeSpan.FromSeconds(1)) .ContinueWith( (t) => { // whatever you like }, // Handy helper method! TaskScheduler.FromCurrentSynchronizationContext()); }
Seeing as I don't own the code in which the call to ContinueWith is being made, adding the call to TaskScheduler.FromCurrentSynchronizationContext isn't really an option.
The quick fix here was to simply restore the current thread's culture to Spanish at the start of the custom reader's Read method before proceeding. (This isn't so much a fix as it is a workaround, but it achieves the desired result with minimum impact. Sometimes doing it cleanly is better than doing it "right".)
Final Thoughts
The take away from this is to remember that just because a method returns an awaitable Task doesn't necessarily mean that the Task being returned uses async/await in its implementation, it could well be using TPL and ContinueWith, in which case your SynchronizationContext won't be preserved.
If you run into an issue where async/await and an asynchronous method you didn't write aren't behaving consistently as expected, be sure to check the reference source for how the asynchronous method is implemented and proceed accordingly.