Pull to refresh

Comments 10

Так где дедлок? Буквально первая рекомендация времён async-await - делать ConfigureAwait(false) и не лочить UI поток, она же получается решает вопросы.

Ну, раз вы настаиваете...
Совет «не лочить UI-поток и ставить ConfigureAwait(false)» абсолютно верный и справедливый и я в статье с ним не спорю. Я вообще про то, не про то, как избежать дедлока, а про то, почему он возникает.
Вы пишите: "ConfigureAwait(false) же решает". Он решает один частный случай, когда продолжение после await хочет вернуться на UI-поток. Ок, с этим все гуд.
Но:
ConfigureAwait(false) вообще мимо когда:
- .Result / .Wait на коде, который вы не контролируете;
- lock, Sleep, блокирующий I/O, тяжелое вычисление прямо на UI-потоке повесят цикл сообщений независимо ни от какого ConfigureAwait;
- ну самое и главное, COM-сторона: если реальный STA-COM-объект (Office interop, shell-расширение, out-of-proc сервер) попытается вызвать вас обратно на UI-поток, а вы его заблокировали - это не async-продолжение, а межапартаментный COM-вызов по той же очереди, и ConfigureAwait тут ни при чём.
Так что дедлок вполне реальный и до сих пор массовый.
В статье хотел показать единый механизм под всеми этими случаями: заблокировал STA-поток → встал цикл сообщений → доставка, будь то async-продолжение или COM-вызов вызывают удивление на лице разраба

- .Result / .Wait на коде, который вы не контролируете;

Засовываем его в Task.Run и STA-треду должно быть хорошо. Не идеальное решение, но рабочее.

- lock, Sleep, блокирующий I/O, тяжелое вычисление прямо на UI-потоке повесят цикл сообщений независимо ни от какого ConfigureAwait;

и их тоже в Task.Run, да.

ну самое и главное, COM-сторона...

вот тут я уже не уверен, возможно действительно там можно устроить дедлок легким движением. Работал с офис-интеропом и с 1с старым, но там вся апишка выглядела как "синхронная", но при этом вроде не завязывалась на UI-тред и могла работать вне его. Не уверен, допускаю что эта проблема актуальна и её стоит опасаться.

Не блокировать UI-поток\очередь сообщений в целом хорошо, тут соглашусь полностью.

Спасибо за честный, конструктивный и въедливый ответ, который приятно читать, редкое качество, честно.
По первым двум пунктам - отдать все на съедение Task.Run, абсолютно согласен, не идеально, но рабочее решение.
Насчет Office-interop и 1С - вы правы 100%, они умеют работать вне UI-треда, но причина не в Task.Run, а в привязке самого объекта:он или создавался и обрабатывался в отдельном потоке, либо он был threadingModel=Both, то есть не был привязан занятому UI-треду. То есть всё упиралось в апартамент и threading model объекта, а не в то, откуда вы его дергаете.
По поводу COM-стороны. Я в статье пытался показать не то, как решить проблему непонятного дедлока, а почему он возникает.
На уровне COM Task.Run не работает так, как вы от него ждете. STA-объект привязан к апартаменту, то есть к потоку, который его создал и вызовы к нему обязаны обслуживаться на этом потоке. Вызовом Task.Run() с пул-потока маршалится обратно в апартамент объекта, то есть в ваш UI-тред, и если тот заблокирован и не обрабатывает очередь, то получите дедлок.
Что я хотел сказать в статье - не давать вам рабочее решение, там нет таких советов. Я про то, что понимание механизма на уровне "я думал это дно, но тут снизу постучали" должно предостеречь от неочевидных ошибок

Этот дедлок является системным ограничением среды исполнения, где любой блокирующий вызов в UI-потоке не просто стопорит поток, а перекрывает единственный доступный транспорт для маршалинга ответов и обработки событий

Плюс от меня, согласен абсолютно. Могу только отметить, что это похоже на костыль, вплетать LPC-вызов в очередь сообщений PeekMessageWDispatchMessageW

