Почему шаблон обновления пользовательского интерфейса из другого потока не встроен в .NET Framework? - PullRequest
10 голосов
/ 23 октября 2010

Я знаю "почему мой этот фреймворк похож / не похож на xyz?"вопросы немного опасны, но я хочу посмотреть, чего мне не хватает.

В WinForms вы не можете обновить пользовательский интерфейс из другого потока.Большинство людей используют этот шаблон :

private void EventHandler(object sender, DirtyEventArgs e)
{
    if (myControl.InvokeRequired)
        myControl.Invoke(new MethodInvoker(MethodToUpdateUI), e);
    else
        MethodToUpdateUI(e);
}

private void MethodToUpdateUI(object obj) 
{
    // Update UI
}

, и еще более умным остается этот шаблон :

public static TResult SafeInvoke(this T isi, Func call) where T : ISynchronizeInvoke
{
    if (isi.InvokeRequired) { 
        IAsyncResult result = isi.BeginInvoke(call, new object[] { isi }); 
        object endResult = isi.EndInvoke(result); return (TResult)endResult; 
    }
    else
        return call(isi);
}

public static void SafeInvoke(this T isi, Action call) where T : ISynchronizeInvoke
{
    if (isi.InvokeRequired)
        isi.BeginInvoke(call, new object[] { isi });
    else
        call(isi);
}

Независимо от того, который используется, хотяКаждый должен написать шаблонный код для решения этой невероятно распространенной проблемы.Почему тогда .NET Framework не был обновлен, чтобы сделать это для нас?Это то, что эта область кодовой базы заморожена?Это опасение, что это нарушит обратную совместимость?Беспокоит ли это путаница, когда какой-то код работает в версии N по-другому, а в версии N + 1 - иначе?

Ответы [ 5 ]

14 голосов
/ 24 октября 2010

Я подумал, что было бы интересно упомянуть, почему в первую очередь существует поток пользовательского интерфейса. Это должно снизить стоимость производства компонентов пользовательского интерфейса при одновременном повышении их корректности и надежности.

Основная проблема безопасности потока заключается в том, что неатомарное обновление приватного состояния может показаться наполовину законченным в потоке чтения, если поток записи наполовину завершен, когда происходит чтение.

Для обеспечения безопасности потоков вы можете сделать несколько вещей.

1) Явно блокирует все чтения и записи. Плюсы: максимально гибкий; все работает на любой теме. Минусы: максимально болезненные; все должно быть заблокировано все время. Замки могут быть оспорены, что делает их медленными. Это очень легко написать взаимоблокировки. Очень легко написать код, который плохо справляется с повторным входом. И так далее.

2) Разрешить чтение и запись только в том потоке, который создал объект. Вы можете иметь несколько объектов в нескольких потоках, но как только объект используется в потоке, это единственный поток, который может его использовать. Поэтому не будет никаких операций чтения и записи в разные потоки одновременно, поэтому вам не нужно ничего блокировать. Это модель «квартиры», и это модель, на которую рассчитано подавляющее большинство компонентов пользовательского интерфейса. Единственное состояние, которое необходимо заблокировать, - это состояние, совместно используемое несколькими экземплярами в разных потоках, и это довольно легко сделать.

3) Разрешить чтение и запись только в принадлежащем потоке, но разрешить одному потоку явно передавать права владения другому, когда в процессе чтения и записи нет. Это модель «аренды», и это модель, используемая страницами Active Server для утилизации скриптовых движков.

Поскольку подавляющее большинство компонентов пользовательского интерфейса написано для работы в модели квартиры, и сделать все эти компоненты свободными от нитей является болезненным и трудным, вы застряли на необходимости выполнения всей пользовательской работы в потоке пользовательского интерфейса.

4 голосов
/ 23 октября 2010

Это встроено , класс BackgroundWorker реализует его автоматически. Его обработчики событий выполняются в потоке пользовательского интерфейса, если он правильно создан.

Более циничный подход к этому: это анти-паттерн. По крайней мере, в моем коде крайне редко, когда один и тот же метод запускается как в потоке пользовательского интерфейса, так и в каком-то рабочем потоке. Просто не имеет смысла тестировать InvokeRequired, я знаю, что это всегда верно, так как я написал код, который будет называть его намеренно как код, который выполняется в отдельном потоке.

