使用 Task.WhenAll 一次向我的 WebAPI 发送多个请求

Posted

技术标签:

【中文标题】使用 Task.WhenAll 一次向我的 WebAPI 发送多个请求【英文标题】:Send multiple requests at once to my WebAPI using Task.WhenAll 【发布时间】:2020-08-01 05:20:43 【问题描述】:

我试图(几乎)一次向我的 WebAPI 发送多个相同的请求以进行一些性能测试。 为此,我多次致电PerformRequest 并使用await Task.WhenAll 等待他们。

我想计算完成每个请求所需的时间加上每个请求的开始时间。但是,在我的代码中,我不知道如果 R3(请求号 3)的结果出现在 R1 之前会发生什么?时长会出错吗?

从我在结果中看到的,我认为结果是相互混合的。例如,R4 的结果集为 R1 的结果。所以任何帮助将不胜感激。

GlobalStopWatcher 是一个静态类,我用它来查找每个请求的开始时间。

基本上我想确保每个请求的elapsedMillisecondsDuration 都与请求本身相关联。 因此,如果第 10 次请求的结果在第 1 次请求的结果之前,则持续时间将为duration = elapsedTime(10th)-(startTime(1st))。不是这样吗?

我想添加一个lock,但似乎不可能在有await 关键字的地方添加它。

 public async Task<RequestResult> PerformRequest(RequestPayload requestPayload)
    
        var url = "myUrl.com";

        var client = new RestClient(url)  Timeout = -1 ;

        var request = new RestRequest  Method = Method.POST ;

        request.AddHeaders(requestPayload.Headers);
        foreach (var cookie in requestPayload.Cookies)
        
            request.AddCookie(cookie.Key, cookie.Value);
        
        request.AddJsonBody(requestPayload.BodyRequest);

        var st = new Stopwatch();
        st.Start();
        var elapsedMilliseconds = GlobalStopWatcher.Stopwatch.ElapsedMilliseconds;
        var result = await client.ExecuteAsync(request).ConfigureAwait(false);
        st.Stop();
        var duration = st.ElapsedMilliseconds;

        return new RequestResult() 
         
           Millisecond = elapsedMilliseconds,
           Content = result.Content,
           Duration = duration
        ;
     



public async Task RunAllTasks(int numberOfRequests)

    GlobalStopWatcher.Stopwatch.Start();

    var arrTasks = new Task<RequestResult>[numberOfRequests];
    for (var i = 0; i < numberOfRequests; i++)
    
         arrTasks[i] = _requestService.PerformRequest(requestPayload, false);
    

    var results = await Task.WhenAll(arrTasks).ConfigureAwait(false);

    RequestsFinished?.Invoke(this, results.ToList());

【问题讨论】:

