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

BackgroundService关闭时支持异步调用的CancellationToken.Register替代方案

可行解决方案

你遇到的核心问题是CancellationToken.Register()仅支持注册同步回调,无法直接安全调用异步方法,硬调用GetAwaiter().GetResult()存在线程池死锁风险。推荐以下3种落地方式,优先选前两种:

方案1:重写BackgroundService.StopAsync方法(最规范)

BackgroundService原生提供异步的StopAsync生命周期方法,服务停止时会自动触发,完全不需要通过stoppingToken.Register注册回调,是最优实现:

  1. 把原来的同步ShutDown方法改为异步ShutDownAsync,直接await异步方法
  2. 重写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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 01:54:03