【问题标题】:How to make a class Thread Safe [closed]如何使类线程安全[关闭]
【发布时间】:2009-08-27 22:12:10
【问题描述】:

我正在编写一个 C# 应用程序。 我有(一种)日志记录类。这个日志类将被许多线程使用。 如何使此类线程安全? 我应该把它作为单身人士吗? 那里的最佳做法是什么? 我可以阅读有关如何使其成为线程安全的文档吗?

谢谢

【问题讨论】:

  • 单例模式并不意味着线程安全。
  • 首先要仔细定义“线程安全”的含义。人们使用这个术语就像它意味着特定的东西,而事实上,它只是意味着“在场景 X 中正常工作”。如果没有“正确”的规范和 X 是什么的声明,你就无法真正实现某些东西并知道你解决了你真正遇到的问题。
  • 查看 Joseph Albahari 的 this great article。文章的一半左右是关于“线程安全”的精彩部分

标签: c# multithreading thread-safety


【解决方案1】:

在 C# 中,任何对象都可以用来保护“临界区”,换句话说,就是不能由两个线程同时执行的代码。

例如,以下将同步对 SharedLogger.Write 方法的访问,因此在任何给定时间只有一个线程正在记录消息。



    public class SharedLogger : ILogger
    {
       public static SharedLogger Instance = new SharedLogger();
       
       public void Write(string s)
       {
          lock (_lock)
          {
             _writer.Write(s);
          }
       }
    
       private SharedLogger() 
       { 
          _writer = new LogWriter();
       }
       
       private object _lock = new object();
       private LogWriter _writer;
    }

【讨论】:

  • 上面的代码就是一个很好的例子。
  • 这很好,但您可以只使 _lock 和 _writer 成为静态,而不必处理使这个东西成为单例。不过,使用现有的记录器仍然会容易得多。
  • 我同意使用现有的记录器实现,但是如果他在处理多线程代码,他最终需要知道如何正确同步对各种资源的访问,并且可以应用相同的过程其他地方...
  • ...关于使用单例类与静态类,我添加了以前被遗忘的 ILogger 接口,以表明在这种情况下,可能需要更换 ILogger 的不同实现。
  • 使用_lock uninitialized可以吗? IE。空
【解决方案2】:

我不确定我是否可以在已经说过的关于使日志记录类线程安全的内容中添加任何内容。如前所述,要做到这一点,您必须同步对资源的访问,即日志文件,以便一次只有一个线程尝试登录它。 C# lock 关键字是执行此操作的正确方法。

但是,我将讨论 (1) 单例方法和 (2) 您最终决定使用的方法的可用性。

(1) 如果您的应用程序将其所有日志消息写入单个日志文件,那么单例模式绝对是要走的路线。日志文件将在启动时打开,在关闭时关闭,单例​​模式非常适合这种操作概念。但是,正如@dtb 指出的那样,请记住将类设为单例并不能保证线程安全。使用 lock 关键字。

(2) 至于该方法的可用性,请考虑以下建议的解决方案:

public class SharedLogger : ILogger
{
   public static SharedLogger Instance = new SharedLogger();
   public void Write(string s)
   {
      lock (_lock)
      {
         _writer.Write(s);
      }
   }
   private SharedLogger()
   {
       _writer = new LogWriter();
   }
   private object _lock;
   private LogWriter _writer;
}

我先说这个方法一般是可以的。它通过Instance 静态变量定义SharedLogger 的单例实例,并通过私有构造函数阻止其他人实例化该类。这是单例模式的精髓,但我强烈建议您阅读并遵循 Jon Skeet 关于singletons in C# 的建议,然后再走得太远。

但是,我想关注的是这个解决方案的可用性。通过“可用性”,我指的是使用此实现记录消息的方式。考虑一下调用的样子:

SharedLogger.Instance.Write("log message");

整个“实例”部分看起来是错误的,但鉴于实现,没有办法避免它。相反,请考虑以下替代方案:

public static class SharedLogger
{
   private static LogWriter _writer = new LogWriter();
   private static object _lock = new object();
   public static void Write(string s)
   {
       lock (_lock)
       {
           _writer.Write(s);
       }
   }
}

请注意,该类现在是静态的,这意味着它的所有成员和方法都必须是静态的。它与前面的示例没有本质区别,但请考虑其用法。

SharedLogger.Write("log message");

编写代码要简单得多。

重点不是贬低前一种解决方案,而是建议您选择的任何解决方案的可用性是一个不容忽视的重要方面。一个好的、可用的 API 可以使代码更简单、更优雅、更易于维护。

