Почему InvokeRequired предпочтительнее, чем WindowsFormsSynchronizationContext? - PullRequest
7 голосов
/ 09 марта 2011

Каждый раз, когда новичок спрашивает что-то вроде: Как обновить GUI из другого потока в C #? , ответ довольно прямой:

if (foo.InvokeRequired)
{
    foo.BeginInvoke(...)
} else {
    ...
}

Но действительно ли это хорошо использоватьЭто?Сразу после выполнения потока без графического интерфейса foo.InvokeRequired состояние foo может измениться.Например, если мы закроем форму сразу после foo.InvokeRequired, но до foo.BeginInvoke, вызов foo.BeginInvoke приведет к InvalidOperationException: Невозможно вызвать Invoke или BeginInvoke для элемента управления, пока не будет создан дескриптор окна. Этого не произойдет, если мы закроем форму перед вызовом InvokeRequired, потому что это будет false даже при вызове из потока без GUI.

Другой пример: скажем, foo это TextBox.Если вы закроете форму, и после этого поток без GUI выполнит foo.InvokeRequired (что неверно, потому что форма закрыта) и foo.AppendText, это приведет к ObjectDisposedException.

В отличие от этого,по моему мнению, использование WindowsFormsSynchronizationContext намного проще - отправка обратного вызова с использованием Post будет происходить только в том случае, если поток все еще существует, а синхронные вызовы с использованием Send выдает InvalidAsynchronousStateException, если поток больше не существует.

IsnНе проще использовать WindowsFormsSynchronizationContext?Я что-то пропустил?Почему я должен использовать шаблон InvokeRequired-BeginInvoke, если он не очень безопасен для потоков?Как вы думаете, лучше?

Ответы [ 2 ]

7 голосов
/ 09 марта 2011

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

Итак

if (foo.InvokeRequired)
{
    foo.BeginInvoke(...)
} else {
    ...
}

Можно заменить на более безопасную версию:

context.Post(delegate
{
    if (foo.IsDisposed) return;
    ...
});

Предполагая, что context - это WindowsFormsSynchronizationContext, созданный в том же потоке пользовательского интерфейса, которым был foo.

Эта версия позволяет избежать проблемы, которую вы вызываете:

Сразу после того, как поток не-GUI выполнит foo.InvokeRequired, состояние foo может измениться. Например, если мы закрываем форму сразу после foo.InvokeRequired, но перед foo.BeginInvoke, вызов foo.BeginInvoke приведет к InvalidOperationException: Invoke или BeginInvoke не могут быть вызваны для элемента управления, пока не будет создан дескриптор окна. Этого не произойдет, если мы закроем форму перед вызовом InvokeRequired, потому что она будет ложной даже при вызове из потока, не являющегося GUI.


Остерегайтесь некоторых особых случаев с WindowsFormsSynchronizationContext.Post, если вы играете с несколькими циклами сообщений или несколькими потоками пользовательского интерфейса:

  • WindowsFormsSynchronizationContext.Post выполнит делегат только в том случае, если в потоке, в котором он был создан, все еще есть насос сообщений. Если нет , то ничего не происходит и не возникает исключение .
    Также, если к потоку позже присоединяется еще один насос сообщений (например, посредством второго вызова Application.Run), делегат будет выполнен (Это связано с тем, что система поддерживает очередь сообщений для каждого потока, не зная о том, что кто-то отправляет сообщение из нее или нет)
  • WindowsFormsSynchronizationContext.Send сгенерирует InvalidAsynchronousStateException, если нить, с которой он связан, больше не жива. Но если поток, с которым он связан, является живым и не запускает цикл сообщений , он не будет выполнен немедленно , но все равно будет помещен в очередь сообщений и выполнен, если Application.Run будет выполнен снова.

Ни в одном из этих случаев не должен выполняться код неожиданно, если IsDisposed вызывается для элемента управления, который автоматически удаляется (как и основная форма), поскольку делегат немедленно завершает работу, даже если он выполняется в неожиданное время.

Опасный случай вызывает WindowsFormsSynchronizationContext.Send и полагает, что код будет выполнен: он может и не быть, и теперь есть способ узнать, что он сделал.


Мой вывод: WindowsFormsSynchronizationContext - лучшее решение, если оно правильно используется.

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

0 голосов
/ 16 июля 2016

Кто сказал, что InvokeRequired / Control.BeginInvoke предпочтительнее?Если вы спросите меня, в большинстве случаев это анти паттерн по точным причинам, которые вы упомянули.На вопрос, на который вы ссылаетесь, есть много ответов, и некоторые действительно предлагают использовать контекст синхронизации (включая mine ).

Хотя верно, что любой данный элемент управления может быть удален к тому времени, когда вы 'пытаясь получить к нему доступ из опубликованного делегата, это легко решить, используя Control.IsDisposed (поскольку ваш делегат выполняется в потоке пользовательского интерфейса, поэтому ничто не может располагать элементами управления во время его работы):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        //...
    }

    private MethodOnOtherThread()
    {
         //...
         _context.Post(status => 
          {
             // I think it's enough to check the form's IsDisposed
             // But if you want to be extra paranoid you can test someLabel.IsDisposed
             if (!IsDisposed) {someLabel.Text = newText;}
          },null);
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...