Блокировка с использованием атомарных операций

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

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

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. Что я упускаю из виду?


person ebb    schedule 07.11.2011    source источник


Ответы (2)


Я предлагаю вам взглянуть на структуру 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 использует полный барьер памяти, который должен создать этот приятный побочный эффект, но это кажется немного опасным, чтобы полагаться на него (я нигде не видел, чтобы это было задокументировано).

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

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

person Ani    schedule 07.11.2011
comment
Спасибо за быстрый ответ :) Если мы на мгновение забудем о SpinLock и т. Д. (Я просто делаю это для удовольствия) - вы абсолютно правы, (ref locked, 1, inLock) == 1 будет оцениваться как true, даже если locked и inLock равны 1. Заменив его на (ref locked, 1, 0) == 1 не решило проблему (по-прежнему печатает числа под фактическим подсчетом). - person ebb; 07.11.2011
comment
Ах, это получилось! Не могу поверить, что я пропустил это. Спасибо! - person ebb; 07.11.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 и использовал полный забор для сброса флага, потому что изменчивость сложна

person Nick Butler    schedule 07.11.2011