使用原子操作锁定

时间:2011-11-07 15:52:34

标签: c# multithreading atomicity

是的,我知道以下问题可以通过“使用锁定关键字”或类似的方式来回答。但由于这只是为了“有趣”,我不关心那些。

我使用原子操作进行了简单的锁定:

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.我可以忽略什么?

2 个答案:

答案 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