0

Я хочу реализовать специальный http-клиент, который будет накладывать ограничение на отправку запросов (например, запрос можно делать не чаще, чем раз в секунду). Я написал следующий код:

public class SmartClient
{
    private readonly HttpClient _client;
    private static DateTime? _lastRequest = null;
public SmartClient()
{
    _client = new();
}

public async Task<string> Get(string url)
{
    while (true)
    {
        if (_lastRequest != null && _lastRequest.Value.AddSeconds(1) > DateTime.Now)
        {
            await Task.Delay(100);
            continue;
        }
        break;
    }

    _lastRequest = DateTime.Now;
    Console.WriteLine($"Request: {url}");
    var request = new HttpRequestMessage(HttpMethod.Get, url);
    var response = await _client.SendAsync(request);
    return await response.Content.ReadAsStringAsync();
}

}

Далее я инициализирую 10 объектов SmartClient, создаю на каждый по одной таске .Get() и помещаю все задачи в один список. Потом вызываю await Tasks.WaitAll(tasks); и жду, что все мои запросы выполнятся в течение 10 секунд. Но на практике получаю несколько одновременных запусков по несколько запросов за раз. Что я делаю не так?

Scrzii
  • 186
  • 8
  • Как по вашему один объект SmartClient должен знать про лимит другого SmartClient? SmartClient должен быть один – vitidev Mar 09 '24 at 07:23
  • В моем понимании каждая задача должна выжидать момент, когда не выполнится условие. А дальше пойдет выставит время последнего запроса и пойдет его выполнять. Раз только одна случайная задача дожидается момента и сразу обновляет _lastRequest, то мне казалось, что они отработают по очереди. _lastRequest - статическое поле, общее для всех задач, так что какая разница сколько создается клиентов? – Scrzii Mar 09 '24 at 08:26
  • Оставим вопрос что создавать много HttpClient не рекомендуется и что есть более адекватные методы синхронизации и сразу в суть - у вас типичная гонка потоков, потому что работа с _lastRequest никак не синхронизируется. В общих чертах: 1 заход - все 10 клиентов увидели что _lastRequest== null и сразу пошли к break. 2 и все прочие заходы - все прочитали _lastRequest подождали секунду и снова все ломанулись на выход. – vitidev Mar 09 '24 at 08:32
  • На .net по дефолту асинхронные задачи запускаются в многопоточном режиме? – Scrzii Mar 09 '24 at 08:34
  • Все ЗАВИСИТ от типа создания таски. Но мало кто создает таски вручную (вот их нужно запускать ручками), а обычно получают их как результат вызова метода. А это значит что они планируются на выполнение в текущем контексте синхронизации, а без контекста это пул потоков. А там. если есть свободные потоки, то выполнение наступит сразу. Ну и сразу скажу - решение "обернем все в lock" является плохим решением. Будет бодание и ситуация что второй запрос постоянно будет ждать своей очереди, а остальные влезать пока он спит. – vitidev Mar 09 '24 at 08:48
  • У меня даже с lock не работает) Я использую его в проверке условий на _lastRequest и там, где этот самый _lastRequest обновляется. Результат тот же самый – Scrzii Mar 09 '24 at 08:50
  • Не работает потому что lock и await не могут работать вместе. А lock должен быть обернут сразу и над проверкой и над присвоением нового времени - то есть 1 штука. Вам тут поможет семафор со значением 1 (он из коробки имеет асинк методы). Только не просто "обернуть все" конечно иначе await Task.Delay(100) будет удерживать от семафор от релиза – vitidev Mar 09 '24 at 08:58
  • Я делаю так: создаю bool-переменную needWait, затем в lock присваиваю ей значение и после конца блока lock идет if – Scrzii Mar 09 '24 at 09:18
  • В асинхронном контексте не долдно быть никаких синхронных lock. Отредактируйте вопрос, опишите цель задачи и покажите вызывающий код. Пока ясно только то что клиент должен быть всего один. – aepot Mar 09 '24 at 09:56
  • https://ru.stackoverflow.com/q/1303748/373567 – aepot Mar 09 '24 at 09:58
  • HttpClient очень хорошо дополняется без всяких оберток/декораторов, делается это все при помощи простых Handler классов. В документации есть отличный пример реализации "лимитера", который если немного дописать, получите нужный вам результат. А пока, как вам и сказали выше, у вас должен быть один экземпляр клиента и правильно созданные "Таски", чтоб все работало так, как вы ожидаете, а еще лучше семафор, а еще лучше очередь. – EvgeniyZ Mar 09 '24 at 10:56
  • Давайте я поставлю задачу так. У меня есть много тасок, каждая из которых делает что-то... Я хочу ограничить промежутки между их запусками, например, одной секундой. Каждая таска может выполняться хоть намного меньше секунды, хоть намного больше, главное, чтобы 10 тасок асинхронно запустились в течение ~10 секунд (то есть не существовало такого временного промежутка длиной 1 секунду, в котором бы запустилось одновременно 2 задачи). А после запуска они могут выполняться как угодно, к примеру если каждая таска выполняется 10 секунд, 10 задач должны отработать условно за 19 секунд – Scrzii Mar 11 '24 at 11:41

0 Answers0