I am using a Task for a long-running asynchronous processing operation that I want to be able to pause and resume at arbitrary moments. Luckily for me one of the TPL's own authors at Microsoft already came up with a solution to this problem. The only trouble is his solution doesn't work properly.
When you take out the await Task.Delay(100)
in the code below, the code will stop honoring pause requests after the very first one. It appears the SomeMethodAsync
code resumes execution on the same thread as the other task if the value of Thread.CurrentThread.ManagedThreadId
is to be believed. Also the output of SomeMethodAsync
suggests that it is running on several threads.
I have always found the TPL rather confusing and difficult to work with and async/await even more so, so I am having a hard time understanding what's even going on here. I'd be very grateful if anyone could explain.
Minimalist example code:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace PauseTokenTest {
class Program {
static void Main() {
var pts = new PauseTokenSource();
Task.Run(() =>
{
while (true) {
Console.ReadLine();
Console.WriteLine(
$"{Thread.CurrentThread.ManagedThreadId}: Pausing task");
pts.IsPaused = !pts.IsPaused;
}
});
SomeMethodAsync(pts.Token).Wait();
}
public static async Task SomeMethodAsync(PauseToken pause) {
for (int i = 0; ; i++) {
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
// Comment this out and repeatedly pausing and resuming will no longer work.
await Task.Delay(100);
await pause.WaitWhilePausedAsync();
}
}
}
public class PauseTokenSource {
internal static readonly Task s_completedTask =
Task.FromResult(true);
volatile TaskCompletionSource<bool> m_paused;
public bool IsPaused {
get { return m_paused != null; }
set {
if (value) {
Interlocked.CompareExchange(
ref m_paused, new TaskCompletionSource<bool>(), null);
} else {
while (true) {
var tcs = m_paused;
if (tcs == null) return;
if (Interlocked.CompareExchange(ref m_paused, null, tcs) == tcs) {
tcs.SetResult(true);
break;
}
}
}
}
}
public PauseToken Token { get { return new PauseToken(this); } }
internal Task WaitWhilePausedAsync() {
var cur = m_paused;
return cur != null ? cur.Task : s_completedTask;
}
}
public struct PauseToken {
readonly PauseTokenSource m_source;
internal PauseToken(PauseTokenSource source) {
m_source = source;
}
public bool IsPaused {
get { return m_source != null && m_source.IsPaused; }
}
public Task WaitWhilePausedAsync() {
return IsPaused ? m_source.WaitWhilePausedAsync() :
PauseTokenSource.s_completedTask;
}
}
}