Это не простая проблема, чтобы решить. У меня похожая проблема с кешем: я хочу, чтобы по истечении срока действия кеша был сделан только один вызов для его повторного заполнения. Очень распространенный подход к токену, например что вы должны обновлять время от времени.
Проблема с обычным использованием семафора заключается в том, что после выхода все ожидающие потоки просто входят и снова выполняют вызов, поэтому для его исправления требуется двойная проверка блокировки. Если у вас может быть какое-то локальное состояние для вашего случая, я не уверен (я полагаю, что у вас есть, поскольку у вас есть причина сделать только один вызов и, скорее всего, есть состояние), но вот как я решил это для кэша токена:
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
public async Task<string> GetOrCreateAsync(Func<Task<TokenResponse>> getToken)
{
string token = Get();
if (token == null)
{
await _semaphore.WaitAsync();
try
{
token = Get();
if (token == null)
{
var data = await getToken();
Set(data);
token = data.AccessToken;
}
}
finally
{
_semaphore.Release();
}
}
return token;
}
Теперь я не знаю, пуленепробиваемые. Если бы это была обычная блокировка с двойной проверкой (не асинхронная), то это не так, хотя объяснение, почему это действительно сложно, объясняет, как процессор выполняет многопоточность за кулисами и как они переупорядочивают инструкции.
Но в случае с кешем, если в голубой луне происходит двойной вызов один раз, это не такая большая проблема.
Я не нашел лучшего способа сделать это, и это пример, предоставленный, например, Скотт Хансельман также нашел несколько мест на переполнении стека.