通话之间暂停的多个异步通话

时间:2019-07-01 11:52:07

标签: c# async-await

我有一个IEnumerable<Task>,其中每个任务将调用相同的终结点。但是,端点每秒只能处理这么多呼叫。 如何在每次通话之间延迟半秒?

我曾尝试添加Task.Delay(),但是等待它们只是意味着应用程序会等待半秒,然后立即发送所有呼叫。

这是一个代码段:

    var resultTasks = orders
        .Select(async task => 
        {
            var result = new VendorTaskResult();
            try
            {
                result.Response = await result.CallVendorAsync();
            }
            catch(Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        } );

    var results = Task.WhenAll(resultTasks);

我觉得我应该做类似

    Task.WhenAll(resultTasks.EmitOverTime(500));

...但是我该怎么做?

6 个答案:

答案 0 :(得分:0)

这只是一个想法,但另一种方法可能是创建一个队列并添加另一个线程,该线程将对队列进行轮询以查找需要发送到端点的呼叫。

答案 1 :(得分:0)

您是否考虑过通过Task.Delay调用将其变成一个foreach循环?您似乎想按顺序显式地调用它们,如果这在您的代码中很明显,则不会有任何伤害。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Fragments.SalesFragment"
    android:orientation="horizontal"
    android:id="@+id/customersales_content">

    <fragment
        android:id="@+id/inventory_fragment"
        android:name="com.example.devcash.Fragments.PurchaseItemListFragment"
        tools:layout="@layout/fragment_purchase_item_list"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3">

    </fragment>

    <View
        style="@style/Divider"
        android:layout_width="1dp"
        android:layout_height="wrap_content" />

    <fragment
        android:id="@+id/purchaselist_fragment"
        android:name="com.example.devcash.Fragments.PurchaseListFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2"
        tools:layout="@layout/fragment_purchase_list">


    </fragment>


</LinearLayout>

答案 2 :(得分:0)

您可以从订单中循环选择而不是从中进行选择,并在循环内部将结果放入列表中,然后调用Task.WhenAll。 看起来像这样:

var resultTasks = new List<VendorTaskResult>(orders.Count);
orders.ToList().ForEach( item => {
    var result = new VendorTaskResult();
    try
    {
        result.Response = await result.CallVendorAsync();
    }
    catch(Exception ex)
    {
        result.Exception = ex;
    }
    resultTasks.Add(result);
    Thread.Sleep(x);
});

var results = Task.WhenAll(resultTasks);

如果要控制同时执行的请求数,则必须使用信号灯。

答案 3 :(得分:0)

我有一些非常相似的东西,并且对我来说效果很好。请注意,我在Linq查询完成后调用ToArray(),这会触发任务:

using (HttpClient client = new HttpClient()) {
    IEnumerable<Task<string>> _downloads = _group
        .Select(job => {
            await Task.Delay(300);
            return client.GetStringAsync(<url with variable job>);
        });

    Task<string>[] _downloadTasks = _downloads.ToArray();

    _pages = await Task.WhenAll(_downloadTasks);
}

现在请注意,这将创建n个并行的任务,而Task.Delay实际上不执行任何操作。如果您想同步调用页面(听起来像是在调用之间放置了延迟),那么这段代码可能会更好:

using (HttpClient client = new HttpClient()) {
    foreach (string job in _group) {
        await Task.Delay(300);
        _pages.Add(await client.GetStringAsync(<url with variable job>));
    }
}

页面的下载仍然是异步的(同时完成其他任务的下载),但是每次下载页面的调用都是同步的,从而确保您可以等待一个页面完成才能调用下一个页面。

可以轻松地更改代码以分块异步地调用页面,例如每10页一次,等待300ms,如本示例所示:

IEnumerable<string[]> toParse = myData
    .Select((v, i) => new { v.code, group = i / 20 })
    .GroupBy(x => x.group)
    .Select(g => g.Select(x => x.code).ToArray());

using (HttpClient client = new HttpClient()) {
    foreach (string[] _group in toParse) {
        string[] _pages = null;

        IEnumerable<Task<string>> _downloads = _group
            .Select(job => {
                return client.GetStringAsync(<url with job>);
            });

        Task<string>[] _downloadTasks = _downloads.ToArray();

        _pages = await Task.WhenAll(_downloadTasks);

        await Task.Delay(5000);
    }
}

所有这些操作是将您的页面分为20个块,迭代这些块,异步下载该块的所有页面,等待5秒钟,然后移至下一个块。

我希望那是你在等待的东西:)

