我有一个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));
...但是我该怎么做?
答案 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
,它等待所有异步完成。