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

如何在.NET gRPC拦截器中启动长时间运行的后台任务

如何在.NET gRPC拦截器中启动长时间运行的后台任务

这种场景我之前做gRPC服务的时候碰到过,刚好可以给你一套落地的解决方案,咱们一步步来拆解实现哈~

先再明确下你的需求:你在ASP.NET Core的gRPC服务里,要调用一段可能长时间运行、本身还不支持取消的代码;希望超时后先给客户端返回取消响应,同时让后台代码继续跑,等它真正执行完再发个事件通知——用拦截器来做这个思路完全没问题,下面给你具体的实现步骤:


  • 第一步:实现gRPC拦截器,包装调用逻辑
    我们需要继承gRPC的Interceptor类,根据你的调用类型(一元/流式)重写对应的处理方法。核心思路是:给原始调用加超时控制,超时后就把任务丢到后台队列里继续跑,同时给客户端返回响应。

    比如针对最常用的一元gRPC调用,示例代码是这样的:

    public class LongRunningTaskInterceptor : Interceptor
    {
        private readonly IEventPublisher _eventPublisher; // 假设这是你用来发事件的服务
        private readonly IBackgroundTaskQueue _taskQueue; // 用后台任务队列托管长任务,避免依赖请求上下文
    
        public LongRunningTaskInterceptor(IEventPublisher eventPublisher, IBackgroundTaskQueue taskQueue)
        {
            _eventPublisher = eventPublisher;
            _taskQueue = taskQueue;
        }
    
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            // 先定义超时时间,比如30秒,你可以根据业务调整
            var timeout = TimeSpan.FromSeconds(30);
            var timeoutCts = new CancellationTokenSource(timeout);
            // 把请求的取消令牌和超时令牌绑在一起
            var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken, timeoutCts.Token);
    
            try
            {
                // 尝试在超时时间内完成原始调用
                return await continuation(request, context).WaitAsync(combinedCts.Token);
            }
            catch (OperationCanceledException)
            {
                // 区分是超时导致的取消,还是客户端主动取消
                if (timeoutCts.IsCancellationRequested)
                {
                    // 超时了,把长任务丢到后台队列,不用等它完成
                    _taskQueue.QueueBackgroundWorkItem(async token =>
                    {
                        try
                        {
                            // 这里重新执行那段长运行代码,注意!绝对不能用原来的ServerCallContext
                            // 要把request里的必要数据提前取出来传进去,别依赖请求上下文
                            var taskResult = await ExecuteLongRunningLogic(request, token);
                            // 任务完成后发布事件
                            await _eventPublisher.PublishTaskCompletedEvent(taskResult);
                        }
                        catch (Exception ex)
                        {
                            // 后台任务的异常一定要处理,比如打日志,不然静默失败你都不知道
                            Console.WriteLine($"后台任务执行出错:{ex.Message}");
                        }
                    });
    
                    // 给客户端返回取消响应,说明后台还在跑
                    context.Status = new Status(StatusCode.Cancelled, "请求超时,后台任务已继续执行");
                    return default!;
                }
                // 如果是客户端主动取消的,就直接抛出异常就行
                throw;
            }
        }
    
        // 这里模拟你那坨不支持取消的长运行代码
        private async Task<YourTaskResultType> ExecuteLongRunningLogic<TRequest>(TRequest request, CancellationToken token)
        {
            // 注意:如果原代码真的不支持取消,那token可能用不上,但一定要确保不依赖请求资源
            // 替换成你实际的业务逻辑就行
            await Task.Delay(TimeSpan.FromMinutes(5)); // 模拟长时间运行
            return new YourTaskResultType();
        }
    }
    
  • 第二步:实现后台任务队列,托管长任务
    为啥要用后台队列?因为ASP.NET Core的请求上下文在请求结束后会被回收,如果直接用Task.Run跑后台任务,很可能会因为依赖了请求资源导致异常或者内存泄漏。用IBackgroundTaskQueue结合BackgroundService是官方推荐的后台任务托管方式,稳定可靠。

    先定义队列接口和实现:

    public interface IBackgroundTaskQueue
    {
        void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
        Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
    }
    
    public class BackgroundTaskQueue : IBackgroundTaskQueue
    {
        private readonly Channel<Func<CancellationToken, Task>> _queue;
    
        public BackgroundTaskQueue(int capacity)
        {
            // 这里的容量是队列最大长度,根据你的并发量调整
            _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(capacity);
        }
    
        public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
        {
            if (workItem == null)
                throw new ArgumentNullException(nameof(workItem));
    
            _queue.Writer.TryWrite(workItem);
        }
    
        public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
        {
            return await _queue.Reader.ReadAsync(cancellationToken);
        }
    }
    

    再实现一个托管服务来处理队列里的任务:

    public class QueuedHostedService : BackgroundService
    {
        private readonly ILogger<QueuedHostedService> _logger;
        private readonly IBackgroundTaskQueue _taskQueue;
    
        public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
        {
            _taskQueue = taskQueue;
            _logger = logger;
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("后台任务队列服务已启动");
    
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    var workItem = await _taskQueue.DequeueAsync(stoppingToken);
                    await workItem(stoppingToken);
                }
                catch (OperationCanceledException)
                {
                    // 服务停止时的正常取消,不用管
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "后台任务执行失败");
                }
            }
    
            _logger.LogInformation("后台任务队列服务已停止");
        }
    }
    
  • 第三步:把所有服务注册到DI容器
    最后在Program.cs里,把拦截器、后台队列和托管服务都注册进去,让DI容器管理它们的生命周期:

    // 注册后台任务队列,容量设为100,根据你的并发需求调整
    builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(100));
    // 注册后台托管服务
    builder.Services.AddHostedService<QueuedHostedService>();
    // 注册gRPC拦截器
    builder.Services.AddGrpc(options =>
    {
        options.Interceptors.Add<LongRunningTaskInterceptor>();
    });
    

几个一定要注意的点

  1. 绝对不要依赖请求上下文:后台任务里不能用HttpContext或者ServerCallContext里的任何请求相关资源!请求结束后这些资源会被ASP.NET Core回收,直接用会导致各种奇怪的异常或者内存泄漏。一定要把需要的数据提前从request里提取出来,传入后台任务。
  2. 异常处理必须到位:后台任务跑在后台,异常不会直接暴露给客户端,所以一定要加日志或者监控,不然静默失败了你都没法排查问题。
  3. 超时时间要贴合业务:别拍脑袋设个时间,要根据你的业务场景测试后调整,比如如果是大数据计算,超时时间可以设长一点;如果是普通查询,就设短一些。
  4. 流式调用要对应调整:如果你的gRPC方法是流式的(比如服务端流式、双向流式),要对应重写ServerStreamingServerHandler或者DuplexStreamingServerHandler,核心逻辑和一元调用类似,只是处理流式的方式略有不同。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 12:48:01