是的,我知道以下问题可以通过“使用锁定关键字”或类似的方式来回答。但由于这只是为了“有趣”,我不关心那些。
我使用原子操作进行了简单的锁定:
public class LowLock
{
volatile int locked = 0;
public void Enter(Action action)
{
var s = new SpinWait();
while (true)
{
var inLock = locked; // release-fence (read)
// If CompareExchange equals 1, we won the race.
if (Interlocked.CompareExchange(ref locked, 1, inLock) == 1)
{
action();
locked = 0; // acquire fence (write)
break; // exit the while loop
}
else s.SpinOnce(); // lost the race. Spin and try again.
}
}
}
我在一个简单的for循环中使用上面的锁,它将一个字符串添加到普通List<string>
,目的是在add
方法包装在Enter
内部时保证线程安全来自LowLock
的方法{1}}。
代码如下:
static void Main(string[] args)
{
var numbers = new List<int>();
var sw = Stopwatch.StartNew();
var cd = new CountdownEvent(10000);
for (int i = 0; i < 10000; i++)
{
ThreadPool.QueueUserWorkItem(o =>
{
low.Enter(() => numbers.Add(i));
cd.Signal();
});
}
cd.Wait();
sw.Stop();
Console.WriteLine("Time = {0} | results = {1}", sw.ElapsedMilliseconds, numbers.Count);
Console.ReadKey();
}
现在棘手的部分是,当主线程点击列出列表中元素的时间和数量的Console.WriteLine
时,元素的数量应该等于给予CountdownEvent
的计数( 10000) - 它大部分时间都有效,但有时候列表中只有9983个元素,其他时间是9993.我可以忽略什么?
答案 0 :(得分:6)
我建议您查看SpinLock
结构,因为它似乎完全符合您的要求。
那就是说,这一切看起来都很冒险,但我会捅它。
您似乎尝试使用0
表示'已解锁'而1
表示'已锁定'。
在哪种情况下行:
if (Interlocked.CompareExchange(ref locked, 1, inLock) == 1)
根本不正确。它只是将locked
变量替换为1
(已锁定)的值,如果其当前值与上次通过inLock = locked
读取时相同(如果是,则获取锁定) 。更糟糕的是,如果原始值为1
(已锁定),则进入互斥部分,这与您要执行的操作完全相反。
你应该通过原子方式检查锁定是否未被采用(原始值== 0),如果可以,则采用它(新值== 1),使用0
(解锁)作为comparand
参数以及测试返回值的值:
if (Interlocked.CompareExchange(ref locked, 1, 0) == 0)
现在,即使您修复此问题,我们还需要确保List<T>.Add
方法将“看到”列表的最新内部状态以正确执行追加。我认为 Interlocked.CompareExchange
使用完整的内存屏障,这应该会产生这种令人愉快的副作用,但这似乎有点危险依赖(我从未在任何地方看到过这种情况)。
除非你是低锁编程的真正专家,否则我强烈建议远离这种低锁模式,除非在最简单(和显然正确)的情况下。我们凡人将弄错了。
编辑:将比较值更新为0
。
答案 1 :(得分:2)
Interlocked.CompareExchange
返回变量的原始值,因此您需要这样的内容:
public class LowLock
{
int locked = 0;
public void Enter( Action action )
{
var s = new SpinWait();
while ( true )
{
// If CompareExchange equals 0, we won the race.
if ( Interlocked.CompareExchange( ref locked, 1, 0 ) == 0 )
{
action();
Interlocked.Exchange( ref locked, 0 );
break; // exit the while loop
}
s.SpinOnce(); // lost the race. Spin and try again.
}
}
}
我删除了volatile并使用了完整的围栏来重置标志,因为volatile is hard