Async Local

2015/08/14

no comments

following the previous post about .NET 4.5 new Async API

this post will focus on AsyncLocal API

the subject of this post brought to my awareness while speaking with

my  colleagues Yuval Detinis and Ido Flatow.

AsyncLocal<T> solving the problem of maintaining call context

for logical call rather than the thread context.

while working with async / await or asynchronous API in general

keeping the call context may become a problem.

the thread context may not represent the logical call context.

putting data on the thread local storage may be bad idea while working

with thread pool (either directly or indirectly via Task).

 

the following scenario demonstrate the issue:

Code Snippet
  1. public static void Main()
  2. {
  3.     Task t = Task.Run(() => Execute());
  4. }
  5. private static void Execute()
  6. {
  7.     using (var client = new NonThreadSafeClient())
  8.     {
  9.         string s = client.Read();
  10.         Console.WriteLine(s);
  11.         SomeLogic();
  12.     }
  13. }
  14. private static void SomeLogic()
  15. {
  16.     Invoker();
  17. }
  18. private static void Invoker()
  19. {
  20.     // get the client and write some data
  21. }

because the client is not thread safe the Invoker has to get the same client which

construct the same logical call (or have to pass the client via the call chain)

using TPL (.NET 4) this can be solve by:

Code Snippet
  1. private static void Execute()
  2. {
  3.     using (var client = new NonThreadSafeClient())
  4.     {
  5.         _threadLocal.Value = client;
  6.         string s = client.Read();
  7.         Console.WriteLine(s);
  8.         SomeLogic();
  9.     }
  10. }
  11. private static void SomeLogic()
  12. {
  13.     Invoker();
  14. }
  15. private static void Invoker()
  16. {
  17.     var client = _threadLocal.Value;
  18.     client.Write("X");
  19. }

pay attention to lines 5 and 17.

unfortunately this won’t work with async method like:

Code Snippet
  1. private static async Task SomeLogicAsync()
  2. {
  3.     await Task.Delay(100);
  4.     Invoker();
  5. }
  6. private static void Invoker()
  7. {
  8.     var client = _threadLocal.Value;
  9.     client.Write("X");
  10. }

on line 3 and line 4 may not execute on the same thread, therefore LocalThread<T> is not helping anymore.

,NET 4.6 target this problem with the AsyncLocal<T> API.

this API maintain context value per logical call context,

this context flow through await boundaries (also when you open new Task or Thread within the logical context)

AsyncLocal<T>solve the issue by changing

Code Snippet
  1. private static ThreadLocal<NonThreadSafeClient> _threadLocal =
  2.     new ThreadLocal<NonThreadSafeClient>();

with

Code Snippet
  1. private static AsyncLocal<NonThreadSafeClient> _threadLocal =
  2.     new AsyncLocal<NonThreadSafeClient>();

anything else stay.

 

if you cannot afford moving to .NET 4.6 you can still achieve logical call using the old CallContext API

in this case the solution will look as follow:

Code Snippet
  1. private static async Task ExecuteAsync()
  2. {
  3.     using (var client = new NonThreadSafeClient())
  4.     {
  5.         CallContext.LogicalSetData("Client", client);
  6.         string s = client.Read();
  7.         Console.WriteLine(s);
  8.         await SomeLogicAsync();
  9.     }
  10. }
  11. private static async Task SomeLogicAsync()
  12. {
  13.     await Task.Delay(100);
  14.     Invoker();
  15. }
  16. private static void Invoker()
  17. {
  18.     var client = CallContext.LogicalGetData("Client") as NonThreadSafeClient;
  19.     client.Write("X");
  20. }

check lines 5 and 18.

summary

AsyncLocal<T> is quite cool API but there is one thing I didn’t mention.

if you really having non thread safe operation you should be aware that

multiple thread may execution simultaneously on the same logical call context.

like the case of the following snippet:

Code Snippet
  1. private static async Task ExecuteAsync()
  2. {
  3.     using (var client = new NonThreadSafeClient())
  4.     {
  5.         _threadLocal.Value = client;
  6.         string s = client.Read();
  7.         Console.WriteLine(s);
  8.         Task t1 = SomeLogicAsync();
  9.         Task t2 = SomeLogicAsync();
  10.         await Task.WhenAll(t1, t2);
  11.     }
  12. }

in this case you should protect the usage of the client.

check the following code snippet:

Code Snippet
  1. private static void Invoker()
  2. {
  3.     var client = _threadLocal.Value;
  4.     lock(client)
  5.     {
  6.         client.Write("X");
  7.     }
  8. }

remember that it is very narrow lock because it’s locking the local context object.

furthermore the .NET lock optimization will likely spin lock on the object and not escalate into kernel mode.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*