这是线程安全吗?

And*_* A. 41 c# multithreading interlocked thread-safety

只是检查...... _count正在安全访问,对吗?

两个方法都由多个线程访问.

private int _count;

public void CheckForWork() {
    if (_count >= MAXIMUM) return;
    Interlocked.Increment(ref _count);
    Task t = Task.Run(() => Work());
    t.ContinueWith(CompletedWorkHandler);
}

public void CompletedWorkHandler(Task completedTask) {
    Interlocked.Decrement(ref _count);
    // Handle errors, etc...
}
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 98

这是线程安全的,对吗?

假设MAXIMUM为1,count为零,五个线程调用CheckForWork.

所有五个线程都可以验证计数小于MAXIMUM.然后,计数器将被提升至五个,并且将开始五个工作.

这似乎违背了代码的意图.

而且:该领域并不易变.那么什么机制可以保证任何线程都能读取无内存屏障路径上的最新值?没有什么可以保证的!如果条件为false,则仅创建内存屏障.

更一般地说:你在这里制造虚假的经济.通过使用低锁定解决方案,您可以节省无争议锁定所需的十几纳秒. 拿锁.你可以承受额外的十几纳秒.

更普遍的是:除非您是处理器体系结构专家并且知道允许CPU在低锁路径上执行的所有优化,否则不要编写低锁代码.你不是这样的专家.我也不是.这就是我不写低锁代码的原因.

  • @ Unlimited071:什么曾经让你认为读取值类型是*安全*?读取值类型甚至不是*atomic*,除非它是(对齐的)整数或更小!原子性只是线程安全的一个非常小的方面.你正在进行原子读取和原子增量; 因为那些是*两个*事物,**操作放在一起不是原子**,它肯定不会在返回路径上形成障碍. (24认同)
  • @ Unlimited071当您尝试在不使用语言的"锁定"功能的情况下同步访问并使用其他内容(例如"Interlocked.*")时,使用术语低锁定代码,就像在代码中进行同步一样.Eric建议除非您是使用低锁技术的专家,否则最好使用'lock'关键字来实现同步.使用'lock'关键字的代码更易于编写,维护和推理. (9认同)
  • @BlueMonkMN:是的,这很重要.在32位框架中,`long`的读写不保证是原子的.因此缺少同步,一个线程可能正在读取而另一个线程正在写入,并且最终得到前一个值的高32位和新值的低32位.我会让你决定这是否是一个潜在的问题. (5认同)
  • @BlueMonkMN:首先:如果你只是*只读*那么你已经解决了线程安全问题.第二,好吧,我们假设读取是非原子的,写入是原子的.现在我们从虚假中推理,但让我们继续吧.你原子地写DEADBEEFBAADF00D为long.你以非原子方式读取DEADBEEF,然后原子写入0000000000000000,然后非原子地读取00000000,并且你已经读过DEADBEEF00000000,这是一个从未写过的值.即使写入是原子的,非原子读取也足以使访问64位长不安全.他们不是. (3认同)
  • @BlueMonkMN:我理解你的观点; 我不明白你为什么要这样做.读取和写入*是*非原子的,故事结束.事实上,在一个我们不知道的世界里,很难推断哪一个*必然是非原子的,这是无趣的; 我们不是生活在那个世界里.如果我们生活在一个巨大的尘埃云中的太阳系中的孤独星球上,那么很难分辨出太阳是绕行星还是行星旋转.我们不是生活在这样一个星球上,我们知道地球围绕着太阳,因此反事实是无关紧要的. (3认同)
  • @ Unlimited071 [取决于线程安全的含义](http://blogs.msdn.com/b/ericlippert/archive/2009/10/19/what-is-this-thing-you-call-thread- safe.aspx).有些操作可以在没有锁的情况下安全地完成,有些则不能.除非你知道自己在做什么,以及你希望如何完成,否则你无法真正谈论该计划是否"安全".如果在特定情况下使用代码可能是安全的,如果在另一种情况下使用则代码不安全.无论如何使用,都没有任何东西可以完全保存. (2认同)

Dav*_* S. 41

不,if (_count >= MAXIMUM) return;不是线程安全的.

编辑:你也必须锁定读取,然后逻辑上应该与增量分组,所以我重写像

private int _count;

private readonly Object _locker_ = new Object();

public void CheckForWork() {
    lock(_locker_)
    {
        if (_count >= MAXIMUM)
            return;
        _count++;
    }
    Task.Run(() => Work());
}

public void CompletedWorkHandler() {
    lock(_locker_)
    {
        _count--;
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)


Jim*_*hel 36

这就是SemaphoreSemaphoreSlim的用途:

private readonly SemaphoreSlim WorkSem = new SemaphoreSlim(Maximum);

public void CheckForWork() {
    if (!WorkSem.Wait(0)) return;
    Task.Run(() => Work());
}

public void CompletedWorkHandler() {
    WorkSem.Release();
    ...
}
Run Code Online (Sandbox Code Playgroud)


Bri*_*eon 22

不,你有什么不安全.检查是否_count >= MAXIMUM可以与Interlocked.Increment来自另一个线程的调用竞争.其实,这是真的很难使用低锁定技术来解决.为了使其正常工作,您需要使一系列的几个操作显示为原子而不使用锁.那是困难的部分.这里涉及的一系列操作是:

  • _count
  • 测试 _count >= MAXIMUM
  • 根据以上内容做出决定.
  • 增加_count取决于做出的决定.

如果你没有让所有这四个步骤看起来都是原子的那么就会出现竞争条件.用于在不进行锁定的情况下执行复杂操作的标准模式如下.

public static T InterlockedOperation<T>(ref T location)
{
  T initial, computed;
  do
  {
    initial = location;
    computed = op(initial); // where op() represents the operation
  } 
  while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
  return computed;
}
Run Code Online (Sandbox Code Playgroud)

注意发生了什么.重复执行该操作,直到ICX操作确定初始值在首次读取的时间和尝试改变它的时间之间没有改变.这是标准模式,并且由于CompareExchange(ICX)调用而发生魔术.但请注意,这并未考虑ABA问题.1

能做什么:

因此,采用上述模式并将其合并到您的代码中将导致此问题.

public void CheckForWork() 
{
    int initial, computed;
    do
    {
      initial = _count;
      computed = initial < MAXIMUM ? initial + 1 : initial;
    }
    while (Interlocked.CompareExchange(ref _count, computed, initial) != initial);
    if (replacement > initial)
    {
      Task.Run(() => Work());
    }
}
Run Code Online (Sandbox Code Playgroud)

就个人而言,我会完全采用低锁策略.我上面提到的有几个问题.

  • 这可能实际上比采取硬锁定运行速度慢.原因很难解释,超出了我的答案范围.
  • 与上述内容的任何偏差都可能导致代码失败.是的,真的很脆弱.
  • 很难理解.我的意思是看看它.这很难看.

该做什么:

使用硬锁定路径,您的代码可能如下所示.

private object _lock = new object();
private int _count;

public void CheckForWork() 
{
  lock (_lock)
  {
    if (_count >= MAXIMUM) return;
    _count++;
  }
  Task.Run(() => Work());
}

public void CompletedWorkHandler() 
{
  lock (_lock)
  {
    _count--;
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,这更简单,并且更不容易出错.实际上你可能会发现这种方法(硬锁)实际上比我上面显示的更快(低锁).同样,原因很棘手,有一些技术可以用来加快速度,但它超出了这个答案的范围.


1在这种情况下,ABA问题实际上不是问题,因为逻辑不依赖于_count保持不变.重要的是它的价值在两个时间点是相同的,无论两者之间发生了什么.换句话说,问题可以减少到一个似乎价值没有改变的问题,即使实际上它可能有.