【问题标题】:ThreadLocal and awaitThreadLocal 和等待
【发布时间】:2020-06-09 09:45:15
【问题描述】:

我们有一个日志系统,我们使用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..." );
    }
}

问题

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

背景信息

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

【问题讨论】:

  • CultureInfo 设置在每个托管线程的开头——独立于Task。如果您想从Task 登录,我建议显式使用Log.SetLoggerForCurrentThread。它表明您已经明确考虑到这一点并进行了补偿。
  • 在我开始回答之前,您应该知道 log4net 是免费的,并且已经解决了您的大部分问题。
  • 嗨@GuillaumeCR,您可能是对的,但将其视为类似模式的一般编程问题。
  • @PeterRitchie,在代码完成 5 秒后,初级程序员会介入并在所有等待的任务(甚至只是其中的几个 - 应该足以如果记录器是线程本地的,则降低整个记录生态系统。
  • 在工作线程上运行的代码中使用 async/await 的附加值会显着下降。它已经异步运行,使用 await 只会增加开销。像这样的问题,它出错,因为继续在另一个线程上运行。这不是免费的。请记住,CultureInfo 也不会流动,这是 .NET 中的一个臭名昭著的问题。

标签: c# multithreading async-await


【解决方案1】:

您可以使用 CallContext 跨线程传递(可序列化)数据。示例见这篇文章:
https://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html

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

【讨论】:

  • 您应该发布一个有意义的代码示例来说明您的答案。不要在可能会死掉的外部链接上中继。
【解决方案2】:

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

但是,如果您想以与await 兼容的方式隐式存储和传递日志记录实例,则需要使用逻辑调用上下文。我有一个blog post describing this approach,它指出了这种方法的局限性:

  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;
    }
  }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-31
    • 2018-12-09
    • 1970-01-01
    • 2021-04-24
    • 1970-01-01
    • 2015-05-22
    • 2015-08-25
    相关资源
    最近更新 更多