Ответ 1
Что GC.KeepAlive(sync)
- который пустой сам - это просто инструкция для компилятора, чтобы добавить объект sync
на конечный автомат struct
, сгенерированный для Start
. Как указывал @usr, внешняя задача, возвращаемая Start
его вызывающей стороне, не содержит ссылки на этот внутренний конечный автомат.
С другой стороны, задача TaskCompletionSource
tcs.Task
, используемая внутри внутри Start
, содержит такую ссылку (поскольку она содержит ссылку на обратный вызов продолжения await
и, следовательно, весь конечный автомат, обратный вызов регистрируется tcs.Task
await
внутри Start
, создавая круговую ссылку между tcs.Task
и конечным автоматом). Однако ни tcs
, ни tcs.Task
не отображается вне Start
(где он мог бы быть сильным), поэтому граф объекта конечного авто изолирован и получает GC'ed.
Вы могли бы избежать преждевременного GC, создав явную сильную ссылку на tcs
:
public Task SynchronizeAsync()
{
var gch = GCHandle.Alloc(tcs);
return tcs.Task.ContinueWith(
t => { gch.Free(); return t; },
TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
Или более читаемая версия с использованием async
:
public async Task SynchronizeAsync()
{
var gch = GCHandle.Alloc(tcs);
try
{
await tcs.Task;
}
finally
{
gch.Free();
}
}
Чтобы продолжить исследование, рассмотрите следующее небольшое изменение, отметим Task.Delay(Timeout.Infinite)
и тот факт, что я возвращаюсь и использую sync
как Result
для Task<object>
. Это не улучшается:
private static async Task<object> Start()
{
Console.WriteLine("Start");
Synchronizer sync = new Synchronizer();
await Task.Delay(Timeout.Infinite);
// OR: await new Task<object>(() => sync);
// OR: await sync.SynchronizeAsync();
return sync;
}
static void Main(string[] args)
{
var task = Start();
Task.Run(() =>
{
Thread.Sleep(500);
Console.WriteLine("Starting GC");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC Done");
});
Console.WriteLine(task.Result);
Console.Read();
}
IMO, это довольно неожиданно и нежелательно, что объект sync
преждевременно получает GC'ed, прежде чем я могу получить к нему доступ через task.Result
.
Теперь измените Task.Delay(Timeout.Infinite)
на Task.Delay(Int32.MaxValue)
и все будет работать как ожидалось.
Внутренне это сводится к сильной ссылке на объект обратного вызова продолжения await
(сам делегат), который должен удерживаться во время операции, в результате чего обратный вызов все еще ожидает (в полете). Я объяснил это в Async/await, пользовательском awaiter и сборщике мусора.
IMO, тот факт, что эта операция может быть бесконечной (например, Task.Delay(Timeout.Infinite)
или неполной TaskCompletionSource
), не должна влиять на это поведение. Для большинства естественно асинхронных операций такая сильная ссылка действительно поддерживается базовым кодом .NET, который делает вызовы ОС низкого уровня (например, в случае Task.Delay(Int32.MaxValue)
), который передает обратный вызов неуправляемому API-интерфейсу Win32 и удерживает его с GCHandle.Alloc
).
В случае отсутствия ожидающих неуправляемых вызовов на любом уровне (что может быть в случае Task.Delay(Timeout.Infinite)
, TaskCompletionSource
, холодного Task
, пользовательского awaiter), нет явных сильных ссылок на месте, граф объекта конечных машин является чисто управляемым и изолированным, поэтому неожиданный GC происходит.
Я думаю, что это небольшой компромисс между дизайном в инфраструктуре async/await
, чтобы избежать создания стандартных избыточных ссылок внутри ICriticalNotifyCompletion::UnsafeOnCompleted
стандартного TaskAwaiter
.
Во всяком случае, возможно универсальное решение довольно легко реализовать, используя пользовательский awaiter (позвоните ему StrongAwaiter
):
private static async Task<object> Start()
{
Console.WriteLine("Start");
Synchronizer sync = new Synchronizer();
await Task.Delay(Timeout.Infinite).WithStrongAwaiter();
// OR: await sync.SynchronizeAsync().WithStrongAwaiter();
return sync;
}
StrongAwaiter
сам (общий и не общий):
public static class TaskExt
{
// Generic Task<TResult>
public static StrongAwaiter<TResult> WithStrongAwaiter<TResult>(this Task<TResult> @task)
{
return new StrongAwaiter<TResult>(@task);
}
public class StrongAwaiter<TResult> :
System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
Task<TResult> _task;
System.Runtime.CompilerServices.TaskAwaiter<TResult> _awaiter;
System.Runtime.InteropServices.GCHandle _gcHandle;
public StrongAwaiter(Task<TResult> task)
{
_task = task;
_awaiter = _task.GetAwaiter();
}
// custom Awaiter methods
public StrongAwaiter<TResult> GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return _task.IsCompleted; }
}
public TResult GetResult()
{
return _awaiter.GetResult();
}
// INotifyCompletion
public void OnCompleted(Action continuation)
{
_awaiter.OnCompleted(WrapContinuation(continuation));
}
// ICriticalNotifyCompletion
public void UnsafeOnCompleted(Action continuation)
{
_awaiter.UnsafeOnCompleted(WrapContinuation(continuation));
}
Action WrapContinuation(Action continuation)
{
Action wrapper = () =>
{
_gcHandle.Free();
continuation();
};
_gcHandle = System.Runtime.InteropServices.GCHandle.Alloc(wrapper);
return wrapper;
}
}
// Non-generic Task
public static StrongAwaiter WithStrongAwaiter(this Task @task)
{
return new StrongAwaiter(@task);
}
public class StrongAwaiter :
System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
Task _task;
System.Runtime.CompilerServices.TaskAwaiter _awaiter;
System.Runtime.InteropServices.GCHandle _gcHandle;
public StrongAwaiter(Task task)
{
_task = task;
_awaiter = _task.GetAwaiter();
}
// custom Awaiter methods
public StrongAwaiter GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return _task.IsCompleted; }
}
public void GetResult()
{
_awaiter.GetResult();
}
// INotifyCompletion
public void OnCompleted(Action continuation)
{
_awaiter.OnCompleted(WrapContinuation(continuation));
}
// ICriticalNotifyCompletion
public void UnsafeOnCompleted(Action continuation)
{
_awaiter.UnsafeOnCompleted(WrapContinuation(continuation));
}
Action WrapContinuation(Action continuation)
{
Action wrapper = () =>
{
_gcHandle.Free();
continuation();
};
_gcHandle = System.Runtime.InteropServices.GCHandle.Alloc(wrapper);
return wrapper;
}
}
}
Обновлено, вот пример взаимодействия Win32 в режиме реального времени, иллюстрирующий важность сохранения состояния машины
async
. Сбой сборки релиза, если строки GCHandle.Alloc(tcs)
и gch.Free()
закомментированы. Либо callback
, либо tcs
должен быть закреплен для правильной работы. Альтернативно, вместо await tcs.Task.WithStrongAwaiter()
можно использовать StrongAwaiter
.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
public class Program
{
static async Task TestAsync()
{
var tcs = new TaskCompletionSource<bool>();
WaitOrTimerCallbackProc callback = (a, b) =>
tcs.TrySetResult(true);
//var gch = GCHandle.Alloc(tcs);
try
{
IntPtr timerHandle;
if (!CreateTimerQueueTimer(out timerHandle,
IntPtr.Zero,
callback,
IntPtr.Zero, 2000, 0, 0))
throw new System.ComponentModel.Win32Exception(
Marshal.GetLastWin32Error());
await tcs.Task;
}
finally
{
//gch.Free();
GC.KeepAlive(callback);
}
}
public static void Main(string[] args)
{
var task = TestAsync();
Task.Run(() =>
{
Thread.Sleep(500);
Console.WriteLine("Starting GC");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC Done");
});
task.Wait();
Console.WriteLine("completed!");
Console.Read();
}
// p/invoke
delegate void WaitOrTimerCallbackProc(IntPtr lpParameter, bool TimerOrWaitFired);
[DllImport("kernel32.dll")]
static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer,
IntPtr TimerQueue, WaitOrTimerCallbackProc Callback, IntPtr Parameter,
uint DueTime, uint Period, uint Flags);
}
}