40

只是检查..._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...
}
4

6 回答 6

98

这是线程安全的,对吧?

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

所有五个线程都可以验证计数小于最大值。然后柜台将增加到五个,五个工作将开始。

这似乎与代码的意图相反。

此外:该领域不是易变的。那么什么机制保证任何线程都会在无内存屏障路径上读取最新值?没有什么可以保证!如果条件为假,您只会设置内存屏障。

更笼统地说:你在这里制造了一个虚假的经济。通过使用低锁定解决方案,您可以节省非竞争锁定所需的十几纳秒。 就拿锁吧。你可以承受额外的十几纳秒。

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

于 2013-10-25T15:00:07.090 回答
40

不,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--;
    }
    ...
}
于 2013-10-25T14:59:37.403 回答
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();
    ...
}
于 2013-10-25T16:07:51.617 回答
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 问题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保持不变。重要的是它的值在两个时间点是相同的,无论其间发生了什么。换句话说,问题可以简化为看起来价值没有改变的问题,即使实际上它可能已经改变了。

于 2013-10-25T15:15:46.657 回答
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--;
    }
    ...
}

您可能还想看看 The SemaphoreSlim类。

于 2013-10-25T15:00:22.843 回答
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 是否小于最大值,如果测试失败,则恢复旧值

于 2013-10-25T17:54:02.560 回答