【问题标题】:Is getting and setting a simple static properties thread safe? [duplicate]获取和设置简单的静态属性线程安全吗? [复制]
【发布时间】:2011-04-01 05:26:29
【问题描述】:

可能重复:
Are C# auto-implemented static properties thread-safe?

在下面的示例类中

static class Shared
{
    public static string[] Values { get; set; }
}

许多读取器线程定期读取Values 字符串数组,而有时单个写入器会使用设置器将整个数组替换为新值。我需要使用 ReaderWriterLock 还是 C# 会自动处理?

编辑:在我的情况下,唯一需要的“线程安全”是:当写入器在读取器搜索值时替换数组时,不会发生任何不好的事情。我不在乎读者是否会立即使用新值,只要他们将来会使用它就可以了

【问题讨论】:

  • 如果您使用 .NET 4,请查看 System.Collections.Concurrent 命名空间。根据您的使用情况,它们也可能满足您的需求。

标签: c# multithreading static thread-safety


【解决方案1】:

这种用法是线程安全的:

string[] localValues = Shared.Values;
for (int index = 0; index < localValues.length; index++)
    ProcessValues(localValues[index]);

这种用法不是线程安全的,并且可能导致越界异常:

for (int index = 0; index < Shared.Values.Length; index++)
    ProcessValues(Shared.Values[index]);

我希望通过执行以下操作使线程安全调用更自然:

static class Shared   
{
    private static string[] values;   
    public static string[] GetValues() { return values; }
    public static void SetValues(string[] values) { Shared.values = values; }
}

当然,用户仍然可以将 GetValues() 放在循环中,它会一样糟糕,但至少它显然很糟糕。

视情况而定,更好的解决方案可能是分发副本,这样调用代码就根本无法改变数组。这是我通常会做的,但它可能不适合你的情况。

static class Shared
{
    private static string[] values;
    public static string[] GetValues()
    {
        string[] currentValues = values;
        if (currentValues != null)
            return (string[])currentValues.Clone();
        else
            return null;
    }
    public static void SetValues(string[] values)
    {
        Shared.values = values;
    }
}

【讨论】:

  • 添加到这个答案:在这种惰性读取场景中不需要锁,因为对共享数组的引用用作原子操作。即使另一个线程即将向共享变量写入新值,从共享变量中读取引用(指针)也是线程安全的。这是“世代”版本控制的一种形式——每次引用共享数据时,它本质上都是数据的不可变快照,一直保持活动状态,直到所有对它的引用都被释放。
  • 澄清:当内存地址是双字对齐时,引用指针的读取在 x86 和 x64 上的硬件级别是原子的,我很确定 .NET 分配器可以保证这一点。非对齐读取可能需要多个时钟周期来读取每一半并将它们组合起来,这为另一个线程中的写入创造了一个机会窗口,以在读取读取第二半之前更改内存中的值。
  • @dthorpe - 重要的是 C# 语言定义保证对对象的引用将以原子方式复制,而不管它在哪个平台上运行或如何实现该机制。对于这样的情况,这是一件非常有用的事情。 :-)
  • 是的,语言定义做出的断言实际上得到了硬件实现的支持,这很好。 :P
  • 无锁线程协调的忠实拥护者。希望我能 + 你的回答更多。
【解决方案2】:

为了稍微偏离主题,Java 2 1.5 内存模型增加了两个保证(参见 Java theory and practice: Fixing the Java Memory Model, Part 2):

  • 易失性读/写也是内存屏障。
  • final 字段在构造函数之外完全初始化。

后者是一个有趣的案例:你曾经能够在一个线程中执行foo = new String(new char[]{'a','b','c'});,在另一个线程中执行foo = "123"; System.out.println(foo) 并打印空字符串,因为无法保证写入 foo 的最终字段会发生在写入 foo 之前。

我不确定 .NET 数组初始化的细节;您可能需要使用 Thread.MemoryBarrier()(在 getter 的开头和 setter 的结尾)来确保读者只能看到完全初始化的数组。

【讨论】:

    【解决方案3】:

    我会锁定。对于多次读取,偶尔写入使用ReaderWriterLockSlim - 比ReaderWriteLock 更有效。

    static class Shared
    {
        private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
        private static string[] _values;
    
        public static string[] Values 
        {
            get
            {
                _rwLock.EnterReadLock();
                try
                {
                    return _values;
                }
                finally
                {
                    _rwLock.ExitReadLock();
                }
            }
            set
            {
                _rwLock.EnterWriteLock();
                try
                {
                    _values = value;
                }
                finally
                {
                    _rwLock.ExitWriteLock();
                }
            }
        }
    }
    

    【讨论】:

    • 一个普通的旧lock 会更快。原因是 RW 锁有一组有限的场景,实际上它们实际上更快。这不是其中的一个。我刚刚在我的机器上做了一个基准测试,lock 快了大约 5 倍。
    • 我同意布赖恩,锁更快,但如果我们谈论的是同时访问数据的大量线程,那么也许这可能有一些优势。不过,从人类的角度来看,差异是微不足道的!
    【解决方案4】:

    这取决于你所说的线程安全。

    读取和替换保证是原子的,但不保证写入后的读取一定会读取新值。

    响应您的修改...

    您的现有代码不会发生任何不良情况(例如,撕裂的读取),但不能保证您的读者会永远看到新值。例如,旧的引用有可能(尽管不太可能)被永久缓存在寄存器中。

    【讨论】:

    • 我用所需的线程安全级别更新了我的问题。
    【解决方案5】:

    如果没有任何额外的保护,则无法保证阅读线程将永远看到新值。在实践中,只要读取线程在做任何有意义的事情,它就会看到新的值。特别是,您永远不会看到“一半”的更新,其引用指向死区。

    如果您填写volatile 字段,我相信甚至可以消除这种危险——但无锁编程通常很难推理。当然,这意味着放弃它作为自动实现的属性。

    【讨论】:

    • @LukeH:可能……我的理解也有限。但是,我相信它“或多或少”确保您始终阅读最后一个值。 (这甚至是 MSDN 对它的描述。)
    【解决方案6】:

    这不是线程安全的。是的,您需要使用锁。

    【讨论】:

    • 如果这个答案包含一些细节——比如你预见的危险,它会更有用(或可证伪)。
    • “线程安全”和“非线程安全”使用过于松散。在这种情况下,读取和写入保证是原子的,但可能不是有序的。锁保留了顺序,但在这里顺序可能并不重要。
    猜你喜欢
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-22
    • 2010-11-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多