Как заставить цикл повторяться 60 раз в секунду? - PullRequest
0 голосов
/ 27 апреля 2019

Я запрашиваю 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 ]

1 голос
/ 28 апреля 2019

Вы должны использовать 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) - это биты, которые можно изменить для настройки поведения.

0 голосов
/ 27 апреля 2019

Хотя таймер может работать , это старый способ ограничения скорости. Может не работать, если серверу не нужно более 20 одновременных соединений в секунду. Это означает, что если результат займет больше времени, чем следующий запрос по времени , вы все равно можете получить 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 для метода Task.Delay().

0 голосов
/ 27 апреля 2019

Используйте таймер, как показано ниже. Вы также можете передать переменную для интервала таймера.

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