Почему SynchronizationContext не работает должным образом? - PullRequest
3 голосов
/ 08 октября 2010

У меня следующий код:

[TestMethod]
public void StartWorkInFirstThread()
{
    if (SynchronizationContext.Current == null)
        SynchronizationContext.SetSynchronizationContext(
            new SynchronizationContext());

    var syncContext = SynchronizationContext.Current;

    Console.WriteLine("Start work in the first thread ({0})", 
        Thread.CurrentThread.ManagedThreadId);

    var action = ((Action) DoSomethingInSecondThread);
    action.BeginInvoke(CallbackInSecondThread, syncContext);

    // Continue its own work
}

private static void DoSomethingInSecondThread()
{
    Console.WriteLine("Do something in the second thread ({0})", 
        Thread.CurrentThread.ManagedThreadId);   
}

private void CallbackInSecondThread(IAsyncResult ar)
{
    Console.WriteLine("Callback in the second thread ({0})", 
        Thread.CurrentThread.ManagedThreadId);
    var syncContext = (SynchronizationContext) ar.AsyncState;
    syncContext.Post(CallbackInFirstThread, null);
}

private void CallbackInFirstThread(object obj)
{
    Console.WriteLine("Callback in the first thread ({0})",
        Thread.CurrentThread.ManagedThreadId);
}

Я ожидаю, что последний метод будет выполнен в первом потоке, то есть в начальном потоке, из которого взят SynchronizationContext, потому что я вызываю Post() метод этого контекста.Т.е. что-то вроде этого:

Start work in the first thread (28)
Do something in the second thread (17)
Callback in the second thread (17)
Callback in the first thread (28)

Не в этом ли смысл SynchronizationContext?Но на самом деле у меня есть следующий вывод:

Start work in the first thread (28)
Do something in the second thread (17)
Callback in the second thread (17)
Callback in the first thread (7)

В чем проблема?Что-то пошло не так с SynchronizationContext или у меня возникли некоторые недоразумения?

Обновление: Я называю этот метод модульным тестом с использованием инструмента выполнения Resharper.

Ответы [ 3 ]

8 голосов
/ 08 октября 2010

См. http://www.codeproject.com/KB/threads/SynchronizationContext.aspx

Вам нужен ответ.Вы должны переопределить SynchronizationContext, чтобы он правильно обрабатывал ваши операции.

Чтение, начиная с:

Обратите внимание, что DoWork выполняется в потоке 11, том же потоке, что и Run1.Не большая часть SynchronizationContext в основной поток.Зачем?В чем дело?Ну ... Это та часть, когда ты понимаешь, что в жизни нет ничего бесплатного.Потоки не могут просто переключать контексты между ними, для этого они должны иметь встроенную инфраструктуру.Например, поток пользовательского интерфейса использует насос сообщений, а в своем SynchronizationContext он использует насос сообщений для синхронизации в потоке пользовательского интерфейса.

5 голосов
/ 08 октября 2010

Реализация по умолчанию SynchronizationContext просто выполняет переданный делегат в вызывающем потоке (в потоке, который вызывает метод Send / Post, а не в потоке, который захватывает контекст).Если вам нужно какое-то определенное поведение, например, сходство потоков для некоторых операций, вы должны реализовать это вручную.BCL содержит несколько готовых реализаций для упрощения взаимодействия с пользовательским интерфейсом, например WindowsFormsSynchronizationContext или DispatcherSynchronizationContext .

3 голосов
/ 03 января 2013

Ваше ожидание неверно, потому что нет общего способа «внедрить» делегат в работающий поток. Ваш «первый поток» был запущен в тестовом прогоне, выполнит один или несколько тестов, а затем остановится - нет способа прервать его и сказать ему запустить CallbackInFirstThread. Класс SynchronizationContext запускает делегаты с Post в пуле потоков, потому что это единственная опция, которую он имеет.

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

Если вы хотите проверить, какой SynchronizationContext код, который вы тестируете, вы можете создать свой собственный производный класс, который устанавливает флаг, который вы можете проверить в своем тесте. Вот пример:

public class TestSynchronizationContext : SynchronizationContext
{
    [ThreadStatic]
    private static object _CurrentPostToken;
    /// <summary>
    /// Gets the context's token, if the current thread is executing a delegate that
    /// was posted to this context; otherwise, null.
    /// </summary>
    public static object CurrentPostToken
    {
        get
        {
            return _CurrentPostToken;
        }
    }

    public object Token { get; private set; }

    /// <summary>
    /// Gets a WaitHandle that is set after the context executes a posted delegate.
    /// </summary>
    public AutoResetEvent PostHandle { get; private set; }

    public TestSynchronizationContext(object token)
    {
        Token = token;
        PostHandle = new AutoResetEvent(false);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        try
        {
            _CurrentPostToken = Token;
            // Execute the callback on this thread, so that we can reset the context
            // when it's finished.
            d(state);
        }
        finally
        {
            _CurrentPostToken = null;
        }

        // The test method will wait on this handle so that it doesn't exit before
        // the synchronization context is called.
        PostHandle.Set();
    }
}

В StartWorkInFirstThread установите контекст для экземпляра TestSynchronizationContext:

SynchronizationContext.SetSynchronizationContext(
        new TestSynchronizationContext(new object()));

После того, как вы позвоните BeginInvoke, вам нужно дождаться, пока Post произойдет, прежде чем выйти из теста, поэтому позвоните:

((TestSynchronizationContext)SynchronizationContext.Current).PostHandle.WaitOne(1000);

В CallbackInFirstThread вы можете проверить, какой контекст используется с чем-то вроде:

Assert.IsNotNull(TestSynchronizationContext.CurrentPostToken);

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

...