【问题标题】:Is a string property itself threadsafe?字符串属性本身是线程安全的吗?
【发布时间】:2009-01-12 09:23:14
【问题描述】:

C# 中的字符串是不可变的和线程安全的。但是,当您拥有公共吸气剂属性时怎么办?像这样:

public String SampleProperty{
    get;
    private set;
}

如果我们有两个线程,第一个在“同一”时间调用'get',第二个在调用'set',会发生什么?

恕我直言,该集合必须像这样将锁设置为线程安全的:

private string sampleField;
private object threadSafer = new object();

public String SampleProperty{
    get{ return this.sampleField; }
    private set{
        lock(threadSafer){
            sampleField = value;
        }
    }
 }

【问题讨论】:

  • “要求”是:所有使用(读取)该属性的线程必须具有相同/最新的值。但只有对象本身会修改值。关键字 'volatile' 是否应该保证这一点?

标签: c# multithreading


【解决方案1】:

大多数答案都使用“原子”一词,好像只需要原子更改。通常情况下,它们不是。

这已在 cmets 中提及,但通常不会在答案中提及 - 这是我提供此答案的唯一原因。 (关于以更粗粒度锁定以允许追加等操作的观点也是完全有效的。)

通常您希望阅读线程查看变量/属性的最新值。那isn't guaranteed by atomicity。举个简单的例子,这是一个 bad 停止线程的方法:

class BackgroundTaskDemo
{
    private bool stopping = false;

    static void Main()
    {
        BackgroundTaskDemo demo = new BackgroundTaskDemo();
        new Thread(demo.DoWork).Start();
        Thread.Sleep(5000);
        demo.stopping = true;
    }

    static void DoWork()
    {
         while (!stopping)
         {
               // Do something here
         }
    }
}

DoWork 可能会永远循环,尽管对布尔变量的写入是原子的 - 没有什么可以阻止 JIT 在 DoWork 中缓存 stopping 的值。要解决此问题,您需要锁定、创建变量 volatile 或使用显式内存屏障。这也适用于字符串属性。

【讨论】:

  • 太棒了。这就是我想听到的。
【解决方案2】:

引用类型字段的 get/set (ldfld/stfld) 被 (IIRC) 保证是原子的,因此这里不应该有任何损坏的风险。所以从 那个 的角度来看它应该是线程安全的,但我个人会将数据锁定在更高级别 - 即

lock(someExternalLock) {
    record.Foo = "Bar";
}

或者也许:

lock(record.SyncLock) {
    record.Foo = "Bar";
}

这允许您对同一个对象进行多次读取/更新作为原子操作,这样其他线程就无法获得无效的对象状态

【讨论】:

  • TomTom:正如 Andreas Huber 在我的回答中的评论中指出的那样,您支持的锁定是自动发生的,但是它不会保护您免受两个线程尝试修改属性的影响,即为什么 Marc 建议锁定属性(较高级别),而不是字段(较低级别)。
  • (题外话,但我欢迎曾经投过反对票的人提供反馈;我不会亲自接受 - 如果回复中有问题,请告诉我)
  • Marc:我的“要求”是所有刚刚读取该值的线程始终具有最新的值。只有“原点”对象才能操纵值(因此是私有设置器)。所以关键字'volatile'应该适合我的需要。希望。
  • @Marc,您将原子性与可见性混淆了。 .Net 中的所有分配都是原子的,但不一定立即可见。可见性需要内存屏障(锁具有隐式内存屏障)。
  • @JaredPar - 是的,我说这是原子的,但不应该假设。事实上,它是原子的——这非常有用!
【解决方案3】:

设置字符串是一个原子操作,即你要么得到新字符串,要么得到旧字符串,你永远不会得到垃圾。

如果你正在做一些工作,例如

obj.SampleProperty = "Dear " + firstName + " " + lastName;

然后字符串连接都发生在调用 set 之前,因此 sampleField 将始终是新字符串或旧字符串。

但是,如果您的字符串连接代码是自引用的,例如

obj.SampleProperty += obj.SampleProperty + "a";

还有你在另一个线程上的其他地方

obj.SampleProperty = "Initial String Value";

那么你需要锁。

假设您正在使用 int。如果您正在分配给 int,并且您从 int 获得的任何值都是有效的,那么您不需要锁定它。

但是,如果 int 保持对两个或多个线程处理的小部件数量的计数,为了使计数准确,您需要锁定 int。 字符串也是同样的情况。

我感觉我没有很好地解释这一点,希望对您有所帮助。

谢谢

体重

【讨论】:

  • 锁定在 OP 建议的级别不会保护您免受 obj.SampleProperty += "a"; 意外结果的影响。通过这个操作,获取一个锁来获取属性,然后释放锁,然后创建一个附加“a”的新字符串,然后再次获取锁来设置字符串。
  • 好点,这是真的。我想这是 Marc Gravells 指出锁定属性分配的地方,而不是字段分配。 + 1 给 Marc(看来他有 +20k 代表是有原因的)
【解决方案4】:

你的第二个代码示例肯定是不对的,因为锁只有在所有变量被访问的地方使用时才会有预期的效果(对于get 设置),所以get 也需要一个锁。

但是,当像这样获取和设置引用类型字段作为属性时,添加锁定语句不会添加任何值。在 .NET 环境中,指针的分配保证是原子的,如果多个线程正在更改一个属性,那么无论如何你都有一个固有的竞争条件(线程可能会看到不同的值;这可能是也可能不是问题)所以几乎没有锁定点。

所以对于它的作用,第一段代码是好的。但是你是否真的想在多线程应用程序中构建固有的竞争条件是另一回事。

【讨论】:

    【解决方案5】:

    这是线程安全的,无需任何锁定。字符串是引用类型,因此只有对字符串的引用被修改。引用的类型保证是原子的(在 32 位系统上为 Int32,在 64 位系统上为 Int64)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-11-07
      • 1970-01-01
      • 1970-01-01
      • 2010-10-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多