Почему код после ожидания выполняется в другом потоке (даже с установленным SynchronizationContext)? - PullRequest
4 голосов
/ 10 июля 2020

Я выполняю следующий фрагмент кода, чтобы проверить, как я могу изменить поток, в котором будет вызываться мой код после await ing. Согласно @ Stephen Cleary в этом ответе , чтобы иметь возможность продолжить выполнение кода asyn c после await ing в том же потоке (контексте), мне нужно установить SynchronizationContext и Я сделал это, однако мой код продолжает работать в другом потоке.

static void Main(string[] args)
{
    var mainSyncContex = new SynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(mainSyncContex);

    Console.WriteLine($"Hello World! ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 1

    try
    {
        Task.Run(async () =>
        {
            SynchronizationContext.SetSynchronizationContext(mainSyncContex);

            Console.WriteLine($"Is there Sync Contex?: {SynchronizationContext.Current != null}");

            Console.WriteLine($"Before delay. ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 3
            await Task.Delay(1000).ConfigureAwait(true);
            Console.WriteLine($"After delay. ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 4
            throw new Exception();
        });
    }
    catch (Exception e)
    {
        Console.WriteLine($"Exception: {e.Message} Catch. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
    }

    Console.WriteLine($"Ending ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 1
    Console.ReadKey();
}

Вывод:

Hello World! ThreadId: 1 Конечный ThreadId: 1 Есть ли Syn c Contex ?: True До задержки. ThreadId: 3 После задержки. ThreadId: 4

Почему это происходит?

Ответы [ 2 ]

4 голосов
/ 12 июля 2020
• 1000 более Post в остальной части кода после await перехода к SynchronizationContext.

Я создал очень очень базовый c и НЕ оптимальный SynchronizationContext, чтобы продемонстрировать, как базовый c реализация должна выглядеть так. Моя реализация создаст новый Thread и запустит некоторые Task s в определенном c контексте внутри того же вновь созданного Thread.

Можно найти лучшую реализацию (но гораздо сложнее) здесь в репозитории Стивена Клири на GitHub.

Моя реализация в основном выглядит следующим образом (из моего репозитория GitHub , код в репозитории может выглядеть иначе в будущем):

/// <summary>
/// This <see cref="SynchronizationContext"/> will call all posted callbacks in a single new thread.
/// </summary>
public class SingleNewThreadSynchronizationContext : SynchronizationContext
{
    readonly Thread _workerThread;
    readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _actionStatePairs = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

    /// <summary>
    /// Returns the Id of the worker <see cref="Thread"/> created by this <see cref="SynchronizationContext"/>.
    /// </summary>
    public int ManagedThreadId => _workerThread.ManagedThreadId;

    public SingleNewThreadSynchronizationContext()
    {
        // Creates a new thread to run the posted calls.
        _workerThread = new Thread(() =>
        {
            try
            {
                while (true)
                {
                    var actionStatePair = _actionStatePairs.Take();
                    SetSynchronizationContext(this);
                    actionStatePair.Key?.Invoke(actionStatePair.Value);
                }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine($"The thread {_workerThread.ManagedThreadId} of {nameof(SingleNewThreadSynchronizationContext)} was aborted.");
            }
        });

        _workerThread.IsBackground = true;
        _workerThread.Start();
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        // Queues the posted callbacks to be called in this SynchronizationContext.
        _actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotSupportedException();
    }

    public override void OperationCompleted()
    {
        _actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(new SendOrPostCallback(_ => _workerThread.Abort()), null));
        _actionStatePairs.CompleteAdding();
    }
}

и вот демонстрация для его использования:

static void SingleNewThreadSynchronizationContextDemo()
{
    var synchronizationContext = new SingleNewThreadSynchronizationContext();

    // Creates some tasks to test that the whole calls in the tasks (before and after awaiting) will be called in the same thread.
    for (int i = 0; i < 20; i++)
        Task.Run(async () =>
        {
            SynchronizationContext.SetSynchronizationContext(synchronizationContext);
            // Before yielding, the task will be started in some thread-pool thread.
            var threadIdBeforeYield = Thread.CurrentThread.ManagedThreadId;
            // We yield to post the rest of the task after await to the SynchronizationContext.
            // Other possiblity here is maybe to start the whole Task using a different TaskScheduler.
            await Task.Yield();

            var threadIdBeforeAwait1 = Thread.CurrentThread.ManagedThreadId;
            await Task.Delay(100);
            var threadIdBeforeAwait2 = Thread.CurrentThread.ManagedThreadId;
            await Task.Delay(100);

            Console.WriteLine($"SynchronizationContext: thread Id '{synchronizationContext.ManagedThreadId}' | type '{SynchronizationContext.Current?.GetType()}.'");
            Console.WriteLine($"Thread Ids: Before yield '{threadIdBeforeYield}' | Before await1 '{threadIdBeforeAwait1}' | Before await2 '{threadIdBeforeAwait2}' | After last await '{Thread.CurrentThread.ManagedThreadId}'.{Environment.NewLine}");
        });
}

static void Main(string[] args)
{
    Console.WriteLine($"Entry thread {Thread.CurrentThread.ManagedThreadId}");
    SingleNewThreadSynchronizationContextDemo();
    Console.WriteLine($"Exit thread {Thread.CurrentThread.ManagedThreadId}");

    Console.ReadLine();
}

Вывод:

Entry thread 1   
Exit thread 1  
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'  
Thread Ids: Before yield '11' | Before await1 '5' | Before await2 '5' | After last await '5'.

SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'  
Thread Ids: Before yield '4' | Before await1 '5' | Before await2 '5' | After last await '5'.

SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'  
Thread Ids: Before yield '12' | Before await1 '5' | Before await2 '5' | After last await '5'.

SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'  
Thread Ids: Before yield '6' | Before await1 '5' | Before await2 '5' | After last await '5'.

SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'  
Thread Ids: Before yield '10' | Before await1 '5' | Before await2 '5' | After last await '5'.

SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'  
Thread Ids: Before yield '7' | Before await1 '5' | Before await2 '5' | After last await '5'.  
2 голосов
/ 10 июля 2020

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

(см. Справочные источники )

public virtual void Send(SendOrPostCallback d, Object state)
{
    d(state);
}

public virtual void Post(SendOrPostCallback d, Object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

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

Для консольного приложения рассмотрите возможность использования Stephen Cleary's AsyncContext .

...