这是线程安全的权利
本文关键字:安全 线程 | 更新日期: 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是否小于最大值,如果测试失败,则恢复旧值