SynchronizationContext.Current в асинхронном обратном вызове

Я использую SynchronizationContext как средство синхронизации с потоком графического интерфейса для WinForms и WPF. Недавно я столкнулся с проблемой асинхронных обратных вызовов старого стиля:

   private void Button_Click(object sender, RoutedEventArgs e)
    {
        uiContext = SynchronizationContext.Current;

        var cl = new TcpClient();
        cl.BeginConnect("127.0.0.1", 22222, ConnectedCallback, null);
    }
    public void ConnectedCallback(IAsyncResult result)
    {
        if (SynchronizationContext.Current != uiContext)
            uiContext.Post(x => MyUIOperation(), null);
        else
            MyUIOperation();
    }

    public void MyUIOperation()
    {
        Title = "Connected";
    }

    private SynchronizationContext uiContext;

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

Использование точно такого же кода в WinForms работает так, как я и ожидал.

На данный момент в качестве обходного пути я вместо этого фиксирую текущий ManagedThreadId и сравниваю его в обратном вызове. Каков правильный способ справиться с этим?

Обновлять:

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

if (control.InvokeRequired())
    control.BeginInvoke(SomeFunction);
else
    SomeFunction();

Я пытаюсь удалить зависимость WinForms, не оказывая большого влияния на клиентов этого класса. SomeFunction() вызывает события, поэтому, если я просто вызову uiContext.Send() или uiContext.Post() , порядок выполнения изменится, поскольку Post() всегда ставит вызов в очередь, а Send() всегда блокируется.

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

Это нацелено на .NET 4.0


person Night94    schedule 28.12.2015    source источник
comment
Я бы сказал, что создание службы, которая обеспечивает и предоставляет вам контекст синхронизации пользовательского интерфейса, — это путь. Затем всегда внедряйте этот сервис в любую часть кода, которая вам нужна, и вызывайте Post on Context без каких-либо проверок везде, где вам нужно убедиться, что он выполняется на переднем плане.   -  person VidasV    schedule 28.12.2015
comment
Можете ли вы опубликовать точное сообщение и трассировку стека исключения, которое вы получаете?   -  person Leandro    schedule 28.12.2015
comment
Вызывающий поток не может получить доступ к этому объекту, поскольку им владеет другой поток.   -  person Night94    schedule 28.12.2015
comment
в System.Windows.Threading.Dispatcher.VerifyAccess() в System.Windows.Window.set_Title(строковое значение) в WpfApplication2.MainWindow.MyUIOperation()   -  person Night94    schedule 28.12.2015
comment
Зачем вам нужно проверять if (SynchronizationContext.Current != uiContext)? Почему бы не всегда вызывать Post?   -  person Yacoub Massad    schedule 28.12.2015
comment
Потому что в моем случае вызов функции MyUIOperation() нужно было бы вызывать немедленно, если функция ConnectedCallback вызывается из основного потока. Если я использую Post(), этого не происходит, вызов всегда откладывается. Это просто очень урезанный пример, иллюстрирующий, что в случае ConnectedCallback SynchronizationContext.Current такой же, как и в основном потоке. Таким образом, имитация Control.InvokeRequired не работает в WPF, как я это делаю в коде. Но это происходит в WinForms.   -  person Night94    schedule 28.12.2015
comment
Код, который вы разместили, должен работать.   -  person Ivan Stoev    schedule 28.12.2015
comment
ConnectedCallback никогда не вызывается в потоке пользовательского интерфейса. Я думаю, вы решаете несуществующую проблему. Кроме того, вместо этого вы должны использовать await. Шаблон APM устарел.   -  person usr    schedule 28.12.2015
comment
Извините, я обновил свой пост, чтобы быть более ясным. Моя главная проблема заключается в том, что у меня всегда было впечатление, что когда вы фиксируете SynchronizationContext в основном потоке, он всегда будет отличаться от SynchronizationContext.Current других потоков. Похоже, это не так, поэтому его нельзя использовать для замены InvokeRequired без изменения поведения класса.   -  person Night94    schedule 28.12.2015
comment
@ Night94: В фоновом потоке не должно быть UI SyncCtx, если только этот фоновый поток не делает что-то, чего он не должен делать, например, создает объект пользовательского интерфейса. Можете ли вы опубликовать минимальное воспроизведение проблемы?   -  person Stephen Cleary    schedule 28.12.2015
comment
@StephenCleary Я могу подтвердить, что SynchronizationContext.Current внутри обратного вызова является той же ссылкой, что и uiContext. Пробовал на Win8.1 x64, ориентируясь на .NET 4.0. drive.google.com/file/d/0B91vNcnuPv_GNE10Z3doOEtvSms/   -  person Leandro    schedule 28.12.2015


Ответы (2)


Потому что в моем случае вызов функции MyUIOperation() нужно было бы вызывать немедленно, если функция ConnectedCallback вызывается из основного потока.

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

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

Кроме того, вы можете просто выполнить Dispatcher.Invoke() вместо.

person Leandro    schedule 28.12.2015
comment
Смотрите мое обновление. Я понимаю, что для синхронизации могу использовать либо «Отправить», либо «Отправить», и я согласен с тем, что первоначальный дизайн был плохим с самого начала. Однако, чтобы не вызывать цепную реакцию других плохих вещей, логика должна быть. Если я работаю в потоке пользовательского интерфейса, немедленно вызовите эту функцию, в противном случае поставьте вызов в очередь, и я хотел бы использовать SynchronizationContext и ничего специфичного для пользовательского интерфейса. - person Night94; 28.12.2015
comment
Я понимаю. В этом случае я не вижу никакого хорошего решения, кроме обходного пути, который вы уже используете. Я не уверен, что SynchronizationContext.Current должно быть разным для каждого потока. В документации говорится, что это полезно для распространения контекст синхронизации из одного потока в другой, поэтому я бы сказал, что он разделяется между потоками. Вы могли бы использовать Thread.CurrentThread.IsBackground вместо SynchronizationContext.Current != uiContext, но я бы тоже не стал на это полагаться. - person Leandro; 28.12.2015

Выяснилось, что в .NET 4.5 SynchronizationContext на самом деле отличается в функции обратного вызова, и оператор if будет иметь значение true. Это было преднамеренное изменение, как обсуждалось здесь

   WPF 4.0 had a performance optimization where it would
     frequently reuse the same instance of the
     DispatcherSynchronizationContext when preparing the
     ExecutionContext for invoking a DispatcherOperation.  This
     had observable impacts on behavior.
     1) Some task-parallel implementations check the reference
         equality of the SynchronizationContext to determine if the
         completion can be inlined - a significant performance win.
     2) But, the ExecutionContext would flow the
         SynchronizationContext which could result in the same
         instance of the DispatcherSynchronizationContext being the
         current SynchronizationContext on two different threads.
         The continuations would then be inlined, resulting in code
         running on the wrong thread.
person Night94    schedule 29.12.2015