我正在使用带有asp.net core 2.0的C#开发一个带有REST Api的Web应用程序
我想要实现的是当客户端向端点发送请求时,我将运行与客户端请求上下文分离的后台任务,如果任务成功启动,后台任务将结束。
我知道有HostedService,但问题是HostedService在服务器启动时启动,据我所知,没有办法从控制器手动启动HostedService。
这是一个展示问题的简单代码。
[Authorize(AuthenticationSchemes = "UsersScheme")]
public class UsersController : Controller
{
[HttpPost]
public async Task<JsonResult> StartJob([FromForm] string UserId, [FromServices] IBackgroundJobService backgroundService) {
//check user account
(bool isStarted, string data) result = backgroundService.Start();
return JsonResult(result);
}
}
答案 0 :(得分:17)
您仍然可以将IHostedService
与BlockingCollection
结合使用作为后台任务的基础。
为BlockingCollection
创建包装器,以便将其作为单例注入。
public class TasksToRun
{
private readonly BlockingCollection<TaskSettings> _tasks;
public TasksToRun() => _tasks = new BlockingCollection<TaskSettings>();
public Enqueue(TaskSettings settings) => _tasks.Add(settings);
public Dequeue(CancellationToken token) => _tasks.Take(token);
}
然后执行IHostedService
&#34;听&#34;对于任务和任务&#34;到达&#34;执行它。
如果集合为空,BlockingCollection
将停止执行 - 因此您的while
循环不会消耗处理器时间
.Take
方法接受cancellationToken
作为参数。使用令牌,您可以取消&#34;等待&#34;应用程序停止时的下一个任务。
public class BackgroundService : IHostedService
{
private readonly TasksToRun _tasks;
private CancellationTokenSource _tokenSource;
private Task _currentTask;
public BackgroundService(TasksToRun tasks) => _tasks = tasks;
public async Task StartAsync(CancellationToken cancellationToken)
{
_tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
while (cancellationToken.IsCancellationRequested == false)
{
try
{
var taskToRun = _tasks.Dequeue(_tokenSource.Token);
// We need to save executable task,
// so we can gratefully wait for it's completion in Stop method
_currentTask = ExecuteTask(taskToRun);
await _currentTask;
}
catch (OperationCanceledException)
{
// execution cancelled
}
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_tokenSource.Cancel(); // cancel "waiting" for task in blocking collection
if (_currentTask == null) return;
// wait when _currentTask is complete
await Task.WhenAny(_currentTask, Task.Delay(-1, cancellationToken));
}
}
在控制器中,您只需将要运行的任务添加到我们的集合
public class JobController : Controller
{
private readonly TasksToRun _tasks;
public JobController(TasksToRun tasks) => _tasks = tasks;
public IActionResult PostJob()
{
var settings = CreateTaskSettings();
_tasks.Enqueue(settings);
return Ok();
}
}
用于阻止收集的包装器应该注册为依赖注入为singleton
services.AddSingleton<TasksToRun, TasksToRun>();
注册后台服务
services.AddHostedService<BackgroundService>();
答案 1 :(得分:2)
Microsoft已在https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1
中记录了相同内容它使用BackgroundTaskQueue完成,该工作从Controller分配工作,而工作由从BackgroundService派生的QueueHostedService执行。
答案 2 :(得分:2)
这在很大程度上受到了 documentation 中链接的 skjagini's answer 的启发,并做了一些改进。
我认为在此处重申整个示例可能会有所帮助,以防链接在某些时候中断。我做了一些调整;最值得注意的是,我注入了一个 IServiceScopeFactory
,以允许后台进程自己安全地请求服务。我在这个答案的最后解释了我的推理。
核心思想是创建一个任务队列,用户可以将其注入到他们的控制器中,然后将任务分配给它。 长期运行的托管服务中存在相同的任务队列,该服务一次出列一个任务并执行它。
任务队列:
public interface IBackgroundTaskQueue
{
// Enqueues the given task.
void EnqueueTask(Func<IServiceScopeFactory, CancellationToken, Task> task);
// Dequeues and returns one task. This method blocks until a task becomes available.
Task<Func<IServiceScopeFactory, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly ConcurrentQueue<Func<IServiceScopeFactory, CancellationToken, Task>> _items = new();
// Holds the current count of tasks in the queue.
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
public void EnqueueTask(Func<IServiceScopeFactory, CancellationToken, Task> task)
{
if(task == null)
throw new ArgumentNullException(nameof(task));
_items.Enqueue(task);
_signal.Release();
}
public async Task<Func<IServiceScopeFactory, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
// Wait for task to become available
await _signal.WaitAsync(cancellationToken);
_items.TryDequeue(out var task);
return task;
}
}
在任务队列的核心,我们有一个线程安全的 ConcurrentQueue<>
。由于我们不想在新任务可用之前轮询队列,因此我们使用 SemaphoreSlim
对象来跟踪队列中当前的任务数。每次我们调用 Release
时,内部计数器都会递增。 WaitAsync
方法会阻塞,直到内部计数器大于 0,然后将其递减。
为了出队和执行任务,我们创建了一个后台服务:
public class BackgroundQueueHostedService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<BackgroundQueueHostedService> _logger;
public BackgroundQueueHostedService(IBackgroundTaskQueue taskQueue, IServiceScopeFactory serviceScopeFactory, ILogger<BackgroundQueueHostedService> logger)
{
_taskQueue = taskQueue ?? throw new ArgumentNullException(nameof(taskQueue));
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Dequeue and execute tasks until the application is stopped
while(!stoppingToken.IsCancellationRequested)
{
// Get next task
// This blocks until a task becomes available
var task = await _taskQueue.DequeueAsync(stoppingToken);
try
{
// Run task
await task(_serviceScopeFactory, stoppingToken);
}
catch(Exception ex)
{
_logger.LogError(ex, "An error occured during execution of a background task");
}
}
}
}
最后,我们需要让我们的任务队列可用于依赖注入,并启动我们的后台服务:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
services.AddHostedService<BackgroundQueueHostedService>();
// ...
}
我们现在可以将后台任务队列注入我们的控制器并使任务入队:
public class ExampleController : Controller
{
private readonly IBackgroundTaskQueue _backgroundTaskQueue;
public ExampleController(IBackgroundTaskQueue backgroundTaskQueue)
{
_backgroundTaskQueue = backgroundTaskQueue ?? throw new ArgumentNullException(nameof(backgroundTaskQueue));
}
public IActionResult Index()
{
_backgroundTaskQueue.EnqueueTask(async (serviceScopeFactory, cancellationToken) =>
{
// Get services
using var scope = serviceScopeFactory.CreateScope();
var myService = scope.ServiceProvider.GetRequiredService<IMyService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<ExampleController>>();
try
{
// Do something expensive
await myService.DoSomethingAsync(cancellationToken);
}
catch(Exception ex)
{
logger.LogError(ex, "Could not do something expensive");
}
});
return Ok();
}
}
为什么要使用 IServiceScopeFactory
?
理论上,我们可以直接使用我们注入控制器的服务对象。这可能适用于单例服务,也适用于大多数范围服务。
但是,对于实现 IDisposable
(例如 DbContext
)的范围服务,这可能会中断:在将任务排队后,控制器方法返回并且请求完成。然后框架清理注入的服务。如果我们的后台任务足够慢或延迟,它可能会尝试调用已处置服务的方法,然后会遇到错误。
为避免这种情况,我们的排队任务应始终创建自己的服务范围,并且不应使用来自周围控制器的服务实例。
答案 3 :(得分:0)
如果您试图创建一个“忘却”任务,例如登录到db并且不想丢失dbcontext对象,那么我为您提供了一种简单的方法来实现并详细回答here。
答案 4 :(得分:0)
您可以在 ThreadPool
中使用另一个线程:
将方法排队以供执行。该方法在线程池时执行 线程变为可用。
public class ToDoController : Controller
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public ToDoController(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public string Index()
{
ThreadPool.QueueUserWorkItem(delegate {
// Get services
using var scope = _serviceScopeFactory.CreateScope();
var toDoSvc= scope.ServiceProvider.GetRequiredService<IToDoService>();
DoHeavyWork(toDoSvc);
});
return "Immediate Response";
}
}