How do the semantics of AsyncLocal differ from the logical call context?

The semantics are pretty much the same. Both are stored in the ExecutionContext and flow through async calls.

The differences are API changes (just as you described) together with the ability to register a callback for value changes.

Technically, there's a big difference in the implementation as the CallContext is cloned each time it is copied (using CallContext.Clone) while the AsyncLocal's data is kept in the ExecutionContext._localValues dictionary and just that reference is copied over without any extra work.

To make sure updates only affect the current flow when you change the AsyncLocal's value a new dictionary is created and all the existing values are shallow-copied to the new one.

That difference can be both good and bad for performance, depending on where the AsyncLocal is used.

Now, as Hans Passant mentioned in the comments CallContext was originally made for remoting, and isn't available where remoting isn't supported (e.g. .Net Core) which is probably why AsyncLocal was added to the framework:

#if FEATURE_REMOTING
    public LogicalCallContext.Reader LogicalCallContext 
    {
        [SecurityCritical]
        get { return new LogicalCallContext.Reader(IsNull ? null : m_ec.LogicalCallContext); } 
    }

    public IllogicalCallContext.Reader IllogicalCallContext 
    {
        [SecurityCritical]
        get { return new IllogicalCallContext.Reader(IsNull ? null : m_ec.IllogicalCallContext); } 
    }
#endif

Note: there's also an AsyncLocal in the Visual Studio SDK that is basically a wrapper over CallContext which shows how similar the concepts are: Microsoft.VisualStudio.Threading.


I'm wondering if and in what ways the two are semantically different

From what can be seen, both CallContext and AsyncLocal internally rely on ExecutionContext to store their internal data inside a Dictionary. The latter seems to be adding another level of indirection for async calls. CallContext has been around since .NET Remoting and was a convenient way of flowing data between async calls where there wasn't a real alternative, until now.

The biggest difference I can spot is that AsyncLocal now lets you register to notifications via a callback when an underlying stored value is changed, either by a ExecutionContext switch or explicitly by replacing an existing value.

// AsyncLocal<T> also provides optional notifications 
// when the value associated with the current thread
// changes, either because it was explicitly changed 
// by setting the Value property, or implicitly changed
// when the thread encountered an "await" or other context transition.
// For example, we might want our
// current culture to be communicated to the OS as well:

static AsyncLocal<Culture> s_currentCulture = new AsyncLocal<Culture>(
args =>
{
    NativeMethods.SetThreadCulture(args.CurrentValue.LCID);
});

Other than that, one resides in System.Threading while the other lives at System.Runtime.Remoting , where the former will be supported in CoreCLR.

Also, it doesn't seem that AsyncLocal has the shallow copy-on-write semantics SetLogicalData has, so the data flows between calls without being copied over.


There appears to be some semantic difference in timing.

With CallContext the context change happens when the context for the child thread/task/async method is set up, i.e. when Task.Factory.StartNew(), Task.Run() or async method are called.

With AsyncLocal the context change (change notification callback being called) happens when the child thread/task/async method actually starts executing.

The timing difference could be interesting, especially if you want the context object to be cloned when the context is switched. Using different mechanisms could result in different content being cloned: with CallContext you clone the content when the child thread/task is created or async method is called; but with AsyncLocal you clone the content when the child thread/task/async method starts executing, the content of the context object could have been changed by the parent thread.