Я показываю метки на карте. Когда пользователь прокручивает или уменьшает масштаб, теперь у меня есть дополнительные контакты для отображения. Если при перемещении будет добавлено еще 50 000 контактов, это может занять 4–10 секунд, поскольку запрос к базе данных сложен и, следовательно, может занять время.
Все это находится в веб-приложении сервера Blazor, и все вызовы являются событиями задачи, поэтому пользователь перемещается, начинается получение большего количества контактов, пользовательский интерфейс остается отзывчивым, пользователь перемещается еще немного, отправляется другое событие и т. д.
В настоящее время я использую SemaphoreSlim
, чтобы гарантировать, что одновременно обрабатывается только один ход. При разумном уровне активности со стороны пользователя это может сопровождаться 5 задачами, ожидающими своей очереди, чтобы получить больше булавок.
Однако если ждут 4 задачи, значение имеет только самая последняя. Если это обработано, то на отображаемой карте будут показаны все нужные контакты. Любая предыдущая задача, ожидающая выполнения, теперь не имеет значения.
Что я хотел бы сделать, когда задача завершается и есть 2+ ожидания:
Я не хочу отменять выполняемую задачу, когда приходят дополнительные задачи, потому что показывать что-то лучше, чем пользователь продолжает двигаться, запросы продолжают отменяться, для них ничего не обновляется. Итак, завершите первое, а затем займитесь ожидающей группой.
Как я могу это сделать?
(Я пробовал сохранять запросы, их параметры и отметку DateTime, а затем обрабатывать. И при освобождении мьютекса проверять каждую задачу, которая будет выполняться, чтобы увидеть, может ли она просто вернуть успех. Но у меня есть сложный беспорядок, который, я думаю, глючный.)
@РобертДж. К сожалению, это не то, что я хочу, так как я хочу, чтобы последняя запись была в стеке, а все остальные записи были удалены. И чтобы другие задачи оставались в ожидании, а затем возвращались, когда последняя из завершает обработку.
@DavidThielen - Итак отредактируйте свой вопрос и укажите этот факт.
@SecurityHound Кажется, я сказал это словами «Выполните все задачи перед этой задачей».
Этот вопрос обсуждается на Meta
Разве не должна работать структура данных стека (потокобезопасная или заблокированная вручную)? Потому что его природа — «последним пришел/первым ушел», а это именно то, что вам нужно. Запросы попадают в стек. Когда приходит время работать над следующим запросом, вы всегда извлекаете самый новый из стека, а затем очищаете стек (таким образом отбрасывая устаревшие запросы) за одно атомарное (т. е. синхронизированное) действие.
@Peter-ReinstateMonica да и да. Я придумал более простое решение (см. ниже), но ваше предложение тоже является хорошим подходом.
Одна из идей — отказаться от SemaphoreSlim
и использовать вместо этого цикл асинхронной обработки, который берет элементы из очереди и обрабатывает их по одному. В качестве очереди вы можете использовать ограниченный Канал<T> с емкостью 1, настроенный на отбрасывание самого старого элемента, когда нет места для самого нового элемента:
Channel<PinRequest> pinRequests = Channel.CreateBounded<PinRequest>(
new BoundedChannelOptions(capacity: 1)
{
FullMode = BoundedChannelFullMode.DropOldest,
});
async Task ProcessPinsAsync()
{
await foreach (PinRequest pinRequest in channel.Reader.ReadAllAsync())
{
await ProcessPinRequestAsync(pinRequest);
}
}
Чтобы добавить новый PinRequest
в очередь:
pinRequests.Writer.TryWrite(new PinRequest());
Пояснение: в приведенном выше примере PinRequest
не представляет собой активный Task
. Он представляет состояние, необходимое для запуска Task
. Task
запускается методом ProcessPinRequestAsync
. Итак, PinRequest
— это то, от чего можно смело отказаться. Вам не придется беспокоиться о его «завершении».
Спасибо, это умный способ получить новейшую информацию. Однако как мне выполнить задачи отклоненных запросов? ТИА
@DavidThielen вообще Task
не наполнишь. Вы можете Wait
/await
его завершить или отменить, только если он связан с CancellationTokenSource
, к которому у вашего кода есть доступ. Ваши задачи случайно не контролируются с помощью связанных TaskCompletionSource?
Нет. Они вызывают этот метод, который возвращает задачу. Существующий подход к блокировке мьютекса работает, потому что, когда каждый из них получает свою очередь, они запускают запрос, заполняют контакты и возвращают выполненную задачу. И мне нужна эта задача для выполнения запроса. Только самое последнее задание, но оно мне нужно. Это определенно не простая проблема :) спасибо
Я нашел простой подход. Это работает, потому что, когда поиск заканчивается, я хочу, чтобы выполнялся только самый последний запрос. Все остальные ожидающие запросы могут быть отменены (и их задачи выполнены).
Я заявляю:
/// <summary>
/// The map does not like multiple queries at once. So we use a mutex to ensure only one
/// runs at a time. THe few times this hits, there's only 2 so no point in coalescing all
/// the calls.
/// </summary>
private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1, 1);
/// <summary>
/// This is an increasing counter used to discard out of date map search queries.
/// </summary>
private long _mapSearchCounter;
Затем в начале моего метода у меня есть:
var myCounter = Interlocked.Increment(ref _mapSearchCounter);
await _mutex.WaitAsync();
// if there's another LATER task to search, then we can skip this one.
var latestCounter = Interlocked.Read(ref _mapSearchCounter);
if (myCounter < latestCounter)
return true;
И в конце метода, наконец, у меня есть:
_mutex.Release();
Заблокированные задачи могут быть освобождены в любом порядке: все более старые задачи возвращаются немедленно, а самые последние выполняют запрос и обновляют контакты.
Это должно работать хорошо. Чтобы сделать это немного менее запутанным, вы можете инкапсулировать SemaphoreSlim
и _mapSearchCounter
в специальный класс, например, с именем AsyncMutex
. bool isLatest = await _mutex.WaitAsync();
вернет значение bool
, которое будет true
, если нет ожидающей новой WaitAsync
операции.
Рассматривали ли вы возможность использования Queue? Learn.microsoft.com/en-us/dotnet/api/…