COM тут ни при чём. Циклов обработки сообщений в программе может быть несколько. Второй можно сделать в явном виде или неявно через модальные диалоги (MessageBox, DialogBoxParam). Это работает без всякого COM. В документации по оконным процедурам WndProc сказано, что процедура должна быть реентерабельной (reentrant — повторно входимой). Это требование связано с особенностями обработки сообщений в Windows, где одна и та же процедура может вызываться несколько раз для обработки разных сообщений, поступающих в систему.

Насчёт голого Win32, да, и я с этим не спорю. Вложенные циклы сообщений, модальные диалоги (MessageBox etc) со своим циклом, требование реентерабельности WndProc это всё чистый Win32, не COM.
Но статья немного о другом.
Реентерабельность это когда поток продолжает обрабатывать новые сообщения, не завершив текущий вызов: отсюда повторный вход в процедуру. COM-дедлок - вещь обратная по смыслу, поток перестаёт обрабатывать сообщения, пока в очереди ждёт входящий вызов.
STA-объект привязан к потоку-создателю; синхронный межапартаментный вызов к нему маршалится, доставляется в поток-владелец как сообщение и обслуживается, только когда тот обрабатывает свою очередь. Заблокируйте этот поток так, что он перестанет разбирать сообщения, и если на нём же висит ожидание, получаете цикл: A ждёт результат от B, B синхронно вызывает STA-объект на A, вызов стоит в очереди A, а A заблокирован и до очереди не доходит. Это и есть дедлок. А голый Win32-поток, который заблокировали, просто зависнет, не рисует, не отвечает, но это не дедлок: цикла ожидания нет, он выдохнет и возобновит работу по окончанию блокировки.

Есть какой‐нибудь минималистичный пример, который демонстрирует проблему?

Минимальный пример, где COM - именно тот случай.

using Excel = Microsoft.Office.Interop.Excel;

// Главный поток приложения - STA. Объект Excel создаётся здесь
// и навсегда привязан к этому апартаменту, т.е.к этому потоку).
var excel = new Excel.Application();

bool done = false;

// Фоновая работа на потоке пула выполняется уже в другом апартаменте
Task.Run(() =>
{
// Обращение к STA-объекту из чужого потока. COM маршалит вызов
// обратно в апартамент-владелец,т.е в главный поток.
int n = excel.Workbooks.Count;
done = true;
});

// Блокировка без обработки очереди на главном потоке:
// входящий вызов к excel не будет доставлен никогда.
while ( !done ) Thread.Sleep(50); // дальше этой строки управление не уйдёт
//endcode

excel.Workbooks.Count вызывается с потока пула, но обслужить его обязан поток-владелец Excel - он тут главный. Вызов маршалится туда и ждёт, пока главный поток разберёт свою очередь сообщений. Тем временем главный поток Excel сидит в Thread.Sleep и очередь не трогает. Worker ждёт главного, главный ждёт worker'а и в итоге получаем дедлок.
Причем здесь COM, а не «просто блокировка»: замените excel на обычный объект напримерvar list = new List<int>(); int n = list.Count; и зависания не будет. Поток пула просто читает поле, без маршалинга, done мгновенно ==true. Структура та же, блокировка та же. Дедлок появляется ровно тогда, когда объект будет STA-COM с привязкой к потоку.
Сразу уточню, и вам отдельное спасибо, за то что задали хороший вопрос и подтолкнули в правильном направлении и помогли уточнить итоговое заключение - блокировка должна быть такой, при которой поток не разбирает свою очередь сообщений: Thread.Sleep, lock, синхронный I/O , например. Замените на Thread.Join() или WaitHandle.WaitOne(), и дедлока может не быть, потому что ожидания на STA-потоке обрабатывают входящие RPC-сообщения и могут протолкнуть COM-вызов. Более подробнее об этом в статье Joe Duffy, ссылку давал наверху.

Sign up to leave a comment.

Articles