Продолжение задачи было запланировано для потока, не являющегося пулом потоков. Зачем?
В моем консольном приложении я создаю свой собственный поток для реализации рабочей очереди. Кроме того, я реализовал свой собственный 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
,
Возможны следующие сценарии:
- И исходный поток, и поток завершения имеют одинаковый контекст синхронизации. Продолжение может быть встроенным (выполняется синхронно в потоке завершения).
- И исходный поток, и поток завершения не имеют контекста синхронизации (
SynchronizationContext.Current == null
). Продолжение все еще может быть встроено. - В любой другой комбинации продолжение не должно быть встроено.
Под "может быть встроено" я подразумеваю, что это не обязательно или не гарантировано (это все еще может быть запланировано с использованием TaskScheduler.Current
или же TaskScheduler.FromCurrentSynchronizationContext
для асинхронного выполнения). Тем не менее, в соответствии с текущей реализацией Microsoft TPL, он действительно встроен в условия № 1 и № 2.
Однако, для #3 это не должно быть вписано, это продиктовано здравым смыслом. Так что не стесняйтесь сообщать об ошибке Xamarin. Сначала попробуйте самую последнюю сборку Mono, чтобы увидеть, сохраняется ли проблема.