HttpClient 的奇怪异步问题

Odd Async Issues with HttpClient

正在使用 HttpClient 将 WebClient 代码从 .Net Framework 4.6.1 转换为 NetStandard 1.6,遇到一个奇怪的问题。这是我有问题的代码块:

public double TestDownloadSpeed(Server server, int simultaniousDownloads = 2, int retryCount = 2)
    {
        var testData = GenerateDownloadUrls(server, retryCount);

        return TestSpeed(testData, async (client, url) =>
        {
            var data = await client.GetByteArrayAsync(url);

            return data.Length;
        }, simultaniousDownloads);
    }

 public double TestUploadSpeed(Server server, int simultaniousUploads = 2, int retryCount = 2)
    {
        var testData = GenerateUploadData(retryCount);

        return TestSpeed(testData, async (client, uploadData) =>
        {
            client.PostAsync(server.Url, new StringContent(uploadData.ToString())).RunSynchronously();

            return uploadData[0].Length;
        }, simultaniousUploads);
    }

private static double TestSpeed<T>(IEnumerable<T> testData, Func<HttpClient, T, Task<int>> doWork, int concurencyCount = 2)
    {
        var timer = new Stopwatch();
        var throttler = new SemaphoreSlim(concurencyCount);

        timer.Start();

        var downloadTasks = testData.Select(async data =>
        {
            await throttler.WaitAsync().ConfigureAwait(true);
            var client = new CoreSpeedWebClient();
            try
            {
                var size = await doWork(client, data).ConfigureAwait(true);
                return size;
            }
            finally
            {
                client.Dispose();
                throttler.Release();
            }
        }).ToArray();

        Task.Run(() => downloadTasks);

        timer.Stop();

        double totalSize = downloadTasks.Sum(task => task.Result);

        return (totalSize * 8 / 1024) / ((double)timer.ElapsedMilliseconds / 1000);
    }

因此,当调用 TestDownloadSpeed 函数时,一切都按预期工作,但是当我调用 TestUploadSpeed 方法时,我收到错误 InvalidOperationException: RunSynchronously 可能不会在未绑定到委托的任务上调用,例如从异步方法返回的任务。* 在 TestSpeed 的这一部分。

double totalSize = downloadTasks.Sum(task => task.Result);

我真的绞尽脑汁想弄清楚 TestUploadSpeed 中的什么东西导致了问题。有人有任何提示可以指出正确的方向吗?

如果有帮助,这里是 .Net 4.6.1 代码,可以正常工作,所以我的翻译中可能有什么地方不对?另外,原始代码的运行速度大约快 5 倍,所以不确定那是什么...

    public double TestDownloadSpeed(Server server, int simultaniousDownloads = 2, int retryCount = 2)
    {
        var testData = GenerateDownloadUrls(server, retryCount);

        return TestSpeed(testData, async (client, url) =>
        {
            var data = await client.DownloadDataTaskAsync(url).ConfigureAwait(false);
            return data.Length;
        }, simultaniousDownloads);
    }

    public double TestUploadSpeed(Server server, int simultaniousUploads = 2, int retryCount = 2)
    {
        var testData = GenerateUploadData(retryCount);
        return TestSpeed(testData, async (client, uploadData) =>
        {
            await client.UploadValuesTaskAsync(server.Url, uploadData).ConfigureAwait(false);
            return uploadData[0].Length;
        }, simultaniousUploads);
    }

    private static double TestSpeed<T>(IEnumerable<T> testData, Func<WebClient, T, Task<int>> doWork, int concurencyCount = 2)
    {
        var timer = new Stopwatch();
        var throttler = new SemaphoreSlim(concurencyCount);

        timer.Start();
        var downloadTasks = testData.Select(async data =>
        {
            await throttler.WaitAsync().ConfigureAwait(false);
            var client = new SpeedTestWebClient();
            try
            {
                var size = await doWork(client, data).ConfigureAwait(false);
                return size;
            }
            finally
            {
                client.Dispose();
                throttler.Release();
            }
        }).ToArray();

        Task.WaitAll(downloadTasks);
        timer.Stop();

        double totalSize = downloadTasks.Sum(task => task.Result);
        return (totalSize * 8 / 1024) / ((double)timer.ElapsedMilliseconds / 1000);
    }

tl;dr

要修复您的具体示例,您需要调用 Wait() 以同步等待 Task 完成。 不是 RunSynchronously().

但您可能实际上想要 await Task 以允许异步完成。 Wait() 在大多数情况下对性能来说不是很好,并且有一些特征如果使用不当可能会导致死锁。

详细解释

RunSynchronously()Wait() 的用法略有不同。它 运行s Task 同步,而 Waitwait 同步但不指示任何内容关于它应该如何 运行.

RunSynchronously() 它在现代 TPL 使用中不是很有用。它只能在 cold Task -- 尚未启动的情况下调用。

这不是很有用的原因是几乎每个返回 Task 的库方法都会返回 hot Task -- 一个那已经开始了。这包括来自 HttpClient 的那些。当您在已经启动的 Task 上调用它时,您会得到刚刚 运行 进入的异常。