如何使循环每秒迭代60次?

时间:2019-04-27 01:43:08

标签: c# loops dotnet-httpclient

我正在查询的API数量有限,您可以每秒查询一次。例如,可能允许您每秒进行20个查询。如果您过度查询服务器,则会收到429错误,阻止您访问API。

问题出现在我的代码中的foreach循环中,其中代码的每次迭代都需要查询API。有没有一种方法可以在限制中进行编码,以便foreach在设置的时间限制内仅查询一定次数,因此我没有达到API的查询限制?或者换句话说,我可以让我的foreach循环以每秒20个循环或每秒任何其他数字的速率进行迭代吗?

如果要查看它,则下面是foreach循环,但我认为您不需要它来回答问题。

foreach(var item in matchlistd)
{
    var response2 = client.GetAsync($@"https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/{item.summonerName}apikeyiswhatgoesintherestofthispartoftheapi).Result;
    if (response2.IsSuccessStatusCode)
    {
        var content2 = response2.Content.ReadAsStringAsync().Result;
        summonerName player = JsonConvert.DeserializeObject<summonerName>(content2);
        accountinfo.Add(player);
    }
}

3 个答案:

答案 0 :(得分:1)

您应该使用Microsoft的Reactive Framework(又名Rx)-NuGet System.Reactive并添加using System.Reactive.Linq;-然后您可以做一些非常酷的事情。

首先,我们需要修复您的代码,以便您不必依赖.Result调用。

让我们假设您的代码正在以Main的方法运行-那么您可以更改代码以使其工作如下:

async void Main()
{
    // ...

    string BuildUrl(string summonerName) => $@"https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/{summonerName}apikeyiswhatgoesintherestofthispartoftheapi";

    foreach (var item in matchlistd)
    {
        var response2 = await client.GetAsync(BuildUrl(item.summonerName));
        if (response2.IsSuccessStatusCode)
        {
            var content2 = await response2.Content.ReadAsStringAsync();
            summonerName player = JsonConvert.DeserializeObject<summonerName>(content2);
            accountinfo.Add(player);
        }
    }

    // ...
}

请注意async和两个await关键字。

现在让我们将循环重写为可观察到的Rx。一个可观察对象就像一个可枚举对象,但是不是立即产生所有值,而是一次产生一个值。

IObservable<summonerName> query =
    /* 1 */ from item in matchlistd.ToObservable()
    /* 2 */ from response2 in Observable.FromAsync(() => client.GetAsync(BuildUrl(item.summonerName)))
    /* 3 */ where response2.IsSuccessStatusCode
    /* 4 */ from content2 in Observable.FromAsync(() => response2.Content.ReadAsStringAsync())
    /* 5 */ select JsonConvert.DeserializeObject<summonerName>(content2);
  1. 将您的matchlistd枚举变成可观察的
  2. 调用client.GetAsync并使用Observable.FromAsync解包任务以创建可观察到的消息响应
  3. 过滤掉response2.IsSuccessStatusCode == false
  4. 调用response2.Content.ReadAsStringAsync()并使用Observable.FromAsync解包任务以创建可观察的string
  5. string转换为summonerName

然后您可以执行以下操作以获取所有结果并将其放入列表中:

accountinfo.AddRange(await query.ToList());

现在,我们只想使其每秒最多产生20个查询。这是修改后的查询:

IObservable<summonerName> query =
    from items in matchlistd.ToObservable().Buffer(20).Zip(Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)), (x, t) => x)
    from item in items
    from response2 in Observable.FromAsync(() => client.GetAsync(BuildUrl(item.summonerName)))
    where response2.IsSuccessStatusCode
    from content2 in Observable.FromAsync(() => response2.Content.ReadAsStringAsync())
    select JsonConvert.DeserializeObject<summonerName>(content2);

请注意以下部分:

    from items in matchlistd.ToObservable().Buffer(20).Zip(Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)), (x, t) => x)
    from item in items

那是秘诀。 .Buffer(20)TimeSpan.FromSeconds(1.0)是可以更改以自定义行为的位。

答案 1 :(得分:0)

使用如下计时器。您还可以为计时器间隔传递一个变量。

private Timer _functionTimer; 

public void InitMatchList()
{
    _functionTimer_Tick = new Timer();
    _functionTimer_Tick.Tick += new EventHandler(_functionTimer_Tick);
    _functionTimer_Tick.Interval = 50; // in miliseconds
    _functionTimer_Tick.Start();
}

private void _functionTimer_Tick(object sender, EventArgs e)
{
    var response2 = client.GetAsync($@"https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/{item.summonerName}apikeyiswhatgoesintherestofthispartoftheapi).Result;
    if (response2.IsSuccessStatusCode)
    {
        var content2 = response2.Content.ReadAsStringAsync().Result;
        summonerName player = JsonConvert.DeserializeObject<summonerName>(content2);
        accountinfo.Add(player);
    }
}

答案 2 :(得分:0)

虽然计时器可以工作,但这是限制速率的传统方法。如果服务器每秒不希望超过20个并发连接,则可能无法正常工作。这意味着,如果结果花费的时间比下一个 timed-request 时间长,您可能还会得到429。替代解决方案可能看起来像:

private static readonly httpClient = new HttpClient();
public async Task<IEnumerable<string>> GetAPIResults(IEnuemrable<MatchList> matchLists,
  int maximumRequestsPerSecond)
{
    var requests = matchLists
      .Select(ml => new RequestStatus { MatchList = ml })
      .ToList();

    foreach (var request in requests)
    {
      var activeRequests = RequestStatus
        .Where(rs => 
          (rs.RequestedOn.HasValue && rs.RequestedOn > DateTime.Now.AddSeconds(-1))
          || (rs.Task.HasValue && rs.Task.TaskStatus != TaskStatus.Running))
        .ToList();

      //wait for either a request to complete
      //or for a request not active within the last second to expire
      while (activeRequests > maximumRequestsPerSecond)
      {
        var lastActive = activeRequests.OrderBy(RequestedOn.Value).First();
        var waitFor = DateTime.Now - lastActive.RequestedOn.Value;

        // or maybe this to be safe
        // var waitFor = (DateTime.Now - lastActive.RequestedOn.Value)
        //   .Add(TimeSpan.FromMilliseconds(100));

        await Task.Delay(waitFor);

        activeRequests = RequestStatus
          .Where(rs => 
            (rs.RequestedOn.HasValue && rs.RequestedOn > DateTime.Now.AddSeconds(-1))
            || (rs.Task.HasValue && rs.Task.TaskStatus != TaskStatus.Running))
          .ToList();
    }

      request.RequestTask = httpClient.GetStringAsync(myUrl);       
    }

    await Task.WhenAll(requests.Select(r => r.RequestTask.Value));

    // not sure about .Result here...
    return requests.Select(r => r.RequestTask.Value.Result).ToList();

    // probably safer:
    return requests.Select(r => await r.RequestTask).ToList();
}

public class RequestStatus
{
  public MatchList MatchList { get; set; }
  public DateTime? RequestedOn { get; set }
  public Task<string>? RequestTask { get; set; }
}

如果有一个Task.WhenAll()任务弹出了CancellationToken的{​​{1}}方法,而不是仅仅等待特定的时间,则延迟可能会更好。