Продолжение задачи было запланировано для потока, не являющегося пулом потоков. Зачем?

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

Когда я жду Задачу из основного потока, внезапно продолжение (оставшаяся часть моей подпрограммы) намечается в моем рабочем потоке, что не так, потому что я не ожидаю, что мой поток будет использоваться в качестве потока ThreadPool для случайных задач.

Я испытываю это поведение только при запуске кода с Mono.

Вот код, который воспроизводит проблему на моно (протестировано в Mac OS X и Linux):

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main( string[] args )
    {
        Foo();
        Console.ReadLine();
    }


    async static void Foo()
    {
        Console.WriteLine( "{0}: current thread ID={1}; scheduler={2}; context={3};",
            "   Main BEFORE awaiting",
            Thread.CurrentThread.ManagedThreadId, 
            TaskScheduler.Current.Id,
            SynchronizationContext.Current != null );
            // MONO Output: Main BEFORE awaiting: current thread ID=1; scheduler=1; context=False;

        WorkQueue queue = new WorkQueue();

        // !!! 
        // I do expect that current context which is null will be captured for continuation.
        // !!!
        await queue.Enqueue();

        // !!!
        // As we can see our custom context was captured to continue with this part of code.
        // 
        Console.WriteLine( "{0}: current thread ID={1}; scheduler={2}; context={3};",
            "   Main AFTER awaiting",
            Thread.CurrentThread.ManagedThreadId,
            TaskScheduler.Current.Id,
            SynchronizationContext.Current != null );
        // MONO Output: Main AFTER awaiting: current thread ID=4; scheduler=1; context=True;
    }
}

// Custom context which does nothing but enqueues fake tasks to the queue.
//
class WorkQueueSyncContext : SynchronizationContext
{
    readonly WorkQueue queue;

    public WorkQueueSyncContext( WorkQueue queue )
    {
        this.queue = queue;
    }

    public override void Post( SendOrPostCallback d, object state )
    {
    }

    public override void Send( SendOrPostCallback d, object state )
    {
        queue.Enqueue().Wait();
    }
}

// The queue
//
class WorkQueue
{
    readonly Thread thread;

    class WorkQueueItem
    {
        public TaskCompletionSource<object> Completion
        {
            get;
            set;
        }
    }

    BlockingCollection<WorkQueueItem> queue = new BlockingCollection<WorkQueueItem>();


    public WorkQueue()
    {
        thread = new Thread( new ThreadStart( Run ) );
        thread.Start();
    }

    private void Run()
    {
        // Set ower own SynchronizationContext.
        //
        SynchronizationContext.SetSynchronizationContext( new WorkQueueSyncContext( this ) );

        Console.WriteLine( "{0}: current thread ID={1}; scheduler={2}; context={3};",
            "   WorkQueue START",
            Thread.CurrentThread.ManagedThreadId,
            TaskScheduler.Current.Id,
            SynchronizationContext.Current != null );
        // MONO Output: current thread ID=4; scheduler=1; context=True;

        // Working loop.
        //
        while ( true )
        {
            WorkQueueItem item = queue.Take();

            Console.WriteLine( "{0}: current thread ID={1}; scheduler={2}; context={3};",
                "   WorkQueue DOING TASK",
                Thread.CurrentThread.ManagedThreadId,
                TaskScheduler.Current.Id,
                SynchronizationContext.Current != null );
            // MONO Output: current thread ID=4; scheduler=1; context=True;

            // Completed the task :)
            //
            item.Completion.SetResult( true );
        }
    }

    public Task<object> Enqueue()
    {
        TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
        queue.Add( new WorkQueueItem() { Completion = completion } );
        return completion.Task;
    }
}

Итак, вот вывод MONO:

   Main BEFORE awaiting: current thread ID=1; scheduler=1; context=False;
   WorkQueue START: current thread ID=3; scheduler=1; context=True;
   WorkQueue DOING TASK: current thread ID=3; scheduler=1; context=True;
   Main AFTER awaiting: current thread ID=3; scheduler=1; context=True;

И это вывод Windows:

   Main BEFORE awaiting: current thread ID=10; scheduler=1; context=False;
   WorkQueue START: current thread ID=11; scheduler=1; context=True;
   WorkQueue DOING TASK: current thread ID=11; scheduler=1; context=True;
   Main AFTER awaiting: current thread ID=6; scheduler=1; context=False;

Обратите внимание (последняя строка), как отличается захват контекста.

РЕДАКТИРОВАТЬ:

Не воспроизводится с Mono 3.4.0, поэтому кажется, что это ошибка в более старой версии (по крайней мере, 3.2.6);

1 ответ

Решение

Я думаю, что вы нашли ошибку в Mono Runtime. Продолжение после await не должно происходить в потоке с контекстом синхронизации, отличным от TaskAwaiterв точке await,

Возможны следующие сценарии:

  1. И исходный поток, и поток завершения имеют одинаковый контекст синхронизации. Продолжение может быть встроенным (выполняется синхронно в потоке завершения).
  2. И исходный поток, и поток завершения не имеют контекста синхронизации (SynchronizationContext.Current == null). Продолжение все еще может быть встроено.
  3. В любой другой комбинации продолжение не должно быть встроено.

Под "может быть встроено" я подразумеваю, что это не обязательно или не гарантировано (это все еще может быть запланировано с использованием TaskScheduler.Current или же TaskScheduler.FromCurrentSynchronizationContext для асинхронного выполнения). Тем не менее, в соответствии с текущей реализацией Microsoft TPL, он действительно встроен в условия № 1 и № 2.

Однако, для #3 это не должно быть вписано, это продиктовано здравым смыслом. Так что не стесняйтесь сообщать об ошибке Xamarin. Сначала попробуйте самую последнюю сборку Mono, чтобы увидеть, сохраняется ли проблема.

Другие вопросы по тегам