You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:06:48