15 异步编程之终止任务——CancellationToken

840 阅读1分钟

CancellationToken

通常在异步场景下,我们需要提前终止任务。如:请求超时提前终止任务,防止一直占用资源、用户主动取消操作等。这里就可以使用 CancellationToken 参数,用于获得提前终止执行的信号。很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。

转到定义可以看到 CancellationToken 是一个结构体,可用于终止线程的成员有:

None:空

bool IsCancellationRequested: 是否取消

(*)Register(Action callback): 注册取消监听

ThrowIfCancellationRequested(): 如果任务被取消,执行到这句话就抛异常。

下面来对比下各种情况下的不同执行效果:

设计实现“下载一个网址N次”的方法。

分别用GetStringAsync + IsCancellationRequested、 GetStringAsync + ThrowIfCancellationRequested()、带CancellationToken的GetAsync()分别实现。取消分别用超时、用户敲按键(不能await)实现。

一.无CancellationToken

案例:下载对应网站内容100次

使用案例:

#region 不用CancellationToken
await DownloadAsync("http://www.baidu.com", 100);

/// <summary>
/// 下载 n 次指定 url 的内容
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <returns></returns>
static async Task DownloadAsync(string url, int n)
{
    using(var client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync(url);
            Console.WriteLine($"{DateTime.Now}:{html}");
        }
    }
}
#endregion

此案例会一直下载对应网站的内容100次,不会因为长时间下载而停止。

运行结果: image.png

二.IsCancellationRequested

IsCancellationRequested 属性。该属性用来判断 CancellationToken 是否发出取消任务的请求。

1.案例:下载对应网站内容100次,超过5秒提前结束下载

使用案例:

#region 用CancellationToken——IsCancellationRequested
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
CancellationToken cToken = cts.Token;
await DownloadAsyncIsCancellationRequested("http://www.baidu.com", 100, cToken);

/// <summary>
/// 下载 n 次指定 url 的内容
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <param name="cancellationToken">取消操作的通知</param>
/// <returns></returns>
static async Task DownloadAsyncIsCancellationRequested(string url, int n,CancellationToken cancellationToken)
{
    using (var client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync(url);
            Console.WriteLine($"{DateTime.Now}:{html}");
            if (cancellationToken.IsCancellationRequested)
            {
                Console.WriteLine("请求被取消");
                break;
            }
        }
    }
}
#endregion

注意:要通过 CancellationTokenSource 类的实例创建 CancellationToken调用。

CancellationTokenSource类包含方法:

  1. CancelAfter():超时后发出取消信号
  2. Cancel() :发出取消信号
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
CancellationToken cToken = cts.Token;
await DownloadAsyncIsCancellationRequested("http://www.baidu.com", 100, cToken);

此案例下载对应网站的内容超过5秒,就会提前结束下载。

运行结果:

image.png

2.案例:下载对应网站内容100次,按q键提前取消下载。

使用案例:

#region 用CancellationToken——IsCancellationRequested,按q键取消请求。
CancellationTokenSource ctsQ = new CancellationTokenSource();
CancellationToken cTokenQ = ctsQ.Token;
//这里不能用await,否则请求结束前,无法执行下面的代码。
DownloadAsyncIsCancellationRequestedQ("http://www.baidu.com", 100, cTokenQ);
while (Console.ReadLine() != "q")
{
}
ctsQ.Cancel();
Console.ReadLine();
/// <summary>
/// 下载 n 次指定 url 的内容
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <param name="cancellationToken">取消操作的通知</param>
/// <returns></returns>
static async Task DownloadAsyncIsCancellationRequestedQ(string url, int n, CancellationToken cancellationToken)
{
    using (var client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync(url);
            Console.WriteLine($"{DateTime.Now}:{html}");
            if (cancellationToken.IsCancellationRequested)
            {
                Console.WriteLine("请求被取消");
                break;
            }
        }
    }
}
#endregion

此案例会一直下载对应网站的内容100次,如果按下“q”键,则会提前取消下载。

运行结果:

image.png

三.ThrowIfCancellationRequested

ThrowIfCancellationRequested(): 如果任务被取消,执行到这句话就抛异常。

案例:下载对应网站内容100次,超过5秒提前结束下载

使用案例:

#region 用CancellationToken——ThrowIfCancellationRequested
CancellationTokenSource cts1 = new CancellationTokenSource();
cts1.CancelAfter(5000);
CancellationToken cToken1 = cts1.Token;
await DownloadAsyncThrowIfCancellationRequested("http://www.baidu.com", 100, cToken1);

