ThreadLocal and await

本文关键字:await and ThreadLocal | 更新日期: 2023-09-27 18:25:17

我们有一个日志系统,在该系统中我们使用Log.Info,它向ILogger写入。

现在,我们有多个工作人员在后台运行,我们希望这些工作人员写入自己的日志。因此,所有的东西都是按Worker捆绑的。执行此任务时记录的所有内容都应转发到其自己的记录器。

我们正在考虑制作一个方法Log.SetLoggerForCurrentThread,并用ThreadLocal实现它。执行的代码看起来像这样:

public class Worker
{
    ILogger _Logger;
    public void ExecuteTask()
    {
        Log.Info( "This goes to the 'Global' logger" );
        using ( Log.SetLoggerForCurrentThread(_Logger) )
        {
             Log.Info( "This goes to local logger" );
             DoWork();
        }
    }
    private async void DoWork()
    {
        Log.Info( "Starting..." );
        // SomeMethod does some logging, 
        // that also needs to be forwared to the local logger
        var value = await SomeDeepDomainClass.SomeMethod();
        // if we use ThreadLocal, and this thread has been reused, 
        // it could be a completely different logger that is now attached.
        Log.Info( "Ended..." );
    }
}

问题

  • 当我们使用wait时,理论上线程可以处理另一个工作线程上的工作,从而混淆本地记录器
  • 做类似事情的最佳模式是什么?我可以使用什么存储方法
  • CultureInfo如何处理此问题

背景信息

这些Worker中的大多数将在Azure WorkerRole实例中运行,但偶尔也会从控制台应用程序触发(一次)。

ThreadLocal and await

您可以使用CallContext在线程之间传递(可序列化)数据。有关示例,请参阅本文:
https://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html

有关一些背景信息,请参阅本文:
https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/

在我看来,最好的解决方案是将记录器实例作为参数(或成员变量)传递,或者注入它们(例如,使用嵌套作用域)。

但是,如果希望以与await兼容的方式隐式存储和传递日志记录实例,则需要使用逻辑调用上下文。我有一篇博客文章描述了这种方法,其中指出了这种方法的局限性:

  1. 它只适用于完整的.NET 4.5框架
  2. 必须使用"覆盖"语义。这通常意味着只存储不可变的数据

考虑到这一点,这里有一些代码应该能满足您的需求:

public static class LocalLogger
{
  private static readonly string name = Guid.NewGuid().ToString("N");
  // Static Log methods should read this.
  public static ILogger CurrentLogger
  {
    public get
    {
      var ret = CallContext.LogicalGetData(name) as ILogger;
      return ret == null ? Logger.GlobalLogger : ret;
    }
    private set
    {
      CallContext.LogicalSetData(name, value);
    }
  }
  // Client code uses this.
  public static IDisposable UseLogger(ILogger logger)
  {
    var oldLogger = CurrentLogger;
    CurrentLogger = logger;
    if (oldLogger == GlobalLogger)
      return NoopDisposable.Instance;
    return new SetWhenDisposed(oldLogger);
  }
  private sealed class NoopDisposable : IDisposable
  {
    public void Dispose() { }
    public static readonly Instance = new NoopDisposable();
  }
  private sealed class SetWhenDisposed : IDisposable
  {
    private readonly ILogger _oldLogger;
    private bool _disposed;
    public SetWhenDisposed(ILogger oldLogger)
    {
      _oldLogger = oldLogger;
    }
    public void Dispose()
    {
      if (_disposed)
        return;
      CurrentLogger = _oldLogger;
      _disposed = true;
    }
  }
}