Когда SyncLock не помогает
Tuesday, 17 July 2007 21:20В .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. Размещайте внутри какие-нибудь флажки и внутренние очереди запросов.
no subject
Date: Tuesday, 17 July 2007 19:09 (UTC)no subject
Date: Tuesday, 17 July 2007 19:18 (UTC)no subject
Date: Tuesday, 17 July 2007 19:23 (UTC)Порадовало, что "дальше не программистам неинтересно". А до того - интересно было, стало быть? :)
no subject
Date: Tuesday, 17 July 2007 19:53 (UTC)эти грабли от смешивания двух подходов. правильно либо использовать готовую очередь приложения и один поток, либо писать в несколько потоков и очередь продумывать самому. посередине получаются странные мутанты.
no subject
Date: Thursday, 19 July 2007 14:57 (UTC)no subject
Date: Wednesday, 18 July 2007 01:41 (UTC)no subject
Date: Thursday, 19 July 2007 06:18 (UTC)no subject
Date: Friday, 20 July 2007 15:05 (UTC)no subject
Date: Wednesday, 18 July 2007 06:46 (UTC)Все мое естество против! Оно говорит, что внутри спинлоков решедулинга не бывает.... иначе -- кернел упс... %)
Вообще-то да, можно сделать SyncLock маленьким (без DoEvents-ов), в котором только флажок устанавливаешь и во внутреннюю очередь потоки кладешь --- и будить эти потоки потом.. но разве какой-нибудь стандартной фичи для этого нет?
типа, waitqueue? или как там ее?
no subject
Date: Thursday, 19 July 2007 06:17 (UTC)SyncLock не спинлок, а обыкновенный мьютекс. Естественно, с разрешением решедулинга и очередью ожидающих. Если уж вспоминать про линукс, то его надо сравнивать или с тем зверем, который лочится по down() (в ядре), или с тем, который pthread_mutex_lock().
Но при этом, судя по рассказу, он рекурсивный (то есть намеренно сделан так что позволяет одной нити лочить много раз, и держит для этого счётчик).
> Вообще-то да, можно сделать SyncLock маленьким (без DoEvents-ов),
DoEvents - это не в нём, а в прикладном коде. Никто не заставляет приложение отрабатывать очередь, просто сидя в ожидании мьютекса - это был бы полный изврат.:)
> типа, waitqueue? или как там ее?
Конечно, есть. Иначе бы не работало.
no subject
Date: Wednesday, 18 July 2007 10:42 (UTC)no subject
Date: Thursday, 19 July 2007 06:11 (UTC)Так это ещё хорошо, что он рекурсивный и даёт аккуратно войти и выйти. Если бы не был таким - нить бы зависла при повторном входе, и всё... а раз сделали рекурсивным - значит, почуяли проблему. Если ещё и в описании написали зачем это сделали и какие грабли - было бы совсем хорошо.