2014-10-07 4 views
4

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

Большая часть кода моей программы использует настраиваемый SynchronizationContext, который просто ставит очередь SendOrPostCallbacks и вызывает их в определенной известной точке моего основного потока. Я устанавливаю этот настраиваемый SynchronizationContext в начале времени, и все работает нормально, когда я использую только этот.

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

void BeginningOfTime() { 
    // MyCustomContext queues each endOrPostCallback and runs them all at a known point in the main thread. 
    SynchronizationContext.SetSynchronizationContext(new MyCustomContext()); 


    // ... later on in the code, wait on something, and it should continue inside 
    // the main thread where MyCustomContext runs everything that it has queued 
    int x = await SomeOtherFunction(); 
    WeShouldBeInTheMainThreadNow(); // ********* this should run in the main thread 
} 

async int SomeOtherFunction() { 
    // Set a null SynchronizationContext because this function wants its continuations 
    // to run in the thread pool. 
    SynchronizationContext prevContext = SynchronizationContext.Current; 
    SynchronizationContext.SetSynchronizationContext(null); 

    try { 

     // I want the continuation for this to be posted to a thread pool 
     // thread, not MyCustomContext. 
     await Blah(); 

     WeShouldBeInAThreadPoolThread(); // ********* this should run in a thread pool thread 

    } finally { 
     // Restore the previous SetSynchronizationContext. 
     SynchronizationContext.SetSynchronizationContext(prevContext); 
    } 
} 

Поведение, которое я получаю, состоит в том, что код сразу после каждого ожидания выполняется в кажущейся случайной цепочке. Иногда WeShouldBeInTheMainThreadNow() работает в потоке пула потоков, а иногда и в основном потоке. Иногда WeShouldBeInAThreadPoolThread() работает

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

+0

Там нет никакой гарантии, что все резьбовые выключатели будет происходить, если задача (или другой awaitable) фактически завершена к тому времени, вы выполнили 'await' - всегда помните, что код может просто продолжать пробегая мимо этой точки. –

ответ

1

Существует распространенное заблуждение о await, что как-то вызове async -implemented функции обрабатывают специальным образом.

Однако ключевое слово await работает на объекте, на него все равно, откуда приходит ожидаемый объект.

То есть, вы всегда можете переписать await Blah(); с var blahTask = Blah(); await blahTask;

Так что же происходит, когда вы перезаписать внешний await вызов таким образом?

// Synchronization Context leads to main thread; 
Task<int> xTask = SomeOtherFunction(); 
// Synchronization Context has already been set 
// to null by SomeOtherFunction! 
int x = await xTask; 

И потом, есть другая проблема: finally от внутреннего метода выполняется в продолжении, а это означает, что она выполняется на пуле - это не только у вас есть снята с охраны вашего SynchronizationContext, но ваш SynchronizationContext (возможно) будет восстановлено в будущем, в другом потоке. Однако, поскольку я действительно не понимаю, как течет поток SynchronizationContext, вполне возможно, что SynchronizationContext не восстановлен вообще, что он просто установлен на другой поток (помните, что SynchronizationContext.Current является нито-локальным ...)

Эти две проблемы, объединенные, легко объяснят случайность, которую вы наблюдаете. (То есть вы манипулируете квази-глобальным состоянием из нескольких потоков ...)

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

В общем, вы просто хотите указать «Это не имеет значения для кода после await быть на том же контексте, что и код перед await», и в этом случае, используя ConfigureAwait(false) бы целесообразно;

async Task SomeOtherFunction() { 
    await Blah().ConfigureAwait(false); 
} 

Однако, если вы абсолютно хотите, чтобы указать «Я хочу, чтобы код после await для запуска на пул потоков» - что-то , что должно быть редким, то вы не можете сделать это с await, но вы можете это сделать, например с ContinueWith - однако вы собираетесь смешивать несколько способов использования объектов Task, и это может привести к довольно запутанному коду.

Task SomeOtherFunction() { 
    return Blah() 
     .ContinueWith(blahTask => WeShouldBeInAThreadPoolThread(), 
         TaskScheduler.Default); 
} 
+0

Хороший вопрос о 'finally', выполняющемся в контексте пула потоков, но я должен категорически не соглашаться с рекомендацией' ContinueWith'. 'ConfigureAwait (false)' будет достаточно. –

+0

@ StephenCleary: Я не знаю, где выполняется 'Blah()'. Если 'Blah()' выполняется в пуле потоков, тогда вы правы. Но если это не так (например, если это задача ввода-вывода, то я думаю, что задача будет завершена на порте завершения ввода-вывода - но нет никакой гарантии в любом случае), то продолжение не будет запланировано на пул потоков. Тем не менее, реальная загадка для меня - это то, почему необходимо контролировать, где выполняется * задача продолжения *. –

+2

Общая концептуальная модель заключается в том, что код либо выполняется в определенном контексте (в данном случае пользовательском 'SynchronizationContext') или« где-то еще ». Является ли это «где-то еще» регулярным потоком пула потоков или потоком пула потоков IOCP почти никогда не имеет значения. –

2

Я бы ожидать, что ваш код работает, но есть несколько возможных причин, почему это не так:

  1. Убедитесь, что ваш SynchronizationContext является текущим, когда он выполняет свои продолжения.
  2. Не определено SynchronizationContext.
  3. Обычный способ запуска кода в SynchronizationContext состоит в том, чтобы установить текущий в одном методе, а затем запустить другой (возможно, асинхронный) метод, который зависит от него.
  4. Обычный способ избежать текущего SynchronizationContext - добавить ConfigureAwait(false) ко всем ожидаемым задачам.