2017-02-09 3 views
4

У нас есть метод, который поддерживает глобальный индекс последовательности всех событий в нашем приложении. Как и на веб-сайте, ожидается, что такой метод будет безопасным. Поточно-реализация была следующая:Interlocked.Increment и return incremented value

private static long lastUsedIndex = -1; 

public static long GetNextIndex() 
{ 
    Interlocked.Increment(ref lastUsedIndex); 
    return lastUsedIndex; 
} 

Однако мы заметили, что в какой-то не большой нагрузке дублированных индексы появились в системе. Простой тест показал, что для 100000 итераций насчитывается около 1500 дубликатов.

internal class Program 
{ 
    private static void Main(string[] args) 
    { 
     TestInterlockedIncrement.Run(); 
    } 
} 

internal class TestInterlockedIncrement 
{ 
    private static long lastUsedIndex = -1; 

    public static long GetNextIndex() 
    { 
     Interlocked.Increment(ref lastUsedIndex); 
     return lastUsedIndex; 
    } 

    public static void Run() 
    { 
     var indexes = Enumerable 
      .Range(0, 100000) 
      .AsParallel() 
      .WithDegreeOfParallelism(32) 
      .WithExecutionMode(ParallelExecutionMode.ForceParallelism) 
      .Select(_ => GetNextIndex()) 
      .ToList(); 

     Console.WriteLine($"Total values: {indexes.Count}"); 
     Console.WriteLine($"Duplicate values: {indexes.GroupBy(i => i).Count(g => g.Count() > 1)}"); 
    } 
} 

Это может быть исправлено с следующей реализацией:

public static long GetNextIndex() 
{ 
    return Interlocked.Increment(ref lastUsedIndex); 
} 

Однако, я не ясно понять, почему первая реализация не работает. Может ли кто-нибудь помочь мне описать, что происходит в этом случае?

+1

Поскольку 'lastUsedIndex' мог быть обновлен другим вызовом' Interlocked.Increment' между получением результата и возвратом его вызывающему. –

+4

Стандартная ошибка раскроя резьбы, другой поток также может увеличивать переменную прямо до 'return lastUsedIndex;' Маленькие коэффициенты, а не ноль. Код выполняет '[read modify write] read'. Гарантия атомарности, которую вы получаете от Interlocked, длится только для операций в скобках. Вам нужно будет использовать 'lock [read edit write read]', чтобы сделать его безопасным. –

+0

RB, Hans Passant, понял. Может ли кто-нибудь из вас написать это как ответ, так что он будет полностью отвечать на вопрос? –

ответ

2

Если она работала так, как в оригинальном примере, вы могли бы сказать, что она будет работать для общего случая

Interlocked.Increment(ref someValue); 

// Any number of operations 

return someValue; 

Для этого, чтобы быть правдой, вы должны устранить все параллелизм (в том числе и параллелизм, реинтерантность, препрессивное выполнение кода ...) между Increment и возвратом. Хуже того, вам нужно убедиться, что даже если someValue используется между возвратом и Increment, это никоим образом не повлияет на возврат. Другими словами - someValue должно быть невозможно изменить (неизменное) между двумя утверждениями.

Вы можете ясно видеть, что если бы это было так, вам не понадобилось бы Interlocked.Increment в первую очередь - вы просто сделали бы someValue++. Вся цель Interlocked и другие атомные операции - обеспечить, чтобы операция выполнялась сразу (атомарно) или вообще отсутствовала. В частности, он защищает вас от любого типа переупорядочения команд (либо путем оптимизации процессора, либо через несколько потоков, выполняемых параллельно на двух логических процессорах, либо предикатов на одном CPU). Но только в рамках атомной операции. Последующее чтение someValue равно , а не часть той же атомной операции (она сама по себе атома, но две атомные операции также не делают сумму атомной).

Но вы не пытаетесь сделать «Любое количество операций», не так ли? На самом деле, ты.Поскольку существуют другие потоки, выполняемые асинхронно по отношению к вашему потоку, ваш поток может быть прерван одним из этих потоков или потоки могут работать параллельно на нескольких логических процессорах.

В реальной среде, ваш пример обеспечивает все большее поле (так является немного лучше, чем someValue++), но это не дает вам уникальные идентификаторы, потому что все, что вы читаете, someValue в некоторый неопределенный момент времени. Если два потока попытаются выполнить приращение в одно и то же время, оба будут успешными (Interlocked.Increment- атомный), но они также будут читать одинаковое значение от someValue.

Это не означает, что вы всегда хотите использовать возвращаемое значение Interlocked.Increment - если вас больше интересует сам приращение, а не добавочное значение. Типичным примером может быть дешевый метод профилирования - каждый вызов метода может увеличивать общее поле, а затем значение считывается раз в то время, например. среднее число вызовов в секунду.

2

Согласно комментариям, происходит следующее.

Предположим, у нас есть lastUsedIndex == 5 и 2 параллельных резьбы.

Первая нить будет выполнена Interlocked.Increment(ref lastUsedIndex); и lastUsedIndex станет 6. Затем вторая нить выполнит Interlocked.Increment(ref lastUsedIndex);, а lastUsedIndex станет 7.

Тогда оба потока вернут значение lastUsedIndex (помните, что они параллельны). И это значение теперь 7.

Во второй реализации оба потока возвращают результат функции Interlocked.Increment(). Который будет отличаться в каждом потоке (6 и 7). Другими словами, во второй реализации мы возвращаем копию увеличенного значения, и эта копия не затрагивается в других потоках.

+0

Обратите внимание, что теоретически это может произойти даже на однопроцессорной машине (где два потока не будут запускаться одновременно), хотя это, очевидно, будет еще менее вероятно (и в зависимости от того, что именно процессор, компилятор, время выполнения и ОС, возможно, невозможно, хотя и не надежно). Первый поток * может быть предварительно обработан после «Приращения», но перед последующим чтением из «someValue». – Luaan