答案 4 :(得分:0)

您在问题中所描述的就是rate limiting。您想对客户端应用速率限制策略,因为您使用的API会在服务器上强制执行此类策略,以保护自己免受滥用。

虽然您可以自己实施速率限制,但建议您使用一些完善的解决方案。 Rate Limiter from Davis Desmaisons是我随机选择的,我立即喜欢它。它具有可靠的文档,卓越的覆盖范围并且易于使用。也可以作为NuGet package使用。

查看下面的简单代码段,该代码段演示了按顺序运行半重叠任务,同时将紧接前一个任务开始后的半秒任务推迟了。每个任务至少持续750毫秒。

using ComposableAsync;
using RateLimiter;
using System;
using System.Threading.Tasks;

namespace RateLimiterTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Log("Starting tasks ...");
            var constraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromSeconds(0.5));
            var tasks = new[]
            {
                DoWorkAsync("Task1", constraint),
                DoWorkAsync("Task2", constraint),
                DoWorkAsync("Task3", constraint),
                DoWorkAsync("Task4", constraint)
            };
            Task.WaitAll(tasks);
            Log("All tasks finished.");
            Console.ReadLine();
        }

        static void Log(string message)
        {
            Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff ") + message);
        }

        static async Task DoWorkAsync(string name, IDispatcher constraint)
        {
            await constraint;
            Log(name + " started");
            await Task.Delay(750);
            Log(name + " finished");
        }
    }
}

示例输出:

  

10:03:27.121 开始任务...
   10:03:27.154 Task1已启动
   10:03:27.658 Task2已启动
   10:03:27.911 任务1已完成
   10:03:28.160 Task3已启动
   10:03:28.410 Task2已完成
   10:03:28.680 Task4已启动
   10:03:28.913 Task3完成
   10:03:29.443 Task4完成
   10:03:29.443 所有任务都已完成。

如果将约束更改为每秒最多允许两个任务(var constraint = TimeLimiter.GetFromMaxCountByInterval(2, TimeSpan.FromSeconds(1));),而不是每秒半个任务,则输出可能类似于:

  

10:06:03.237 开始任务...
   10:06:03.264 Task1已启动
   10:06:03.268 Task2已启动
   10:06:04.026 任务2已完成
   10:06:04.031 任务1已完成
   10:06:04.275 Task3已启动
   10:06:04.276 Task4已启动
   10:06:05.032 Task4完成
   10:06:05.032 Task3完成
   10:06:05.033 所有任务都已完成。

请注意,当前版本的Rate Limiter针对.NETFramework 4.7.2+或.NETStandard 2.0 +。

答案 5 :(得分:-2)

建议的方法EmitOverTime是可行的,但只能通过阻塞当前线程来实现:

public static IEnumerable<Task<TResult>> EmitOverTime<TResult>(
    this IEnumerable<Task<TResult>> tasks, int delay)
{
    foreach (var item in tasks)
    {
        Thread.Sleep(delay); // Delay by blocking
        yield return item;
    }
}

用法:

var results = await Task.WhenAll(resultTasks.EmitOverTime(500));

可能更好的方法是创建一个Task.WhenAll的变体,该变体接受一个delay参数,并异步延迟:

public static async Task<TResult[]> WhenAllWithDelay<TResult>(
    IEnumerable<Task<TResult>> tasks, int delay)
{
    var tasksList = new List<Task<TResult>>();
    foreach (var task in tasks)
    {
        await Task.Delay(delay).ConfigureAwait(false);
        tasksList.Add(task);
    }
    return await Task.WhenAll(tasksList).ConfigureAwait(false);
}

用法:

var results = await WhenAllWithDelay(resultTasks, 500);

此设计意味着可枚举的任务应仅枚举一次。在开发过程中很容易忘记这一点,然后再次枚举它,从而产生了一组新的任务。因此,我建议将其设为OnlyOnce可枚举,如this question所示。


更新:我应该提到上述方法为何起作用,以及在什么前提下起作用。前提是所提供的IEnumerable<Task<TResult>>被推迟,换言之,未实现。在该方法的开始处,尚未创建任何任务。在枚举的枚举期间,任务是一个接一个地创建的,诀窍是枚举缓慢且受控制。循环内部的延迟可确保不会一次创建所有任务。它们是热创建的(换句话说,已经开始),因此在创建最后一个任务时,某些第一个任务可能已经完成。然后,将半运行/半完成的任务的物化列表传递给Task.WhenAll,它等待所有异步完成。