我想问一个我想到的问题。这个关于内存访问的问题,包含了在asp.net core中具有单例生命周期的对象。假设在这个结构中存在两个线程。其中之一是asp.net中使用的普通请求/响应线程。另一个线程在后台持续运行worker服务。
我的计划是创建任务队列。在队列中,我存储了不想在请求/响应线程中执行的任务。此存储函数在后台连续执行。
此代码分区包含一个任务队列。所以这个类使用后台worker服务和asp.net中的任何地方。
public class EventQueue : IEventQueue
{
public LinkedList<Task> Queue = new LinkedList<Task>();
public void AddEvent(Task task)
{
Queue.AddFirst(task);
}
public Task GetNextEvent()
{
var task = Queue.Last.Value;
Queue.RemoveLast();
return task;
}
}
这个代码分区包含了一个worker服务。它一个接一个地执行队列任务
public class QueueWorker : BackgroundService
{
private readonly IEventQueue _queue;
public QueueWorker(IEventQueue queue)
{
_queue = queue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var task = _queue.GetNextEvent();
if (task != null)
task.RunSynchronously();
}
}
}
这个代码分区包含了注册的服务。
services.AddSingleton<IEventQueue,EventQueue>();
services.AddHostedService<QueueWorker>();
问题:
- 这个结构工作得好吗?我认为,它不会工作得很好,因为有多个访问队列实例。或者更确切地说,工作者服务总是访问队列实例。因此,将没有时间访问其他线程。所以这种方法是对的?
- 如果单例生命周期没有使用,EventQueue是静态的(至少LinkedList属性是静态的),事情会有所不同吗?
- 对于这个结构你有什么改进的建议吗?
Channel
class Message { public int AmountToIncrement { get; set; } }
可以将Channel<Message>
,ChannelReader<Message>
和ChannelWriter<Message>
注册为单例:
services.AddSingleton<Channel<Message>>(Channel.CreateUnbounded<Message>());
services.AddSingleton<ChannelReader<Message>>(svc => svc.GetRequiredService<Channel<Message>>().Reader);
services.AddSingleton<ChannelWriter<Message>>(svc => svc.GetRequiredService<Channel<Message>>().Writer);
所以现在你可以注入ChannelWriter<Message>
到你想要发送消息的地方,和ChannelReader<Message>
到你的后台服务(通过构造函数,例如)。您的ExecuteAsync
可以使用ChannelReader<T>.ReadAsync
来等待消息并相应地处理它。
对于这样的任务,我建议看一下ConcurrentQueue集合。
这应该提供一个线程安全的集合,而不需要任何锁。如果不能使用前面提到的队列,也可以使用SemaphoreSlim对象作为锁。
据我所知,使用ConcurrentQueue应该删除您提到的访问块。如果你使用SemaphoreSlim,那么我建议在后台服务循环中设置一个小的Task.Delay()。
为了改进,你可以尝试重构这个服务来利用事件,这样当有东西被添加到队列中时,后台服务就会开始处理,直到什么都没有剩下。
我建议不要在一个单独的队列线程上处理任务,并且有一个worker后台。
可以考虑使用异步任务,.net非常擅长传递数据,因为每个任务都有自己的线程,无论如何,这种结构释放了足够的响应线程:
[HttpGet("")]
public async Task<IActionResult> Get(){
return await Task<IActionResult>.Factory.StartNew(() => {
//TODO: Your lengthy task here
});
}
现在假设你想要一个工作者类型对象,它必须是一个单例吗?我猜不会,但是不管它可以被注入,但是单例必须处理并发请求,它们真的必须考虑使用。
private readonly IWorkerInterface _worker;
public ControllerConstructor(IWorkerInterface worker){
//Assign variable to controller variable
_worker = worker ?? throw new ArgumentNullException(nameof(worker));
}
[HttpGet("")]
public async Task<IActionResult> Get(){
return await Task<IActionResult>.Factory.StartNew(() => {
//TODO: Your lengthy task here
_worker.DoWork();
});
}
你得到的异步等待模式是,一旦任务从工厂创建,线程被释放服务,并创建了一个等待句柄,这真的是快,只有在任务完成后才需要再次。
如果你想要一个线程安全的队列,考虑使用ConcurrentQueue
后台worker的问题,正如我所看到的,它不是一个线程池,除非它需要它,并且线程池已经通过task.factory
供您使用毕竟,如果我们真的想要一个队列,而不是预先处理我们的工作,难道我们不希望它持久,这样一旦控制器接受请求,它就不会在服务器崩溃时丢失吗?那么你需要考虑另一种技术,而不是与web服务器一起生死存亡的技术。
现在你可以让你的web服务器在一个持久的服务总线上发布一个主题,这个主题是由你的后台工作人员订阅的,只有在处理实际成功时才完成…我认为你的想法是这样的,但我担心,试图建立它到web服务器的RAM存储最终会给你头痛。