是什么让你觉得结果好坏参半? @GabrielLuci 假设我们在ForLoop 中触发了 100 个请求。例如,第一个请求的 elapsedMilliseconds 将是 1ms 和最后一个请求的 100ms。但是,第 100 个请求的结果在第一个请求的结果之前。那不是返回第 100 个请求(先到)的结果内容和第一个请求(最后一个)的elapsedMilliseconds 吗? 每个请求都有一个单独的本地Stopwatch,所有请求都有一个全局Stopwatch。您将“持续时间”称为本地Stopwatch 获得的测量值,将“毫秒”称为全局Stopwatch 获得的测量值。您担心两种测量中的哪一种可能是错误的,是局部的、全局的还是两者兼而有之? @TheodorZoulias 他们俩。我想知道该请求在多少毫秒后触发(elapsedMilliseconds)以及该请求需要多长时间才能完成(duration 那你的问题可能太宽泛了。在这里,每个帖子我们只能问一个问题。关于全局Stopwatch,你可能会觉得这个问题很有趣:Is Stopwatch.ElapsedTicks threadsafe? 【参考方案1】:

我认为你会出错的地方是尝试使用静态 GlobalStopWatcher,然后将此代码推送到你正在测试的函数中。

您应该将所有内容分开,并为每个 RunAllTasks 调用使用一个新的 Stopwatch 实例。

让我们这样吧。

从这些开始:

public async Task<RequestResult<R>> ExecuteAsync<R>(Stopwatch global, Func<Task<R>> process)

    var s = global.ElapsedMilliseconds;
    var c = await process();
    var d = global.ElapsedMilliseconds - s;
    return new RequestResult<R>()
    
        Content = c,
        Millisecond = s,
        Duration = d
    ;


public class RequestResult<R>

    public R Content;
    public long Millisecond;
    public long Duration;

现在您可以测试任何符合Func&lt;Task&lt;R&gt;&gt; 签名的内容。

让我们试试这个:

public async Task<int> DummyAsync(int x)

    await Task.Delay(TimeSpan.FromSeconds(x % 3));
    return x;

我们可以这样设置测试:

public async Task<RequestResult<int>[]> RunAllTasks(int numberOfRequests)

    var sw = Stopwatch.StartNew();
    var tasks = 
        from i in Enumerable.Range(0, numberOfRequests)
        select ExecuteAsync<int>(sw, () => DummyAsync(i));
    return await Task.WhenAll(tasks).ConfigureAwait(false);

请注意,var sw = Stopwatch.StartNew(); 行会为每个 RunAllTasks 调用捕获一个新的 Stopwatch。没有什么是真正的“全球性”了。

如果我使用RunAllTasks(7) 执行该操作,那么我会得到以下结果:

它运行并且计数正确。

现在你可以重构你的 PerformRequest 方法来做它需要做的事情:

public async Task<string> PerformRequest(RequestPayload requestPayload)

    var url = "myUrl.com";
    var client = new RestClient(url)  Timeout = -1 ;
    var request = new RestRequest  Method = Method.POST ;
    request.AddHeaders(requestPayload.Headers);
    foreach (var cookie in requestPayload.Cookies)
    
        request.AddCookie(cookie.Key, cookie.Value);
    
    request.AddJsonBody(requestPayload.BodyRequest);
    var response = await client.ExecuteAsync(request);
    return response.Content;

运行测试很容易:

public async Task<RequestResult<string>[]> RunAllTasks(int numberOfRequests)

    var sw = Stopwatch.StartNew();
    var tasks = 
        from i in Enumerable.Range(0, numberOfRequests)
        select ExecuteAsync<string>(sw, () => _requestService.PerformRequest(requestPayload));
    return await Task.WhenAll(tasks).ConfigureAwait(false);


如果对Stopwatch 的线程安全性有任何疑问,那么您可以这样做:

public async Task<RequestResult<R>> ExecuteAsync<R>(Func<long> getMilliseconds, Func<Task<R>> process)

    var s = getMilliseconds();
    var c = await process();
    var d = getMilliseconds() - s;
    return new RequestResult<R>()
    
        Content = c,
        Millisecond = s,
        Duration = d
    ;


public async Task<RequestResult<int>[]> RunAllTasks(int numberOfRequests)

    var sw = Stopwatch.StartNew();
    var tasks = 
        from i in Enumerable.Range(0, numberOfRequests)
        select ExecuteAsync<int>(() =>  lock (sw)  return sw.ElapsedMilliseconds;  , () => DummyAsync(i));
    return await Task.WhenAll(tasks).ConfigureAwait(false);

【讨论】:

不错的答案。不过,在不同步的情况下从多个线程访问非线程安全类的实例属性ElapsedMilliseconds 似乎有风险。 @TheodorZoulias - 不是根据链接:***.com/questions/6664538/…。那里有很多答案说,只要您不启动、停止或重新启动Stopwatch,它就是安全的。我想我可以用Func&lt;long&gt; 把它包起来,然后把它锁起来。 @Enigmativity 非常好的答案。一个问题。 lock 那里是因为 Task.WhenAll 并行运行所有任务吗? @MehrdadKamelzadeh - lock 存在是因为我们希望确保对 Stopwatch 的访问是线程安全的。尽管如此,Task.WhenAll 不会创建或使用任何线程。以防万一此代码被更改,以便ExecAsync(或更深层次的东西)使用其他线程。

以上是关于使用 Task.WhenAll 一次向我的 WebAPI 发送多个请求的主要内容,如果未能解决你的问题,请参考以下文章

c# Task.WhenAll(tasks) 和 SemaphoreSlim - 如何知道所有任务何时已完全完成

Parallel.ForEach 与 Task.Run 和 Task.WhenAll

从 Task.WhenAll 调用的异步方法使用 DbContext 并返回错误

Task CancellationTokenSource和Task.WhenAll的应用

csharp Task.WhenAllを利用している场合のタイムアウト处理の书き方(Task.WhenAll,Task.WhenAny,Task.Delay)

Task.WhenAll(taskList).Wait() 是不是与 Task.WaitAll(taskList) 相同?