Блокировка с использованием атомарных операций - PullRequest
5 голосов
/ 07 ноября 2011

Да, я знаю, что на следующий вопрос можно было бы ответить «Вместо этого используйте ключевое слово lock» или что-то подобное. Но так как это просто ради удовольствия, мне все равно.

Я сделал простую блокировку с использованием атомарных операций:

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.

Код выглядит так:

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 ]

6 голосов
/ 07 ноября 2011

Я предлагаю вам взглянуть на структуру SpinLock, поскольку она, кажется, делает именно то, что вы хотите.


Тем не менее, все это выглядит очень рискованно, но я попробую.

Вы, похоже, пытаетесь использовать 0 для обозначения "разблокировано" и 1 для обозначения "заблокировано".

В этом случае строка:

if (Interlocked.CompareExchange(ref locked, 1, inLock) == 1)

совсем не правильно. Он просто заменяет переменную locked значением 1 (заблокировано), если ее текущее значение совпадает с последним разом, когда вы читаете ее через inLock = locked (и, если это так, получает блокировку). Хуже того, входит в раздел взаимного исключения, если исходное значение было 1 (заблокировано), что является полной противоположностью того, что вы хотите делать .

На самом деле вы должны атомарно проверять, что блокировка не была взята (исходное значение == 0), и взять ее, если можете (новое значение == 1), используя 0 (разблокировано) как * 1027. * аргумент, а также значение для проверки возвращаемого значения:

if (Interlocked.CompareExchange(ref locked, 1, 0) == 0)

Теперь, даже если вы исправили это, мы также должны быть уверены, что метод List<T>.Add будет «видеть» обновленное внутреннее состояние списка для правильного выполнения добавления. Я думаю Interlocked.CompareExchange использует полный барьер памяти, который должен создать этот приятный побочный эффект, но на это действительно немного опасно полагаться (я никогда нигде не видел этого документированного).

Я настоятельно рекомендую держаться подальше от таких шаблонов с низким уровнем блокировки, за исключением самых тривиальных (и очевидно правильных) сценариев, если вы не являетесь настоящим экспертом в программировании с низким уровнем блокировки. Мы, простые смертные поймем это неправильно.

EDIT : обновлено значение сравнения до 0.

2 голосов
/ 07 ноября 2011

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 сложный

...