【问题标题】:Async/await with CancellationToken doesn't cancel the operation带有 CancellationToken 的异步/等待不会取消操作
【发布时间】:2015-05-05 13:02:35
【问题描述】:

我想使用CancellationToken 中止文件下载。这是我尝试过的:

public async Task retrieveDocument(Document document)
{
    // do some preparation work first before retrieving the document (not shown here)
    if (cancelToken == null)
    {
        cancelToken = new CancellationTokenSource();
        try
        {
            Document documentResult = await webservice.GetDocumentAsync(document.Id, cancelToken.Token);
            // do some other stuff (checks ...)
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("abort download");
        }
        finally
        {
            cancelToken = null;
        }
    }
    else
    {
        cancelToken.Cancel();
        cancelToken = null;
    }
}

public async Task<Document> GetDocumentAsync(string documentId, CancellationToken cancelToken)
{
    Document documentResult = new Document();

    try
    {

        cancelToken.ThrowIfCancellationRequested();

        documentResult = await Task.Run(() => manager.GetDocumentById(documentId));
    }

    return documentResult;
}

然后应该使用 cancelToken 来取消操作:

public override void DidReceiveMemoryWarning ()
{
    // Releases the view if it doesn't have a superview.
    base.DidReceiveMemoryWarning ();

    if (cancelToken != null) {
        Console.WriteLine ("Token cancelled");
        cancelToken.Cancel ();
    }
}

IsCancellationRequested 好像没有更新。所以操作不会被取消。我也试过用这个

cancelToken.ThrowIfCancellationRequested();
try{
    documentResult = await Task.Run(() => manager.GetDocumentById (documentId), cancelToken);
} catch(TaskCanceledException){
    Console.WriteLine("task canceled here");
}

但没有任何改变。

我做错了什么?

编辑:

这里是缺少的部分,例如GetDocumentById:

public Document GetDocumentById(string docid)
{
    GetDocumentByIdResult res;
    try
    {
        res = ws.CallGetDocumentById(session, docid);
    }
    catch (WebException e)
    {
        throw new NoResponseFromServerException(e.Message);
    }

    return res;
}

public Document CallGetDocumentById(Session session, string parmsstring)
{
    XmlDocument soapEnvelope = Factory.GetGetDocumentById(parmsstring);
    HttpWebRequest webRequest = CreateWebRequest(session);
    webRequest = InsertEnvelope(soapEnvelope, webRequest);
    string result = WsGetResponseString(webRequest);
    return ParseDocument(result);
}

static string WsGetResponseString(WebRequest webreq)
{
    string soapResult = "";
    IAsyncResult asyncResult = webreq.BeginGetResponse(null, null);
    if (asyncResult.AsyncWaitHandle.WaitOne(50000))
    {
        using (WebResponse webResponse = webreq.EndGetResponse(asyncResult))
        {
            if (webResponse != null)
            {
                using (var rd = new StreamReader(webResponse.GetResponseStream()))
                {
                    soapResult = rd.ReadToEnd();
                }
            }
        }
    }
    else
    {
        webreq.Abort();
        throw new NoResponseFromServerException();
    }

    return soapResult;
}

【问题讨论】:

  • 我不太明白你想做什么。 开始之前调用 ThrowIfCancellationRequested,这不太可能做任何事情。您需要在为其工作的代码中定期调用它才能运行。例如,在循环内。
  • 当我使用Task.Delay 而不是webservice.GetDocumentAsync 时无法重现该问题。因此,可能该服务调用或后面的代码 (do some other stuff) 不完全支持协作任务取消。
  • GetDocumentById 是做什么的?如果您在GetDocumentById 开始执行后取消令牌,则不会有人注意到它,因为它没有被监控。
  • @svinja 说的是对的,这是一个很难掌握的概念。 ThrowIfCancellationRequested 没有设置选项,它检查令牌,如果设置,它会抛出错误。您需要定期调用它以引发异常,因为该调用实际上是引发它的原因。
  • @svinja:对于我的示例,这样的循环会是什么样子?我看到其他人使用while(true),但我不知道使用它是否是个好主意。

标签: c# async-await cancellationtokensource


【解决方案1】:

我想使用 CancellationToken 中止文件下载

下载文件是一种 I/O 操作,在 .NET 平台上可以使用异步可取消(基于 I/O 完成端口)功能。然而你似乎没有使用它们。

相反,您似乎正在使用执行阻塞 I/O 的 Task.Run 创建(一系列)任务,其中取消令牌不会传递给您的 Task.Run 链中的每个任务。

有关执行异步、可等待和可取消文件下载的示例,请参阅:

您可以执行以下操作:

static async Task<string> WsGetResponseString(WebRequest webreq, CancellationToken cancelToken)`
{
    cancelToken.Register(webreq.Abort);
    using (var response = await webReq.GetResponseAsync())
    using (var stream = response.GetResponseStream())
    using (var destStream = new MemoryStream())
    {
        await stream.CopyToAsync(destStream, 4096, cancelToken);
        return Encoding.UTF8.GetString(destStream.ToArray());
    }
}

【讨论】:

  • 我已经考虑过使用WebClient,但是我看到的实现都使用了一个URI。就我而言,我正在构建一个单独的 Web 请求(请参阅已编辑的问题)。那么这里怎么做呢?
  • @testing 我更新了答案以包含WebRequest 变体
  • 非常感谢。我会试试的。如果我想读取字符串并将其保存为文件而不将其缓冲到内存中,我可以像链接示例的one 那样简单地使用File.Write 而不是MemoryStream 吗?我认为这应该可行,对于这个 CopyToAsync 和 UTF-8 转换将不再需要。
  • @testing,是的,您将打开文件流:using (var destSteam = File.OpenWrite(path)) 并删除 return Encoding ....
  • 是的,当然。我在您的方法上建立了一些东西,因此对其进行了更多测试:-)
【解决方案2】:

您的代码在启动 GetDocumentAsync 方法后只调用一次ThrowIfCancellationRequested(),使得捕获取消的窗口非常小。

您需要将CancellationToken 传递给GetDocumentById,并让它在操作之间调用ThrowIfCancellationRequested,或者将令牌直接传递给较低级别​​的某些调用。

作为对调用方法和CancellationToken 之间关系的快速测试,您可以将GetDocumentAsync 更改为:

cancelToken.ThrowIfCancellationRequested();
documentResult = await Task.Run(() => manager.GetDocumentById(documentId));
cancelToken.ThrowIfCancellationRequested();

并在创建CancellationTokenSource 后立即调用CancelToken.CancelAfter(50) 或类似名称...您可能需要根据GetDocumentById 运行的时间来调整50 的值。

[编辑]

鉴于您对问题的编辑,最快的解决方法是将CancelToken 传递给WsGetResponseString 并使用CancelToken.Register() 调用WebRequest.Abort()

您也可以使用CancelAfter() 来实现您的50 秒超时,从BeginGetResponse..EndGetResponse 切换到GetResponseAsync 等。

【讨论】:

  • 我得到 操作被取消。 在控制台输出中,但似乎在 await 完成后调用它。 GetDocumentAsync 中抛出异常。
  • 是的,它会在上面的第三行抛出。你可能不想在那个方法中捕获OperationCancelledException,让它冒泡到retrieveDocument,它应该处理它并打印到控制台。
猜你喜欢
  • 2019-07-21
  • 1970-01-01
  • 1970-01-01
  • 2020-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-22
  • 1970-01-01
相关资源
最近更新 更多