【问题标题】:.Net Core Queue Background Tasks.Net Core 队列后台任务
【发布时间】:2023-03-20 11:00:01
【问题描述】:

Slender 回答了我最初的问题,即发送 HTTP 响应后会发生什么触发和遗忘,但现在我剩下的问题是如何正确地将后台任务排队

编辑

众所周知,Async void 通常很糟糕,except for in the case when it comes to event handlers,我想执行一些后台逻辑而不必让客户端等待。我最初的想法是使用 Fire and Forget

假设我有一个活动:

public event EventHandler LongRunningTask;

然后有人订阅了一个fire and forget task:

LongRunningTask += async(s, e) => { await LongNetworkOperation;};

web api 方法被调用:

[HttpGet]
public async IActionResult GetTask()
{
    LongRunningTask?.Invoke(this, EventArgs.Empty);
    return Ok();
}

但是如果我这样做,我的长时间运行的任务不能保证完成,我怎样才能在不影响发出请求的时间的情况下处理正在运行的后台任务(例如,我不想等待任务先完成)?

【问题讨论】:

  • 既不能保证运行完成,也不会被丢弃。网络服务器不是执行长时间运行的作业的最佳场所,您需要了解网络服务器插入代码的后果(例如,如果您在 IIS 和应用程序池托管中运行您的代码会被回收),以及运行“副业”对您的服务器及时实际提供页面的能力的影响。
  • @spender,所以这些任务会泄漏内存,我想这样做的唯一原因是为了我的 websocket,如果更新后需要向 1000 个客户端广播,我希望更新到发生并返回,而不是等待所有这些都必须发送

标签: c# events async-await .net-core


【解决方案1】:

.NET Core 2.1 有一个IHostedService,它将在后台安全地运行任务。我在QueuedHostedServicedocumentation 中找到了一个示例,我已将其修改为使用BackgroundService

public class QueuedHostedService : BackgroundService
{
   
    private Task _backgroundTask;
    private readonly ILogger _logger;

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

    public IBackgroundTaskQueue TaskQueue { get; }

    protected async override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (false == stoppingToken.IsCancellationRequested)
        {
            var workItem = await TaskQueue.DequeueAsync(stoppingToken);
            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                this._logger.LogError(ex, $"Error occurred executing {nameof(workItem)}.");
            }
        }
    }
}

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

    Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}

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

    private 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;
    }
}

现在我们可以在后台安全地将任务排队,而不会影响响应请求所需的时间。

【讨论】:

  • 我可以使用相同的方法在 UI 点击时运行后台任务吗? ,例如当用户单击 View 上的按钮时?!
  • 在 mvc 中?您可以在运行后台任务的服务器上触发操作
  • 如何使特定工作项出列?假设我之前添加了很多工作项。 @johnny5
  • @ThiệnSinh,ExecuteAsync 在执行时自动将工作出列您想在执行之前明确出列工作项吗?
  • @johnny5 是的,在某些情况下,我想在完成之前明确地将工作项出列。你能给我示例代码吗?
【解决方案2】:

只是想在@johnny5 答案中添加一些额外的注释。现在你可以使用https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/ 代替 ConcurrentQueue 和 Semaphore。 代码将是这样的:

public class HostedService: BackgroundService
{
        private readonly ILogger _logger;
        private readonly ChannelReader<Stream> _channel;

        public HostedService(
            ILogger logger,
            ChannelReader<Stream> channel)
        {
            _logger = logger;
            _channel = channel;
        }

        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            await foreach (var item in _channel.ReadAllAsync(cancellationToken))
            {
                try
                {
                    // do your work with data
                }
                catch (Exception e)
                {
                    _logger.Error(e, "An unhandled exception occured");
                }
            }
        }
}

[ApiController]
[Route("api/data/upload")]
public class UploadController : ControllerBase
{
    private readonly ChannelWriter<Stream> _channel;

    public UploadController (
        ChannelWriter<Stream> channel)
    {
        _channel = channel;
    }

    public async Task<IActionResult> Upload([FromForm] FileInfo fileInfo)
    {
        var ms = new MemoryStream();
        await fileInfo.FormFile.CopyToAsync(ms);
        await _channel.WriteAsync(ms);
        return Ok();
    }
}

【讨论】:

    猜你喜欢
    • 2021-02-14
    • 2015-10-05
    • 1970-01-01
    • 1970-01-01
    • 2022-01-07
    • 2018-07-02
    • 2020-11-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多