这是线程安全的权利

本文关键字:安全 线程 | 更新日期: 2023-09-27 18:36:56

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

这是线程安全的权利

这是线程安全的,对吧?

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

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

这似乎与守则的意图背道而驰。

此外:该场不易失。那么,什么机制可以保证任何线程都会在无内存屏障路径上读取最新值呢? 没有什么能保证这一点!只有当条件为假时,你才会建立记忆屏障。

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

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

不,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--;
    }
    ...
}
这就是

信号量和信号量Slim的用途:

private readonly SemaphoreSlim WorkSem = new SemaphoreSlim(Maximum);
public void CheckForWork() {
    if (!WorkSem.Wait(0)) return;
    Task.Run(() => Work());
}
public void CompletedWorkHandler() {
    WorkSem.Release();
    ...
}

不,你拥有的东西不安全。检查_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保持不变。重要的是,无论两者之间发生了什么,它的值在两个时间点都是相同的。换句话说,问题可以简化为一个看起来值没有改变的问题,即使实际上它可能发生了变化。

定义线程安全。

如果要确保_count永远不会大于最大值,那么您没有成功。

你应该做的是锁定它:

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

您可能还想看看 信号量苗条类。

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

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是否小于最大值,如果测试失败,则恢复旧值