14

In a normal/synchronous/single-threaded console app, NDC.Push works fine for managing the 'current item' (potentially at multiple levels of nesting, but just 1 level for this example).

For example:

private static ILog s_logger = LogManager.GetLogger("Program");

static void Main(string[] args)
{
    BasicConfigurator.Configure();

    DoSomeWork("chunk 1");
    DoSomeWork("chunk 2");
    DoSomeWork("chunk 3");
}

static void DoSomeWork(string chunkName)
{
    using (NDC.Push(chunkName))
    {
        s_logger.Info("Starting to do work");
        Thread.Sleep(5000);
        s_logger.Info("Finishing work");
    }
}

This will result in the expect log output, showing a 'chunk X' NDC entry just to the right of 'Program' (the default pattern for the basic configurator)

232 [9] INFO Program chunk 1 - Starting to do work

5279 [9] INFO Program chunk 1 - Finishing work

5279 [9] INFO Program chunk 2 - Starting to do work

10292 [9] INFO Program chunk 2 - Finishing work

10292 [9] INFO Program chunk 3 - Starting to do work

15299 [9] INFO Program chunk 3 - Finishing work

However, I can't figure out how to maintain that using 'normal' async methods.

For instance, trying to do this:

private static ILog s_logger = LogManager.GetLogger("Program");

static void Main(string[] args)
{
    BasicConfigurator.Configure();

    var task1 = DoSomeWork("chunk 1");
    var task2 = DoSomeWork("chunk 2");
    var task3 = DoSomeWork("chunk 3");

    Task.WaitAll(task1, task2, task3);
}

static async Task DoSomeWork(string chunkName)
{
    using (log4net.LogicalThreadContext.Stacks["NDC"].Push(chunkName))
    //using (log4net.ThreadContext.Stacks["NDC"].Push(chunkName))
    {
        s_logger.Info("Starting to do work");
        await Task.Delay(5000);
        s_logger.Info("Finishing work");
    }
}

Shows them all starting "normally", but when the task completes on a different thread, the stack is lost (I was hoping the log4net.LogicalThreadContext would be TPL-'aware' I guess).

234 [10] INFO Program chunk 1 - Starting to do work

265 [10] INFO Program chunk 2 - Starting to do work

265 [10] INFO Program chunk 3 - Starting to do work

5280 [7] INFO Program (null) - Finishing work

5280 [12] INFO Program (null) - Finishing work

5280 [12] INFO Program (null) - Finishing work

Outside of adding a new TaskContext (or the like) to log4net, is there a way of tracking this kind of activity?

The goal is really to do so with the async/await syntax sugar - either forcing some kind of thread affinity or doing things like keeping a concurrent dictionary around keyed by task are likely workable options, but I'm trying to keep as close to the synchronous version of the code as possible. :)

Community
  • 1
  • 1
James Manning
  • 13,429
  • 2
  • 40
  • 64
  • 6
    FYI, I recently discovered that Microsoft fixed `CallContext` in .NET 4.5 RTW to work with `async`. So log4net's NDC and other solutions using `Logical*Data` will work as expected with `async` methods (on .NET 4.5 only). – Stephen Cleary Mar 08 '13 at 15:20
  • I'm not sure it's a problem coming from the logical thread context. The log4net implementation looks wrong to me because parent and child thread share the same stack. The child should receive a clone of the parent stack so that if the parent modifies the stack it doesn't perturb the child... – dou bret Nov 26 '14 at 15:20

1 Answers1

17

There isn't a good story for async logical call contexts right now.

CallContext can't be used for this. The logical CallContext doesn't understand how async methods return early and resume later, so it will not always work correctly for code that uses simple parallelism such as Task.WhenAll.

Update: CallContext was updated in .NET 4.5 RTW to correctly work with async methods.

I looked into log4net; LogicalThreadContext is documented as using CallContext, but there was a bug that made it use the non-logical contexts (fixed in their SVN on Feb 2, 2012; the current 1.2.11 release does not include that fix). Even when it's fixed, though, it'll still not work with async (because the logical CallContext doesn't work with async).

When I need an async logical call context, I make a class that contains the context data and keep all of my async methods in a functional style as instance members of that class. This is certainly not an ideal solution, but it's a dirty hack that works.

In the meantime, please upvote the suggestion that Microsoft provide some mechanism for this.

P.S. A concurrent dictionary keyed by Task won't work, because async methods are not necessarily running tasks (i.e., in your example code, at the using statement, Task.CurrentId would be null because there is no task actually executing at that point).

And thread affinity has its own problems, too. You actually end up needing a separate thread for each independent asynchronous operation. Bye-bye, scalability...

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Sorry I wasn't clear - for the concurrent dictionary approach, I wouldn't be using async methods at all, but instead would be 'explicitly' using tasks (so, what you can already do in VS2010/.NET4). Kind of wish I could just attach data to a Task (well, without subclassing and making my own) and get the 'current task context' similar to HttpContext.Current, although I haven't really thought that through, either. :) – James Manning Mar 20 '12 at 18:22
  • I see. Well, if you want to attach data to a `Task` (other than `AsyncState`), you could use [Connected Properties](http://connectedproperties.codeplex.com/), but IMO it's just as easy to use `ConcurrentDictionary` for this scenario. – Stephen Cleary Mar 20 '12 at 18:30
  • @StephenCleary Stephen Toub replied to your linked post on User Voice saying that your initial information was outdated and CallContext is appropriate for async/await. – Lex Aug 28 '12 at 08:39
  • @Lex: Updated. Note that `CallContext` - while it *can* be used - still won't solve the problem. – Stephen Cleary Jan 06 '13 at 01:04
  • It looks like this is finally fixed in log4net 1.2.14 - see http://stackoverflow.com/a/28868941/2144 – Eric Pohl Apr 26 '16 at 18:22
  • @Eric Pohl, I'm on 1.2.15 and LogicalThreadContext still doesn't persist with async/await. – Sergey Akopov Nov 18 '16 at 21:08
  • I'm using log4net 2.08 and the LogicalThreadContext properties are indeed carried across different threads when the "async" keyword is used. – jk1990 Aug 23 '18 at 06:13