Monitor.TryEnter не работает

Часть моего кода:

object _sync = new object();

private async void OnKeyDown(object sender, KeyEventArgs e) {
    if (!Monitor.TryEnter(_sync)) return;

    Trace.Write("taken...");
    await Task.Delay(TimeSpan.FromSeconds(5));
    Trace.WriteLine(" done");

    Monitor.Exit(_sync);
}

Выход (нажатие несколько раз менее чем за 5 секунд):

taken...taken...taken... done
done
done

Как пришел?? блокировка _sync никогда не выполняется, почему?

Ответ 1

Смешивание Monitor и await является... более чем рискованным. Похоже, что вы пытаетесь сделать это, чтобы он работал только один раз за раз. Я подозреваю, что Interlocked может быть проще:

object _sync = new object();
int running = 0;
private async void OnKeyDown(object sender, KeyEventArgs e) {
    if(Interlocked.CompareExchange(ref running, 1, 0) != 0) return;

    Trace.Write("taken...");
    await Task.Delay(TimeSpan.FromSeconds(5));
    Trace.WriteLine(" done");

    Interlocked.Exchange(ref running, 0);
}

Обратите внимание, что вы также можете подумать, что произойдет, если произошла ошибка и т.д.; как значение становится reset? Вы можете использовать try/finally:

if(Interlocked.CompareExchange(ref running, 1, 0) != 0) return;

try {
    Trace.Write("taken...");
    await Task.Delay(TimeSpan.FromSeconds(5));
    Trace.WriteLine(" done");
} finally {
    Interlocked.Exchange(ref running, 0);
}

Ответ 2

Вы не можете использовать нить-аффинный тип типа Monitor с await. В этом конкретном случае вы всегда получаете блокировку в одном потоке (поток пользовательского интерфейса), и этот тип блокировки позволяет рекурсивную блокировку.

Попробуйте вместо SemaphoreSlim (WaitAsync и Release вместо Enter и Exit):

SemaphoreSlim _sync = new SemaphoreSlim(1);

private async void OnKeyDown(object sender, KeyEventArgs e) {
  await _sync.WaitAsync();

  Trace.Write("taken...");
  await Task.Delay(TimeSpan.FromSeconds(5));
  Trace.WriteLine(" done");

  _sync.Release();
}

Ответ 3

Вы не можете использовать await между вызовами метода Monitor.TryEnter() и Monitor.Exit(). После await контекст потока может быть другим, что означало бы, что поток не имеет блокировки entered, и поэтому он не сможет exit его.

Фактически, компилятор защитит вас, если вы используете ключевое слово lock:

lock(_sync)
{
  await Task.Delay(...); // <- Compiler error...
}

Ответ 4

Что происходит, так это то, что TryEnter будет успешным, если текущий поток уже получил блокировку. Событие KeyDown всегда будет запускаться в потоке диспетчера, в то время как фоновый поток обрабатывает ожидание, а затем помещает разблокировку обратно в поток диспетчера.

Ответ 5

TryEnter будет работать в вашем потоке gui. Действительно, чтобы поток мог получать монитор несколько раз без блокировки, ему просто нужно выпустить их столько же раз.

Ваш вызов Monitor.Exit будет выполняться в контексте, продиктованном вашим вызовом async. Если он заканчивается в потоке, отличном от потока, который называется TryEnter, тогда он не сможет выпустить монитор.

Итак, вы каждый раз приобретаете монитор в одном и том же потоке, который никогда не будет блокироваться, и вы выпускаете его на какой-то другой поток, который может работать. Вот почему вы можете быстро щелкнуть в 5-секундном окне.