在 .net 中多次访问单例对象

Multiple accessing to singleton object in .net

我想问一个我想到的问题。这个关于内存访问的问题,包含 asp.net 核心中具有单例生命周期的对象。所以,假设在这个结构中存在两个线程。其中之一是 asp net 中使用的普通 request/response 线程。另一个线程在后台连续 运行 工作者服务。

我的计划是创建任务队列。在队列中,我存储了我不想在 request/response 线程中执行的任务。此存储函数在后台连续执行。

此代码分区包含任务队列。所以这个 class 在后台工作人员服务和 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;
            }
    }

此代码分区包含工作程序服务。它在队列任务

中一个一个执行
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>();

问题:

对于这样的任务,我建议看一下 ConcurrentQueue 集合。

这应该提供一个不需要任何锁的线程安全集合。 或者,如果您不能使用上述队列,您可以使用 SemaphoreSlim 对象作为锁。

据我所知,使用 ConcurrentQueue 应该删除您提到的访问块。如果您使用 SemaphoreSlim,那么我建议在后台服务循环中使用一个小 Task.Delay()。

至于改进,您总是可以尝试重构此服务以利用事件,因此当有内容添加到队列时,后台服务将开始处理,直到什么都没有为止。

我建议不要在具有工作背景的单独队列线程上处理任务。

而不是考虑使用异步任务,.net 非常擅长推送数据,因为每个任务都有自己的线程,无论如何这种结构足以释放您的响应线程:

[HttpGet("")]
public async Task<IActionResult> Get(){
  return await Task<IActionResult>.Factory.StartNew(() => {
      //TODO: Your lengthy task here
  });
}

现在说你想要一个worker类型的对象,它必须是单例吗?我猜不是,但不管它是否可以注入,但是单例必须处理并发请求,他们确实必须考虑使用。

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

后台工作者的问题,正如我所见,它不是线程池,除非它需要它,并且线程池已经通过 task.factory[=12= 供您使用]

毕竟如果我们真的想要一个队列而不是预先处理我们的工作,我们难道不希望它持久化,这样一旦控制器接受了请求,它就不会在服务器崩溃时丢失吗?那么你需要考虑另一种技术,而不是与网络服务器一起生死存亡的技术。

现在您可以让您的网络服务器在持久服务总线上发布一个主题,该主题由您的后台工作者订阅,只有在处理实际成功时才能完成……我想您的想法是这样的,但我担心尝试将其构建到网络服务器 RAM 存储中最终会让您头疼。

Channel<T>class专门针对producer/consumer这种场景设计的。对于您的用例,假设您有一条消息 class 要发送到您的队列,例如:

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 等待消息并相应地处理它。