SignalR Hub多组场景下计时器重叠问题求助
嘿,这个多组计时器阻塞的问题我太熟了!核心问题就是你现在用了单实例计时器,多个组抢同一个资源肯定会乱套。咱们给每个组分配独立的计时器实例,让它们各跑各的就行,我给你捋清楚步骤和代码示例:
问题根源拆解
你当前的计时器是全局单实例,当第二个组触发计时时,会覆盖或抢占第一个组的计时器资源,导致两个组的计时逻辑重叠、其中一组被阻塞。解决的核心思路是:让每个组拥有专属的计时器实例,并用字典把组ID和计时器绑定起来。
具体解决方案
1. 用线程安全的字典存储组与计时器的映射
在你的Hub类里,新增一个字典来管理每个组的计时器,同时加个锁保证多线程环境下的操作安全(或者直接用ConcurrentDictionary):
// 存储组ID对应的计时器 private readonly Dictionary<string, Timer> _groupTimers = new Dictionary<string, Timer>(); // 存储组ID对应的剩余计时秒数(每个组独立维护) private readonly Dictionary<string, int> _groupRemainingTime = new Dictionary<string, int>(); // 线程安全锁 private readonly object _timerLock = new object();
2. 启动组计时器的逻辑
当某个组发起计时请求时,先检查该组是否已有计时器:
- 如果有,可以选择重置计时器(比如重新开始计时)
- 如果没有,就新建一个专属计时器,绑定到该组的计时逻辑
public async Task StartGroupTimer(string groupId, int totalSeconds) { lock (_timerLock) { // 检查组是否已有计时器 if (_groupTimers.TryGetValue(groupId, out var existingTimer)) { // 重置计时器,立即开始每秒触发 existingTimer.Change(0, 1000); // 重置剩余时间 _groupRemainingTime[groupId] = totalSeconds; await Clients.Group(groupId).SendAsync("TimerReset", totalSeconds); return; } // 为该组新建专属计时器 var timer = new Timer(async _ => { lock (_timerLock) { if (_groupRemainingTime.TryGetValue(groupId, out int remainingSeconds)) { if (remainingSeconds > 0) { remainingSeconds--; _groupRemainingTime[groupId] = remainingSeconds; // 只向当前组推送计时更新 await Clients.Group(groupId).SendAsync("UpdateTimer", remainingSeconds); } else { // 计时结束,清理计时器 StopGroupTimer(groupId); await Clients.Group(groupId).SendAsync("TimerCompleted"); } } } }, null, 0, 1000); _groupTimers.Add(groupId, timer); _groupRemainingTime[groupId] = totalSeconds; } await Clients.Group(groupId).SendAsync("TimerStarted", totalSeconds); }
3. 停止并清理计时器的逻辑
当组计时结束、所有用户离开组时,一定要及时清理计时器,避免内存泄漏:
public void StopGroupTimer(string groupId) { lock (_timerLock) { if (_groupTimers.TryGetValue(groupId, out var timer)) { timer.Dispose(); _groupTimers.Remove(groupId); _groupRemainingTime.Remove(groupId); } } } // 可选:当用户断开连接时,如果是组内最后一个用户,自动停止计时器 public override async Task OnDisconnectedAsync(Exception exception) { var userGroups = _connectionMapping.GetGroupsForUser(Context.UserIdentifier); foreach (var groupId in userGroups) { var connectionsInGroup = _connectionMapping.GetConnections(groupId); // 组内只剩当前用户,断开后清空组,停止计时器 if (connectionsInGroup.Count == 1) { StopGroupTimer(groupId); await Clients.Group(groupId).SendAsync("TimerStopped"); } } await base.OnDisconnectedAsync(exception); }
关键注意事项
- 线程安全:SignalR Hub是多线程环境,操作计时器字典和剩余时间字典时必须加锁,或者使用
ConcurrentDictionary替代普通字典 - 资源释放:计时器不再使用时一定要调用
Dispose(),否则会造成内存泄漏 - 计时精度:
Timer的回调是近似触发(不是绝对精准的每秒一次),如果业务需要高精度计时,可以改为记录起始时间,每次回调计算剩余时间,而不是依赖递减计数
内容的提问来源于stack exchange,提问作者Sunny_Sid
相关产品推荐
相关产品推荐

