4
\$\begingroup\$

I added this implementation of PauseToken and PauseTokenSource to my project from this MSDN blog post: Cooperatively pausing async methods

Since I will usually be passing in both a PauseToken and a CancellationToken into async methods, I am trying to encapsulate both of them into a single class, PauseOrCancelToken, created by PauseOrCancelTokenSource. I'm not entirely sure if I have implemented it correctly, although it does work as intended. Just wondering if I overlooked anything important that someone else might be able to spot.

public partial class PauseCancelForm : Form
{

    private PauseOrCancelTokenSource pcts = new PauseOrCancelTokenSource();

    public PauseCancelForm()
    {
        InitializeComponent();
    }

    private void StartButton_Click(object sender, EventArgs e)
    {
        Task.Run(() =>
        {
            DoSomeAsyncStuff(pcts.Token).Wait();
        });
    }

    private async Task DoSomeAsyncStuff(PauseOrCancelToken pct)
    {
        var proxies = new List<Proxy>();
        var judges = new List<ProxyJudge>();

        for (int i = 0; i < 100; i++)
        {
            proxies.Add(new Proxy("127.0.0." + RandomUtility.GetRandomInt(1, 5), 8888));
        }

        judges.Add(new ProxyJudge("http://azenv.net"));

        await ProxyTester.Start(proxies, judges, pct);
    }

    private void PauseButton_Click(object sender, EventArgs e)
    {
        pcts.Pause();
    }

    private void StopButton_Click(object sender, EventArgs e)
    {
        pcts.Cancel();
    }
}

public class PauseOrCancelTokenSource
{

    private PauseTokenSource pts = new PauseTokenSource();
    private CancellationTokenSource cts = new CancellationTokenSource();
    public PauseOrCancelToken Token { get { return new PauseOrCancelToken(pts, cts); } }

    public void Pause()
    {
        pts.IsPaused = true;
    }

    public void Cancel()
    {
        cts.Cancel();
    }
}

public class PauseOrCancelToken
{
    private PauseToken pt;
    private CancellationToken ct;

    public PauseOrCancelToken(PauseTokenSource pts, CancellationTokenSource cts)
    {
        this.pt = pts.Token;
        this.ct = cts.Token;
    }

    public async Task PauseIfRequested()
    {
        await pt.WaitWhilePausedAsync();
    }

    public void CancelIfRequested()
    {
        ct.ThrowIfCancellationRequested();
    }
}

Example

public class ProxyTester
{

