HttpClient.GetAsync 挂起然后互联网在执行期间消失

HttpClient.GetAsync hangs then internet disappears during its execution

我正在一个周期内下载大量小文件(每个文件大小约为 1 MB),我希望在网络连接中断的情况下中断这个周期。但是 httpClient.GetAsync 挂起,如果我在执行时关闭 wi-fi(或拔掉电源线),也不会抛出异常。

如果我使用 CancellationToken 它也不会被取消(如果它在 httpClient.GetAsync 之前被取消它会被取消,但我需要在执行期间取消它)。

我的经验表明,如果在调用 httpClient.GetAsync(url, cancellationToken); 之前互联网消失了,它会抛出异常,但如果互联网消失,那么 httpClient.GetAsync(url, cancellationToken); 已经开始执行,那么它会一直挂起,即使我触发设置 CancelationTokenSource.Cancel() 操作不会取消。

具有HttpClient用法的函数:

private static readonly HttpClient httpClient = new HttpClient();

protected async Task<byte[]> HttpGetData(string url, CancellationToken cancellationToken)
{
    var response = await httpClient.GetAsync(url, cancellationToken);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsByteArrayAsync();
}

正在使用行从循环中调用

byte[] data = await LimitedTimeAwaiter<byte[]>.Execute(
async (c) => { return await HttpGetData(chunkUrl, c); }, cancellationToken, 5);

这里是LimitedTimeAwaiter代码

public class LimitedTimeAwaiter<T>
{
    public static async Task<T> Execute(Func<CancellationToken, Task<T>> function, CancellationToken originalToken, int awaitTime)
    {
        originalToken.ThrowIfCancellationRequested();

        CancellationTokenSource timeout = new CancellationTokenSource(TimeSpan.FromSeconds(awaitTime));

        try
        {
            return await function(timeout.Token);
        }
        catch (OperationCanceledException err)
        {
            throw new Exception("LimitedTimeAwaiter ended function ahead of time", err);
        }
    }
}

虽然我取消了给 httpClient.GetAsync 的令牌,但它没有抛出 OperationCanceledException 并无休止地挂起。如果 return 在有限的时间内没有值,我正在寻找一种方法来中止它。

我不是 100% 确定您要尝试做什么,但您的 LimitedTimeAwaiter 中存在缺陷。您实际上并未将 originalToken 提供给 HttpClient,因此它不会用于取消。要解决这个问题,您应该 link 您的两个令牌:

public class LimitedTimeAwaiter<T>
{
    public static async Task<T> Execute(Func<CancellationToken, Task<T>> function, CancellationToken originalToken, int awaitTime)
    {
        originalToken.ThrowIfCancellationRequested();

        var timeout = CancellationTokenSource.CreateLinkedTokenSource(originalToken);
        timeout.CancelAfter(TimeSpan.FromSeconds(awaitTime));

        try
        {
            return await function(timeout.Token);
        }
        catch (OperationCanceledException err)
        {
            throw new Exception("LimitedTimeAwaiter ended function ahead of time", err);
        }
    }
}

(也不完全确定为什么会捕获异常,但这不是重点)。

现在我假设您的问题确实是 HttpClient 返回的任务没有完成,即使您取消了正确的令牌。首先,您应该报告它,因为那将是 HttpClient 实现中的错误。然后你可以使用这个解决方法:

public class LimitedTimeAwaiter<T>
{
    public static async Task<T> Execute(Func<CancellationToken, Task<T>> function, CancellationToken originalToken, int awaitTime)
    {
        originalToken.ThrowIfCancellationRequested();

        using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(originalToken))
        {
            timeout.CancelAfter(TimeSpan.FromSeconds(awaitTime));

            try
            {
                var httpClientTask = function(timeout.Token);
                var timeoutTask = Task.Delay(Timeout.Infinite, timeout.Token); // This is a trick to link a task to a CancellationToken

                var task = await Task.WhenAny(httpClientTask, timeoutTask);

                // At this point, one of the task completed
                // First, check if we timed out
                timeout.Token.ThrowIfCancellationRequested();

                // If we're still there, it means that the call to HttpClient completed
                return await httpClientTask;
            }
            catch (OperationCanceledException err)
            {
                throw new Exception("LimitedTimeAwaiter ended function ahead of time", err);
            }
        }
    }
}