Search code examples
c#visual-studio-2017.net-standard-1.6

Odd Async Issues with HttpClient


Working on using the HttpClient to convert WebClient code from .Net Framework 4.6.1 to NetStandard 1.6, and having an odd issue. Here's my code blocks that are in question:

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);
    }

So, when calling the TestDownloadSpeed function everything works as expected, but when I call the TestUploadSpeed method I get the error InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.* on this portion of TestSpeed.

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

I am really wracking my brain trying to figure out what in TestUploadSpeed is blowing things up. Anyone have any hints to point me in the right direction?

If it helps, here's the .Net 4.6.1 code that works without issue, so maybe something in my translation is off? Plus the original code runs about 5 times faster so not sure what that's about...

    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);
    }

Solution

  • tl;dr

    To fix your specific example, you want to call Wait() to synchronously wait for the Task to complete. Not RunSynchronously().

    But you probably actually want to await the Task to allow asynchronous completion. Wait() isn't very good for performance in most cases and has some characteristics that can cause deadlock if used unwisely.

    long explanation

    RunSynchronously() has a subtly different usage than Wait(). It runs the Task synchronously, while Wait will wait synchronously but doesn't dictate anything about how it should run.

    RunSynchronously() it isn't very useful in modern TPL usage. It is meant to be called only on a cold Task -- one that hasn't been started.

    The reason this isn't very useful is that just about every library method returning a Task will be returning a hot Task -- one that has already started. This includes the ones from HttpClient. When you call it on a Task that has already been started, you get the exception you've just run into.