【问题标题】:How to make a loop iterate 60 times a second?如何使循环每秒迭代 60 次?
【发布时间】:2019-04-27 01:43:07
【问题描述】:

我正在查询一个 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);
    }
}

【问题讨论】:

  • 使用计时器。这不是循环的工作
  • 您的 API 密钥已经泄露,请确保重新生成它。
  • @phuclv 我不知道计时器类。将对此进行调查。
  • 要在循环中计算这个,您必须准确知道每个请求花费了多长时间,这意味着您必须能够保证您和服务器之间的延迟永远不会改变。单个条件的任何微小差异都意味着您的循环失败。这意味着循环在这里不是合适的方法。
  • .Result 有潜在危险可能导致死锁。

标签: c# loops dotnet-httpclient


【解决方案1】:

您应该使用 Microsoft 的反应式框架(又名 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 observable。 observable 类似于 enumerable,但不是立即产生所有值,而是一次产生一个值。

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) 是您可以更改以自定义行为的位。

【讨论】:

    【解决方案2】:

    虽然计时器可能会起作用,但这是一种老式的限速方式。如果服务器不希望每秒超过 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() 任务为Task.Delay() 方法弹出一个CancellationToken,延迟可能会更好。

    【讨论】:

    • 我同意这是一个更好的解决方案 Erik,它不会超出 http 连接!
    【解决方案3】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-10-21
      • 2020-06-29
      • 1970-01-01
      • 2016-08-24
      • 2022-07-24
      • 1970-01-01
      • 2015-08-30
      • 2014-07-06
      相关资源
      最近更新 更多