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