【讨论】:

  • 我同意这一点……废话少说。
  • 根据经验,尽可能使用静态类。
  • 我们不能只使用 lock(this) 而不是 lock(_this) 吗? (使用instant作为锁定对象?)
  • 创建一个实例并且什么都不做,即使在肉眼看来也是错误的。这至少要干净得多
  • 如果你充满了静态类,你将如何进行单元测试?用接口制作模拟类似乎是唯一的选择,而且毫无意义。
【解决方案3】:

我会为此使用现成的记录器,因为有几个非常坚固且易于使用。无需自己动手。我推荐Log4Net.

【讨论】:

  • Log4Net 很棒,我用过很多次,一直很满意。
  • 关于日志记录,我同意这一点。但它没有回答问题。
【解决方案4】:
  • 尝试使用局部变量进行大部分计算,然后在一个快速的locked 块中更改对象的状态。
  • 请记住,某些变量可能会在您读取它们和更改状态之间发生变化。

【讨论】:

  • +1 记住第二行,我浪费了 2 周的时间来理解这一点
【解决方案5】:

使用lock(),这样多个线程不会同时使用日志记录

【讨论】:

    【解决方案6】:

    根据 BCS 的回答:

    BCS 描述的是无状态对象的情况。这样的对象本质上是线程安全的,因为它没有自己的变量,可以被来自不同 theads 的调用破坏。

    所描述的记录器确实有一个文件句柄(对不起,不是 C# 用户,也许它被称为 IDiskFileResource 或一些类似的 MS-ism),它必须序列化使用。

    因此,将消息的存储与将它们写入日志文件的逻辑分开。该逻辑一次只能对一条消息进行操作。

    一种方法是:如果记录器对象要保留一个消息对象队列,并且记录器对象仅具有从队列中弹出消息的逻辑,然后从消息对象中提取有用的东西,然后将其写入日志,然后在队列中查找另一条消息 - 然后您可以通过使队列的 add/remove/queue_size/etc 操作线程安全来使该线程安全。它需要记录器类、消息类和线程安全队列(可能是第三个类,其实例是记录器类的成员变量)。

    【讨论】:

    • 另一种选择(在某些情况下)是让记录器调用在本地内存中生成完整记录,然后在一次调用中将其写入输出流。 IIRC 大多数操作系统都提供了一个系统调用,该调用将对流进行原子写入(几乎?)除了资源耗尽之外的任何情况,然后您就会遇到其他问题。
    【解决方案7】:

    在我看来,上面提供的代码不再是线程安全的: 在之前的解决方案中,您必须实例化一个 SharedLogger 的新对象,并且每个对象的 Write 方法都存在一次。

    现在你只有一个 Write 方法,它被所有线程使用,例如:

    线程 1: SharedLogger.Write("线程 1")

    线程 2: SharedLogger.Write("线程 2");

       public static void Write(string s)
       {
           // thread 1 is interrupted here <=
           lock (_lock)
           {
               _writer.Write(s);
           }
       }
    
    • 线程 1 想写消息,但被线程 2 中断(见注释)
    • 线程2覆盖线程1的消息,被线程1中断

    • 线程 1 获得锁并写入“线程 2”

    • 线程 1 释放锁
    • 线程 2 获得锁并写入“线程 2”
    • 线程 2 释放锁

    当我错的时候纠正我...

    【讨论】:

      【解决方案8】:

      如果性能不是主要问题,例如,如果类没有承受很大的负载,那么就这样做:

      让你的类继承 ContextBoundObject

      将此属性应用于您的类[同步]

      您的整个班级现在一次只能由一个线程访问。

      它对诊断确实更有用,因为速度方面它几乎是最坏的情况......但要快速确定“这个奇怪的问题是赛车状况吗”,扔掉它,重新运行测试......如果问题消失了...你知道这是一个线程问题...

      一个更高效的选择是让你的日志类有一个线程安全的消息队列(它接受日志消息,然后将它们拉出来并按顺序处理它们......

      例如,新的并行机制中的 ConcurrentQueue 类是一个很好的线程安全队列。

      或者用户log4net RollingLogFileAppender,已经是thread safe了。

      【讨论】:

        猜你喜欢
        • 2021-08-12
        • 1970-01-01
        • 2014-07-20
        • 1970-01-01
        • 1970-01-01
        • 2013-05-08
        • 1970-01-01
        • 2017-11-17
        • 1970-01-01
        相关资源
        最近更新 更多