这是线程安全吗?

时间:2013-10-25 14:54:14

标签: c# multithreading thread-safety interlocked

正在检查...... _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...
}

6 个答案:

答案 0 :(得分:98)

  

这是线程安全的,对吧?

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

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

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

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

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

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

答案 1 :(得分: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--;
    }
    ...
}

答案 2 :(得分: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();
    ...
}

答案 3 :(得分:22)

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

  • 阅读_count
  • 测试_count >= MAXIMUM
  • 根据以上内容做出决定。
  • 根据做出的决定增加_count

如果你没有让所有这4个步骤都显示为原子,那么就会出现竞争条件。无需锁定即可执行复杂操作的标准模式如下:

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;
}

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

就个人而言,我会完全采用低锁策略。我上面提到的内容存在一些问题。

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

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

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--;
  }
}

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


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

答案 4 :(得分:4)

定义线程安全。

如果你想确保_count永远不会超过MAXIMUM,那么你就不会成功。

你应该做的就是锁定它:

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

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

public void CompletedWorkHandler() 
{
    lock(locker)
    {
        _count--;
    }
    ...
}

您可能还想查看SemaphoreSlim课程。

答案 5 :(得分:0)

如果您不想锁定或移动到信号量,则可以执行以下操作:

if (_count >= MAXIMUM) return; // not necessary but handy as early return
if(Interlocked.Increment(ref _count)>=MAXIMUM+1)
{
    Interlocked.Decrement(ref _count);//restore old value
    return;
}
Task.Run(() => Work());

递增返回递增的值,您可以在其上仔细检查_count是否小于最大值,如果测试失败,则恢复旧值