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

SignalR Hub调用GetValues返回null 实时股票数据推送问题排查

问题根因分析

    1. StockCaller的AddValue方法未主动运行:你没有在程序启动时将AddValue作为长期后台任务启动,静态字典里从未写入过数据,自然读取不到有效返回值(注意ConcurrentDictionary的Values本身不会为null,你遇到的null大概率是序列化失败、字典为空时前端收到的空值被误判为null)
    1. 数据写入逻辑错误:你用了TryAdd方法,同一个股票代码第一次写入成功后,后续循环写入都会直接失败,根本不会更新股价数据
    1. 无限循环无延迟导致CPU占满:两个while死循环都没有加等待间隔,会占满CPU资源,导致数据写入、推送逻辑都被阻塞
    1. SignalR Hub的生命周期问题:Hub是瞬态对象,在Hub方法内写无限循环会导致当前Hub实例无法释放,还可能触发连接超时,推送逻辑极不稳定
    1. 缺少序列化配置:如果Stock类不是公共类、属性没有公共get访问器,SignalR序列化时会返回null

修复步骤

第一步:修改StockCaller逻辑

public class StockCaller
{
    // 静态字典保持不变,初始化后不会为null
    public static ConcurrentDictionary<string, Stock> _stock = new ConcurrentDictionary<string, Stock>();
    private readonly string[] symbols = new string[6] { "USD", "EUR", "ATLAS", "GARAN", "ISBNK", "AKBNK" };
    // 随机数不要放在循环里实例化,避免生成重复随机值
    private readonly Random random = new Random();

    public async Task AddValue(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            foreach (var item in symbols)
            {
                Stock stock = new Stock()
                {
                    symbol = item,
                    price = random.Next(100, 500),
                    percent = random.NextDouble(),
                };
                // 用AddOrUpdate替代TryAdd,存在就更新,不存在就新增
                _stock.AddOrUpdate(item, stock, (key, oldValue) => stock);
            }
            // 加1秒间隔,避免CPU占满,可根据需要调整频率
            await Task.Delay(1000, stoppingToken);
        }
    }

    public Task<ICollection<Stock>> GetValues()
    {
        // 提前转成列表,避免遍历过程中字典变化报错,同时保证序列化正常
        return Task.FromResult<ICollection<Stock>>(_stock.Values.ToList());
    }
}

注意:确保你的Stock类是public的,所有属性都有public get访问器,示例如下:

public class Stock
{
    public string symbol { get; set; }
    public int price { get; set; }
    public double percent { get; set; }
}

第二步:将StockCaller注册为单例+后台服务

在Program.cs(.NET 6+)里添加注册,让AddValue方法随程序启动自动运行:

// 注册StockCaller为单例
builder.Services.AddSingleton<StockCaller>();
// 注册后台服务运行AddValue逻辑
builder.Services.AddHostedService(sp => 
{
    var stockCaller = sp.GetRequiredService<StockCaller>();
    // 用后台服务的停止token控制循环退出
    return new BackgroundService().ExecuteAsync(async stoppingToken => 
    {
        await stockCaller.AddValue(stoppingToken);
    });
});
// 正常注册SignalR
builder.Services.AddSignalR();

然后中间件部分正常配置Hub路由即可。

第三步:修改MyHub逻辑,移除Hub内的无限循环

Hub内不要写死循环,推送逻辑改到后台服务里统一处理,避免Hub实例被阻塞:

public class MyHub : Hub
{
    public readonly StockCaller _stock;
    public MyHub(StockCaller stock)
    {
        _stock = stock;
    }

    // 仅保留客户端主动拉取数据的方法即可
    public async Task GetStockData()
    {
        var result = await _stock.GetValues();
        await Clients.Caller.SendAsync("receiveMessage", result);
    }
}

第四步:新增定时推送后台服务

统一在后台服务里做定时推送到所有客户端的逻辑:

public class StockPushService : BackgroundService
{
    private readonly IHubContext<MyHub> _hubContext;
    private readonly StockCaller _stockCaller;

    public StockPushService(IHubContext<MyHub> hubContext, StockCaller stockCaller)
    {
        _hubContext = hubContext;
        _stockCaller = stockCaller;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var data = await _stockCaller.GetValues();
            // 推送给所有客户端
            await _hubContext.Clients.All.SendAsync("receiveMessage", data, stoppingToken);
            // 和数据生成的间隔保持一致,可自行调整
            await Task.Delay(1000, stoppingToken);
        }
    }
}

然后在Program.cs里注册这个推送服务:

builder.Services.AddHostedService<StockPushService>();

内容的提问来源于stack exchange,提问作者Yunus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 23:15:02