如何使用SemaphoreSlim实现仅首个线程执行Token刷新,其余线程异步释放?
调整思路
你当前的实现虽然通过双重检查避免了重复执行刷新逻辑,但所有等待的线程仍然需要依次排队获取锁、再判断过期状态,高并发下会产生不必要的排队开销。我们可以通过缓存正在执行的刷新任务的方式,让首个线程执行刷新时,其余线程直接等待该任务完成即可,无需排队进锁。
调整后的封装代码
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // 用volatile修饰保证多线程下的可见性,存储当前正在执行的刷新任务 private volatile Task _runningRefreshTask; public async Task RunOnceAsync(Func<Task> worker) { // 第一层检查:如果已有正在执行的刷新任务,直接等待完成即可,无需抢锁 if (_runningRefreshTask != null) { await _runningRefreshTask; return; } await _semaphore.WaitAsync(); try { // 第二层检查:抢锁成功后二次判断,避免多个线程同时通过第一层检查 if (_runningRefreshTask != null) { await _runningRefreshTask; return; } // 首个进入锁的线程执行刷新任务,并存入缓存变量 _runningRefreshTask = worker(); await _runningRefreshTask; } finally { // 任务执行完成(无论成功失败)都清空缓存,释放锁 _runningRefreshTask = null; _semaphore.Release(); } }
业务侧调用逻辑
基本无需修改,仅将原来的LockAsync替换为新的RunOnceAsync即可,原有双重检查可作为兜底保留:
if (!string.IsNullOrEmpty(UserSettings.Token) && !isWithoutRefresh) { if (UserSettings.Expires < ConvertToTimestamp(DateTime.UtcNow.AddMinutes(2))) { await _locker.RunOnceAsync(async () => { if (UserSettings.Expires < ConvertToTimestamp(DateTime.UtcNow.AddMinutes(2))) { if (Role.Guest.Equals(UserSettings.Role) && !string.IsNullOrEmpty(UserSettings.Email)) { //TODO: initialize guest session } else { await RefreshToken(httpClient); await AlertService.ShowAsync("Odswiezony"); } } }); } }
效果说明
- 仅首个触发刷新的线程会进入锁执行刷新逻辑
- 其余同时触发刷新的线程不需要排队抢锁,直接等待同一个刷新任务完成后同步释放
- 刷新任务无论执行成功还是失败,都会清空缓存,下次Token过期时可正常触发新的刷新流程
- 保留了双重检查兜底,不会出现重复刷新、漏刷新的问题
内容的提问来源于stack exchange,提问作者Redduke
相关产品推荐
相关产品推荐

