Когда SyncLock не помогает
В .NET Framework есть аналог synchronized блоков в Java - в VB.NET это блок SyncLock, а в C# он называется lock. Он гарантирует, что несколько потоков одновременно не будут исполнять код внутри блока - пока один из потоков находится внутри, остальные претенденты ждут своей очереди.
Но есть ситуация, в которой SyncLock не работает.
Делаем такую конструкцию:
1. Метод, который всегда вызывается только в основном (интерфейсном) потоке. Например, обработчик тиков Windows.Forms.Timer или просто метод, который другие потоки вызывают через Control.Invoke.
2. В теле метода размещаем блок SyncLock.
3. Внутри метода, среди прочего, делаем что-нибудь длительное, вызывая в промежутках Application.DoEvents.
4. Обеспечиваем нагрузку на метод - если он таймерный, то тики должны приходить достаточно часто, если он вызывается из других потоков через Invoke, то вызываться он должен достаточно часто, чтобы несколько запросов иногда скапливались в очереди.
И тогда мы обнаружим, что несколько точек исполнения попадают вовнутрь SyncLock одновременно.
Как это получается?
Одной фразой проблема объясняется так: SyncLock обеспечивает блокировку разных потоков, но не рекурсивных вызовов в одном и том же потоке.
И Forms.Timer, и Control.Invoke работают через очередь сообщений Windows. Да, ту самую очередь сообщений, которая тянется ещё с ранних 16-битных версий Windows. Пока одно сообщение обрабатывается, остальные ждут в очереди.
Вызов Application.DoEvents - это требование разобрать и обработать сообщения, накопившиеся в очереди. Пока вся очередь не разберётся, DoEvents не вернёт управление. Разбор очереди происходит в том же самом потоке управления, откуда вызывали DoEvents.
Если одним из сообщений в очереди является требование вызвать наш метод, то получается рекурсивный вызов: метод A вызывает DoEvents, который вызывает метод A. И вот мы снова вошли в наш метод - и SyncLock не заблокировал второй вход (и хорошо, что не заблокировал, а то бы тут всё и зависло). Но если вы такого не ожидали, можете нарваться на неприятный сюрприз.
Отсюда следует рекомендация: если вы используете SyncLock в основном (интерфейсном) потоке, учитывайте возможность повторного входа вовнутрь SyncLock. Размещайте внутри какие-нибудь флажки и внутренние очереди запросов.
Но есть ситуация, в которой SyncLock не работает.
Делаем такую конструкцию:
1. Метод, который всегда вызывается только в основном (интерфейсном) потоке. Например, обработчик тиков Windows.Forms.Timer или просто метод, который другие потоки вызывают через Control.Invoke.
2. В теле метода размещаем блок SyncLock.
3. Внутри метода, среди прочего, делаем что-нибудь длительное, вызывая в промежутках Application.DoEvents.
4. Обеспечиваем нагрузку на метод - если он таймерный, то тики должны приходить достаточно часто, если он вызывается из других потоков через Invoke, то вызываться он должен достаточно часто, чтобы несколько запросов иногда скапливались в очереди.
И тогда мы обнаружим, что несколько точек исполнения попадают вовнутрь SyncLock одновременно.
Как это получается?
Одной фразой проблема объясняется так: SyncLock обеспечивает блокировку разных потоков, но не рекурсивных вызовов в одном и том же потоке.
И Forms.Timer, и Control.Invoke работают через очередь сообщений Windows. Да, ту самую очередь сообщений, которая тянется ещё с ранних 16-битных версий Windows. Пока одно сообщение обрабатывается, остальные ждут в очереди.
Вызов Application.DoEvents - это требование разобрать и обработать сообщения, накопившиеся в очереди. Пока вся очередь не разберётся, DoEvents не вернёт управление. Разбор очереди происходит в том же самом потоке управления, откуда вызывали DoEvents.
Если одним из сообщений в очереди является требование вызвать наш метод, то получается рекурсивный вызов: метод A вызывает DoEvents, который вызывает метод A. И вот мы снова вошли в наш метод - и SyncLock не заблокировал второй вход (и хорошо, что не заблокировал, а то бы тут всё и зависло). Но если вы такого не ожидали, можете нарваться на неприятный сюрприз.
Отсюда следует рекомендация: если вы используете SyncLock в основном (интерфейсном) потоке, учитывайте возможность повторного входа вовнутрь SyncLock. Размещайте внутри какие-нибудь флажки и внутренние очереди запросов.