Со всей необходимой сантехникой, необходимой для того, чтобы сделать такой код безопасным и правильно взаимодействовать. Использование оператора блокировки и Manual / AutoResetEvents для передачи сигналов между потоками. И если InvokeRequired будет false , то я знаю, что в этом коде есть ошибка. Потому что вызывать поток пользовательского интерфейса, когда компоненты пользовательского интерфейса еще не созданы или удалены, очень плохо. В лучшем случае он принадлежит вызову Debug.Assert ().

4 голосов
/ 23 октября 2010

Я думаю, что проблема в том, что эту конструкцию, вероятно, нужно будет встроить в каждое свойство всех компонентов пользовательского интерфейса в платформе.Это также должно быть сделано сторонними разработчиками, создающими такие компоненты.

Другой вариант может заключаться в том, что компиляторы добавляют конструкцию вокруг доступа к компонентам пользовательского интерфейса, но вместо этого это усложняет компиляторы.Насколько я понимаю, для возможности проникновения в компилятор она должна

  • играть хорошо, все остальные существующие функции в компиляторе
  • будут иметь стоимость реализации, которая делает ее достойнойв то время как по отношению к проблеме это решает

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

Конечно, все это домыслы, но я могу себе представить, что такого рода рассуждения лежат в основе решения.

2 голосов
/ 23 октября 2010

Более чистый шаблон в .NET 4 использует TPL и продолжения.См. http://blogs.msdn.com/b/csharpfaq/archive/2010/06/18/parallel-programming-task-schedulers-and-synchronization-context.aspx

Использование

var ui = TaskScheduler.FromCurrentSynchronizationContext();

, и теперь вы можете легко запросить выполнение продолжений в потоке пользовательского интерфейса.

0 голосов
/ 24 октября 2010

Если я правильно понимаю ваш вопрос, вы бы хотели, чтобы фреймворк (или компилятор, или какой-то другой элемент технологии) включал Invoke / BeginInvoke / EndInvoke вокруг всех открытых членов объекта UI, чтобы сделать его потокобезопасным. Проблема в том, что это само по себе не сделает ваш поток кода безопасным. Вам все равно придется использовать BeginInvoke и другие механизмы синхронизации очень часто. (См. Эту замечательную статью о безопасности потоков в блоге Эрика Липперта )

Представьте, что вы пишете код как

if (myListBox.SelectedItem != null) 
{
    ...
    myLabel.Text = myListBox.SelectedItem.Text;
    ...
}

Если фреймворк или компилятор обернут каждый доступ к SelectedItem и вызов Delete в вызове BeginInvoke / Invoke, этот не будет безопасным для потоков. Существует потенциальное состояние гонки, если SelectedItem не равно нулю, когда вычисляется условие if, но другой поток устанавливает его в null до завершения блока then. Вероятно, все предложение if-then-else-должно быть заключено в вызов BeginInvoke, но как это должно знать компилятор?

Теперь вы можете сказать «но это верно для всех общих изменяемых объектов, я просто добавлю блокировки». Но это довольно опасно. Представьте, что вы сделали что-то вроде:

// in method A
lock (myListBoxLock)
{
    // do something with myListBox that secretly calls Invoke or EndInvoke
}

// in method B
lock (myListBoxLock)
{
    // do something else with myListBox that secretly calls Invoke or EndInvoke
}

Это приведет к тупику: метод A вызывается в фоновом потоке. Он получает блокировку, вызывает Invoke. Invoke ожидает ответа из очереди сообщений потока пользовательского интерфейса. В то же время метод B выполняется в основном потоке (например, в Button.Click-Handler). Другой поток содержит myListBoxLock, поэтому он не может войти в блокировку - теперь оба потока ждут друг друга, и ни один не может добиться прогресса.

Найти и избежать таких потоковых ошибок достаточно сложно, как есть, но, по крайней мере, теперь вы можете увидеть , что вы вызываете Invoke и что это блокирующий вызов. Если какое-либо свойство может молча блокировать, такие ошибки будут далеко труднее найти.

Мораль: нить сложная. Взаимодействие между потоками и пользовательским интерфейсом еще сложнее, поскольку существует только одна общая очередь сообщений. И, к сожалению, ни наши компиляторы, ни наши фреймворки не настолько умны, чтобы «просто заставить его работать правильно».

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