【问题标题】:How to run a C# task in background so the UI is not blocked?如何在后台运行 C# 任务以免 UI 被阻塞?
【发布时间】:2018-02-23 05:14:40
【问题描述】:

我有一个 ASP.NET MVC Web 应用程序。

在某个时刻,UI 用户向服务器发出 POST。服务器必须在不同的线程中做一些繁重的操作,并尽快向用户返回响应。

返回给 UI 的响应不依赖于繁重操作的结果,因此 UI 不需要阻塞,直到繁重的操作完成。这个 POST 方法的行为应该像一些大型计算内容的触发器。应立即通知用户服务器已开始执行繁重的操作。

应该发生的事情的骨架是:

[HttpPost]
public ActionResult DoSomething(PostViewModel model)
{
    //////////////////////////
    /*
        * The code in this section should run asynchronously (in another thread I guess).
        * That means the UI should not wait for any of these operations to end.
        * 
        * */
    ComputeHeavyOperations();
    //////////////////////////


    //the response should be returned immediatelly 
    return Json("Heavy operations have been triggered.");
}

private void ComputeHeavyOperations()
{
    //execute some heavy operations; like encoding a video 
}

我怎样才能实现这样的东西?

【问题讨论】:

  • 通常发生的情况是网页将数据放在某处,并且服务器上的一个单独进程处理数据并设置网页定期检查的通知。尝试在 IIS 线程池中执行此操作肯定会拖累 Web 端,即使您将其卸载到单独的线程。
  • 繁重的计算内容实际上并不在同一台机器上运行。编码由外部服务完成。我的应用程序在 ComputeHeavyOperations() 方法中所做的只是等待服务完成其操作。
  • 你知道 async 和 await 关键字吗?
  • @seesharper 是的,我知道。但我就是无法做到正确。能否提供一些代码示例?
  • 考虑像HangFire这样的框架(参考How to run Background Tasks in ASP.NET

标签: c# asp.net-mvc asynchronous


【解决方案1】:

您可以使用排队的后台任务并实现BackgroundService。 这个link很有用。

public class BackgroundTaskQueue : IBackgroundTaskQueue
    {
        private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
            new ConcurrentQueue<Func<CancellationToken, Task>>();
        private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);

        public void QueueBackgroundWorkItem(
            Func<CancellationToken, Task> workItem)
        {
            if (workItem == null)
            {
                throw new ArgumentNullException(nameof(workItem));
            }

            _workItems.Enqueue(workItem);
            _signal.Release();
        }

        public async Task<Func<CancellationToken, Task>> DequeueAsync(
            CancellationToken cancellationToken)
        {
            await _signal.WaitAsync(cancellationToken);
            _workItems.TryDequeue(out var workItem);

            return workItem;
        }
    }

QueueHostedService 中,队列中的后台任务被出列并作为BackgroundService 执行,这是实现长时间运行IHostedService 的基类:

public class QueuedHostedService : BackgroundService
    {
        private readonly ILogger _logger;

        public QueuedHostedService(IBackgroundTaskQueue taskQueue,
            ILoggerFactory loggerFactory)
        {
            TaskQueue = taskQueue;
            _logger = loggerFactory.CreateLogger<QueuedHostedService>();
        }

        public IBackgroundTaskQueue TaskQueue { get; }

        protected override async Task ExecuteAsync(
            CancellationToken cancellationToken)
        {
            _logger.LogInformation("Queued Hosted Service is starting.");

            while (!cancellationToken.IsCancellationRequested)
            {
                var workItem = await TaskQueue.DequeueAsync(cancellationToken);

                try
                {
                    await workItem(cancellationToken);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex,
                        $"Error occurred executing {nameof(workItem)}.");
                }
            }

            _logger.LogInformation("Queued Hosted Service is stopping.");
        }
    }

服务注册在Startup.ConfigureServicesIHostedService 实现注册了AddHostedService 扩展方法:

services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();

在控制器中: IBackgroundTaskQueue 被注入构造函数并分配给队列。 一个IServiceScopeFactory 被注入并分配给_serviceScopeFactory。工厂用于创建IServiceScope 的实例,用于在范围内创建服务。创建范围是为了使用应用程序的AppDbContext(范围服务)在IBackgroundTaskQueue(单例服务)中写入数据库记录。

public class SomeController : Controller
{
    private readonly AppDbContext _db;
    private readonly ILogger _logger;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public SomeController(AppDbContext db, IBackgroundTaskQueue queue, 
        ILogger<SomeController> logger, IServiceScopeFactory serviceScopeFactory)
    {
            _db = db;
            _logger = logger;
            Queue = queue;
            _serviceScopeFactory = serviceScopeFactory;
    }

    public IBackgroundTaskQueue Queue { get; }

    [HttpPost]
    public ActionResult DoSomething(PostViewModel model)
    {
        //////////////////////////
        /*
            * The code in this section should run asynchronously (in another thread I guess).
            * That means the UI should not wait for any of these operations to end.
            * 
            * */
        ComputeHeavyOperations();
        //////////////////////////


        //the response should be returned immediatelly 
        return Json("Heavy operations have been triggered.");
    }

    private void ComputeHeavyOperations()
    {
        Queue.QueueBackgroundWorkItem(async token =>
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                var scopedServices = scope.ServiceProvider;
                var db = scopedServices.GetRequiredService<AppDbContext>();

                    try
                    {
                        //use db to crud operation on database
                        db.doSomeThingOnDatabase();
                        await db.SaveChangesAsync();
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, 
                            "An error occurred writing to the " +
                            $"database. Error: {ex.Message}");
                    }

                    await Task.Delay(TimeSpan.FromSeconds(5), token);        

      }
        _logger.LogInformation(
            "some background task have done on database successfully!");
    });
} }

【讨论】:

    【解决方案2】:

    你可以使用 async 和 await

    [HttpPost]
    public async Task<ActionResult> DoSomething(PostViewModel model)
    {                    
        Task.Run(async () => await ComputeHeavyOperations());
        //the response should be returned immediatelly 
        return Json("Heavy operations have been triggered.");
    }
    
    private  async void ComputeHeavyOperations()
    {
        //execute some heavy operations; like encoding a video 
        // you can use Task here
    }
    

    【讨论】:

    • 不建议在 ASP.NET 中启动长时间运行的任务/线程,因为 ASP.NET 不知道您在做什么,并且您的 AppPool 有被回收的危险
    • @MickyD 是的,有道理
    【解决方案3】:

    Task.Factory?.StartNew(() => ComputeHeavyOperations(), TaskCreationOptions.LongRunning);

    【讨论】:

    • 不建议在 ASP.NET 中启动长时间运行的任务/线程,因为 ASP.NET 不知道您在做什么,并且您的 AppPool 有被回收的危险
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-17
    相关资源
    最近更新 更多