BackgroundService关闭时支持异步调用的CancellationToken.Register替代方案
可行解决方案
你遇到的核心问题是CancellationToken.Register()仅支持注册同步回调,无法直接安全调用异步方法,硬调用GetAwaiter().GetResult()存在线程池死锁风险。推荐以下3种落地方式,优先选前两种:
方案1:重写BackgroundService.StopAsync方法(最规范)
BackgroundService原生提供异步的StopAsync生命周期方法,服务停止时会自动触发,完全不需要通过stoppingToken.Register注册回调,是最优实现:
- 把原来的同步
ShutDown方法改为异步ShutDownAsync,直接await异步方法 - 重写
StopAsync,在里面调用ShutDownAsync即可
方案2:在ExecuteAsync末尾等待取消信号后执行清理
在ExecuteAsync逻辑最后添加等待取消的逻辑,触发后直接执行异步清理:
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 原有业务逻辑不变,删除stoppingToken.Register(ShutDown); // 新增:等待取消信号触发 try { await Task.Delay(Timeout.Infinite, stoppingToken); } catch (OperationCanceledException) { // 触发取消后执行异步清理 await ShutDownAsync(); } }
方案3:使用CancellationToken.RegisterAsync(.NET 6+ 适用)
如果你用的是.NET 6及以上版本,可以直接用异步注册方法:
stoppingToken.Register(async () => await ShutDownAsync());
修改后的完整代码
using Binance.Net; using Binance.Net.Enums; using Binance.Net.Interfaces; using Binance.Net.Objects; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; namespace QSGEngine.Server.Services { public class PortfolioService : BackgroundService { private const string ApiKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; private const string SecretKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; private readonly ILogger<PortfolioService> _logger; private readonly IBinanceClient _restClient; private readonly IBinanceSocketClient _socketClient; public PortfolioService(ILogger<PortfolioService> logger) { _logger = logger; _restClient = new BinanceClient(new BinanceClientOptions { ApiCredentials = new ApiCredentials(ApiKey, SecretKey), AutoTimestamp = true, AutoTimestampRecalculationInterval = TimeSpan.FromMinutes(30), TradeRulesBehaviour = TradeRulesBehaviour.AutoComply, #if DEBUG LogLevel = LogLevel.Debug, LogWriters = new List<ILogger> { _logger } // TODO: FIX #endif }); _socketClient = new BinanceSocketClient(new BinanceSocketClientOptions { ApiCredentials = new ApiCredentials(ApiKey, SecretKey), AutoReconnect = true, ReconnectInterval = TimeSpan.FromSeconds(15), #if DEBUG LogLevel = LogLevel.Debug, LogWriters = new List<ILogger> { _logger } // TODO: FIX #endif }); } private string? _listenKey; private CallResult<UpdateSubscription>? _userDataUpdateSubscription; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Create listen key var listenKeyResult = await _restClient.Spot.UserStream.StartUserStreamAsync(stoppingToken); if (!listenKeyResult.Success) { return; } _listenKey = listenKeyResult.Data; // Subscribe to web socket _userDataUpdateSubscription = await _socketClient.Spot.SubscribeToUserDataUpdatesAsync(_listenKey, null, null, data => { _logger.LogInformation("ASD {Balances}", data.Data.Balances); }, data => { _logger.LogInformation("BALANCE DELTA {BalanceDelta}", data.Data.BalanceDelta); }).ConfigureAwait(false); if (!_userDataUpdateSubscription.Success) { return; } _userDataUpdateSubscription.Data.Exception += PortfolioService_Exception; // Keep the listen key alive using var keepAlive = Task.Run(async () => { while (!stoppingToken.IsCancellationRequested) { // Listen key will be alive for 60 minutes await _restClient.Spot.UserStream.KeepAliveUserStreamAsync(_listenKey, stoppingToken).ConfigureAwait(false); await Task.Delay(TimeSpan.FromMinutes(50), stoppingToken).ConfigureAwait(false); } }, stoppingToken); // 等待取消信号 try { await Task.Delay(Timeout.Infinite, stoppingToken); } catch (OperationCanceledException) { await ShutDownAsync(); } } // 修改为异步清理方法 private async Task ShutDownAsync() { // Unsubscribe from Exception event if (_userDataUpdateSubscription != null) { _userDataUpdateSubscription.Data.Exception -= PortfolioService_Exception; _userDataUpdateSubscription = null; } // Stop listen key if (_listenKey != null) { // 直接await异步方法,不需要GetResult await _restClient.Spot.UserStream.StopUserStreamAsync(_listenKey); _listenKey = null; } } // 也可以选择重写StopAsync,不需要上面的等待取消逻辑,二选一即可 // public override async Task StopAsync(CancellationToken cancellationToken) // { // await ShutDownAsync(); // await base.StopAsync(cancellationToken); // } private void PortfolioService_Exception(Exception ex) { _logger.LogInformation("Exception: {StackTrace}", ex.StackTrace); } } }
注意事项
- 清理逻辑建议加try-catch包裹,避免清理过程抛出的异常导致服务退出异常
- 重写StopAsync的方案不需要修改ExecuteAsync的原有逻辑,更适合已有复杂业务逻辑的场景
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