    public async static Task Start(List<Proxy> proxies, List<ProxyJudge> judges, PauseOrCancelToken pct, List<ProxyTest> tests = null)
    {
        if (tests == null)
        {
            tests = new List<ProxyTest>();
        }

        //Get external IP to check if proxy is anonymous.
        var publicIp = await WebUtility.GetPublicIP();

        //Validate proxy judges.
        var tasks = new List<Task>();
        foreach (var judge in judges)
        {
            tasks.Add(Task.Run(async () => {
                judge.IsValid = await judge.TestValidityAsync();
                await pct.PauseIfRequested();
                pct.CancelIfRequested();
            }));
        }

        await Task.WhenAll(tasks);

        var validJudges = from judge in judges
                            where judge.IsValid
                            select judge;

        if (validJudges.Count() == 0)
        {
            throw new Exception("No valid judges loaded.");
        }

        //Validate proxy tests.
        tasks.Clear();
        foreach (var test in tests)
        {
            tasks.Add(Task.Run(async () => {
                test.IsValid = await test.TestValidityAsync();
                await pct.PauseIfRequested();
                pct.CancelIfRequested();
            }));
        }

        await Task.WhenAll(tasks);

        var validTests = from test in tests
                            where test.IsValid
                            select test;

        //Test proxies with a random, valid proxy judge.  If valid, test with all valid proxy tests.
        tasks.Clear();
        foreach (var proxy in proxies)
        {
            tasks.Add(Task.Run(async () =>
            {
                proxy.IsValid = await proxy.TestValidityAsync(validJudges.ElementAt(RandomUtility.GetRandomInt(0, validJudges.Count())));
                await pct.PauseIfRequested();
                pct.CancelIfRequested();

                if (proxy.IsValid)
                {
                    proxy.TestedSites.AddRange(validTests);
                    var childTasks = new List<Task>();
                    foreach (var test in validTests)
                    {
                        childTasks.Add(Task.Run(async () =>
                        {
                            proxy.TestedSites.ElementAt(proxy.TestedSites.IndexOf(test)).IsValid = await proxy.TestValidityAsync(test);
                            await pct.PauseIfRequested();
                            pct.CancelIfRequested();
                        }));
                    }

                    await Task.WhenAll(childTasks);
                }
            }));
        }


        await Task.WhenAll(tasks);

    }
}
\$\endgroup\$
  • \$\begingroup\$ Perhaps it would be more beneficial for you, if you include a real situation where this is used. There might a better approach, maybe using ManualResetEvent. There are only minors stuff that I can spot in your current solution that are potential improvements. \$\endgroup\$ – Denis Apr 6 '18 at 2:33
  • 1
    \$\begingroup\$ @Denis Hi, thanks for the response. I posted a simplified use case simply because it would add about 150 more LOC to this example, most of it unrelated to the implementations of PauseOrCancelToken and PauseOrCancelTokenSource. But if you really think it would help, I will edit my post in a bit. \$\endgroup\$ – David Stampher Apr 6 '18 at 2:39
  • 1
    \$\begingroup\$ @Denis Ok, I edited my post and added what I intend to use this for in production. I left some unrelated classes out like Proxy and ProxyJudge, but if you need to see those too, let me know. I will probably end up creating a new method, PauseOrCancelIfRequested() to turn those 2 LOC into 1. \$\endgroup\$ – David Stampher Apr 6 '18 at 3:27
4
\$\begingroup\$

The only important thing, I can see is missing, is to let PauseOrCancelTokenSource implement IDisposable to make it possible to dispose the CancellationTokenSource field properly. There are many opinions about the necessity to dispose CancellationTokens, but you should at least give the client the opportunity to do so. See this.


PauseOrCancelToken should have a public CancellationToken CancellationToken => ct; to make it possible to pass the cancellation token to async calls to external apis like await Task.Delay(1000, pct.CancellationToken); so they can respond to a cancel request.


You could maybe combine PauseIfRequested() and CancelIfRequested() into one method, because you always call them both at the same time anyway.


My modification to your classes would be something like:

  public class PauseOrCancelTokenSource : IDisposable
  {

    private PauseTokenSource pts = new PauseTokenSource();
    private CancellationTokenSource cts = new CancellationTokenSource();
    public PauseOrCancelToken Token { get { return new PauseOrCancelToken(pts, cts); } }

    public void Pause()
    {
      pts.IsPaused = true;
    }

    public void Cancel()
    {
      cts.Cancel();
    }

    public bool IsDisposed => cts == null;

    public void Dispose()
    {
      if (cts != null)
      {
        cts.Dispose();
        cts = null;
      }
    }
  }

  public class PauseOrCancelToken
  {
    private PauseToken pt;
    private CancellationToken ct;

    public PauseOrCancelToken(PauseTokenSource pts, CancellationTokenSource cts)
    {
      this.pt = pts.Token;
      this.ct = cts.Token;
    }

    public CancellationToken CancellationToken => ct;

    public async Task PauseIfRequested()
    {
      await pt.WaitWhilePausedAsync();
    }

    public void CancelIfRequested()
    {
      ct.ThrowIfCancellationRequested();
    }

    public async Task PauseOrCancel()
    {
      ct.ThrowIfCancellationRequested();
      await pt.WaitWhilePausedAsync();
    }

  }

Disclaimer: I haven't tested properly, so you should read it as only suggestions.


As @Denis mentioned, the pause part could be done really simple and easy with AsyncManualResetEvent but the PauseToken/-Source seems to work just fine.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.