什么是 C# 中用于发出唯一计数器值的简单线程安全赋值模式

本文关键字:简单 单线程 安全 模式 赋值 计数器 唯一 用于 什么 | 更新日期: 2023-09-27 17:57:09

我不是 C# 多线程专家;我想确保防止竞争条件,这种竞争条件在测试中很难触发,同时也几乎不可能调试。 要求(我的应用程序是一个实用程序类,用于可能的多线程 HTTP 服务器,而不是使用 IIS 或 ASP.NET)是类的每个实例都具有该实例的唯一标识符。

我宁愿不使用重量级 GUID 来避免此问题,因为它们的序列化长度,就像在 HTML 表示形式中一样。

我的问题是:在下面的Widget类中设置Unique值的简单模式是否适合此要求? 有没有更好、更安全的模式,希望实施起来不会造成负担?

public abstract class Widget : Node
{
    private static int UniqueCount = 0;
    private static object _lock = new object();
    protected int Unique { private set; get; }
    protected Widget() : base()
    {
        // There could be a subtle race condition if this is not thread-safe
        // if it is used with a multi-threaded web server
        lock (_lock)
        {
            UniqueCount += 1;
            Unique = UniqueCount;
        }
    }  
}

什么是 C# 中用于发出唯一计数器值的简单线程安全赋值模式

从这个答案:

如果希望以保证不受争用条件约束DoneCounter = DoneCounter + 1的方式实现属性,则无法在属性的实现中执行此操作。 该操作不是原子操作,它实际上是三个不同的步骤:

  1. 检索 DoneCounter 的值。
  2. 添加 1
  3. 将结果存储在DoneCounter 中。

您必须防止在任何这些步骤之间发生上下文切换的可能性。锁定在 getter 或 setter 内部无济于事,因为锁定范围完全存在于其中一个步骤(1 或 2)中。 如果要确保所有三个步骤一起发生而不会中断,则同步必须涵盖所有三个步骤。这意味着它必须发生在包含所有三个上下文的环境中。 这最终可能会成为不属于包含 DoneCounter 属性的任何类的代码。

使用您的对象的人有责任注意线程安全。 通常,任何具有读/写字段或属性的类都不能以这种方式成为"线程安全"。 但是,如果可以更改类的接口,以便不需要 setter,则可以使其更加线程安全。 例如,如果您知道 DoneCounter 仅递增和递减,那么您可以像这样重新实现它:

private int _doneCounter;
public int DoneCounter { get { return _doneCounter; } }
public void IncrementDoneCounter() { Interlocked.Increment(ref _doneCounter); }
public void DecrementDoneCounter() { Interlocked.Decrement(ref _doneCounter); }

如前所述,+=(和相关)操作不是线程安全的。在进行简单的数字增量时,只需使用 Interlocked.Increment ;它将返回旧值并且是完全线程安全的:

public abstract class Widget : Node
{
    private static int UniqueCount = 0;
    protected int Unique { private set; get; }
    protected Widget() : base()
    {
        Unique = Interlocked.Increment(ref UniqueCount);
    }  
}