/// <summary>
/// 下载 n 次指定 url 的内容
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <param name="cancellationToken">取消操作的通知</param>
/// <returns></returns>
static async Task DownloadAsyncThrowIfCancellationRequested(string url, int n, CancellationToken cancellationToken)
{
    using (var client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync(url);
            Console.WriteLine($"{DateTime.Now}:{html}");
            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}
#endregion

此案例下载对应网站的内容超过5秒,就会抛出异常:System.OperationCanceledException:“The operation was canceled.”,提前结束下载。

运行结果:

image.png

我们反编译看看 ThrowIfCancellationRequested 内部的原理,发现方法内部同样是判断了IsCancellationRequested 属性。

建议使用 IsCancellationRequested,因为思路更加清晰,并且可以进行其他额外的精确控制。而通过方法抛出异常,需要先 catch 异常在进行处理,没有直接判断属性来的直接高效。

四.带CancellationToken的方法

直接调用带CancellationToken参数的方法。

1.案例:调用带CancellationToken的GetAsync(),下载对应网站内容100次,超过5秒提前结束下载

使用案例:

#region 用CancellationToken——带CancellationToken的GetAsync()
CancellationTokenSource cts2 = new CancellationTokenSource();
cts2.CancelAfter(5000);
CancellationToken cToken2 = cts2.Token;
await DownloadAsyncGetAsync("http://www.baidu.com", 100, cToken2);

/// <summary>
/// 下载 n 次指定 url 的内容
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <param name="cancellationToken">取消操作的通知</param>
/// <returns></returns>
static async Task DownloadAsyncGetAsync(string url, int n, CancellationToken cancellationToken)
{
    using (var client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            var resp = await client.GetAsync(url,cancellationToken);
            string html = await resp.Content.ReadAsStringAsync();
            Console.WriteLine($"{DateTime.Now}:{html}");        
        }
    }
}
#endregion

此案例下载对应网站的内容超过5秒,就会抛出异常,提前结束下载。

运行结果:

image.png

2.案例:调用带CancellationToken的GetStringAsync,下载对应网站内容100次,超过5秒提前结束下载

使用案例:

#region 用CancellationToken——带CancellationToken的GetStringAsync
CancellationTokenSource cts3 = new CancellationTokenSource();
cts3.CancelAfter(5000);
CancellationToken cToken3 = cts3.Token;
await DownloadAsyncGetStringAsync("http://www.baidu.com", 100, cToken3);

/// <summary>
/// 下载 n 次指定 url 的内容
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <param name="cancellationToken">取消操作的通知</param>
/// <returns></returns>
static async Task DownloadAsyncGetStringAsync(string url, int n, CancellationToken cancellationToken)
{
    using (var client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {          
            string html = await client.GetStringAsync(url,cancellationToken);
            Console.WriteLine($"{DateTime.Now}:{html}");
        }
    }
}
#endregion

此案例下载对应网站的内容超过5秒,就会抛出异常,提前结束下载。

运行结果:

image.png

五.ASP.NET Core中CancellationToken使用

ASP.NET Core开发中,一般不需要自己处理CancellationToken、CancellationTokenSource这些,只要做到“能转发CancellationToken就转发”即可。ASP.NET Core会对于用户请求中断进行处理。

演示一下ASP.NET Core中的使用:写一个方法,下载对应网站内容1000次,用Debug.WriteLine()输出,访问中间跳到放到其他网站。

两个案例对比:

1.不传CancellationToken参数

  #region 不传CancellationToken参数
        public async Task<IActionResult> IndexAsync()
        {
            await DownloadAsyncGetStringAsync("http://www.baidu.com", 1000);
            return View();
        }

        static async Task DownloadAsyncGetStringAsync(string url, int n)
        {
            using (var client = new HttpClient())
            {
                for (int i = 0; i < n; i++)
                {
                    string html = await client.GetStringAsync(url);
                    Debug.WriteLine($"{DateTime.Now}:{html}");
                }
            }
        }
  #endregion

访问中间跳到放到其他网站,后台代码依然下载,直到下载1000次,结束运行。

2.传CancellationToken参数

 #region 传CancellationToken参数
        public async Task<IActionResult> IndexAsync(CancellationToken cancellationToken)
        {
            await DownloadAsyncGetStringAsync("http://www.baidu.com", 10000, cancellationToken);
            return View();
        }

        static async Task DownloadAsyncGetStringAsync(string url, int n, CancellationToken cancellationToken)
        {
            using (var client = new HttpClient())
            {
                for (int i = 0; i < n; i++)
                {
                    string html = await client.GetStringAsync(url, cancellationToken);
                    Debug.WriteLine($"{DateTime.Now}:{html}");
                }
            }
        }
  #endregion

访问中间跳到放到其他网站,后台代码,结束运行。不会傻傻地还在一直运行。