Приведение переменных, захваченных закрытием, изменчивым

Как переменные, захваченные замыканием, взаимодействуют с разными потоками? В следующем примере кода я хотел бы объявить totalEvents как volatile, но С# не разрешает это.

(Да, я знаю, что это плохой код, это просто пример)

private void WaitFor10Events()
{
     volatile int totalEvents = 0; // error CS0106:

     _someEventGenerator.SomeEvent += (s, e) => totalEvents++;

     while(totalEvents < 10)
        Thread.Sleep(100);
}

РЕДАКТИРОВАТЬ. Люди, похоже, немного теряют смысл моего вопроса. Я знаю, что не могу использовать volatile для локальных варов. Я также знаю, что код кода примера плох и может быть реализован другими способами, поэтому мой отказ от "плохого кода". Это было просто для иллюстрации проблемы.

Во всяком случае, казалось бы, нет возможности принудительно изменять волатильную семантику на захваченные локальные переменные, поэтому я буду реализовывать другой способ. Спасибо за ответы, хотя я все-таки узнал пару полезных вещей.:)

Ответ 1

Volatile. Запишите на помощь:

private void WaitFor10Events()
{
     int totalEvents = 0; 

     _someEventGenerator.SomeEvent += (s, e) => Volatile.Write(ref totalEvents, totalEvents+1);

     while(totalEvents < 10)
        Thread.Sleep(100);
}

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

Ответ 2

Неправильно иметь локальную переменную, помеченную как изменчивую. Закрытие может захватывать изменчивые поля, следующее совершенно законно:

volatile int totalEvents = 0;
private void WaitFor10Events()
{
   _someEventGenerator.SomeEvent += (s, e) => totalEvents++;
   ...
}

Смотрите здесь для информации о ключевом слове volatile;

В качестве альтернативы вы можете использовать событие reset (auto, manual), класс монитора (pulse и wait) или событие обратного отсчета, чтобы поток нился, пока не будет поднято событие, оно гораздо эффективнее, чем спать в цикле.

Обновление

Следуя за редактированием вопроса, простой способ получить потокобезопасную семантику - использовать Interlocked class. Чтобы повторно написать свой пример таким образом (хотя, как указано в других ответах, есть лучшие способы написания этого примера):

private void WaitFor10Events()
{
   long totalEvents = 0;
   _someEventGenerator.SomeEvent += (s, e) => Interlocked.Increment(ref totalEvents);

   while(Interlocked.Read(ref totalEvents) < 10)
   {
     Thread.Sleep(100);
   }
}

Ответ 3

Вы не можете объявить locals volatile. Кроме того, есть лучшие способы достижения вашей цели... Вместо этого используйте System.Threading.CountdownEvent. Это будет более эффективно, чем ваш метод опроса/сна.

using(CountdownEvent cde = new CountdownEvent(10))
{
  _someEventGenerator.SomeEvent += (s, e) => cde.Signal();
  cde.Wait();
}

Ответ 4

Это не будет работать, если события будут запущены параллельно. n ++, к сожалению, не является атомарной операцией в .NET, поэтому вы не можете просто ожидать, что несколько потоков выполнят n ++ 10 раз, чтобы фактически увеличить n на 10, его можно увеличить на меньшее. Вот небольшая программа, которая это доказывает (и в этом процессе убедитесь, что затворы правильно обрабатываются при параллельном использовании):

class Program
{
    static volatile int _outer = 0;
    static void Main(string[] args)
    {
        int inner = 0;

        Action act_outer1 = () => _outer++; // Interlocked.Increment(ref _outer);
        Action act_inner1 = () => inner++;  // Interlocked.Increment(ref inner);
        Action<int> combined = (i) => { act_outer1(); act_inner1(); };

        Console.WriteLine("None outer={0}, inner={1}", _outer, inner);
        Parallel.For(0, 20000000, combined);
        Console.WriteLine("Once outer={0}, inner={1}", _outer, inner);
        Console.ReadKey();
    }
}

Вариант Interlocked.Increment работает как ожидалось.

Ответ 5

Локальные переменные, захваченные закрытием, получают "hoisted" в другой класс, сгенерированный компилятором, который не ослабляет "локальные жители могут" t volatile "при этом, даже если локальный" действительно "заканчивается как поле экземпляра. Лучше всего построить класс закрытия вручную (" объект функции") или использовать некоторый инструмент манипуляции с IL, например. ILDASM.