在 .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>();
问题:
- 这个结构好用吗?我认为,如果存在对队列实例的多次访问,它不会很好地工作。或者更确切地说,工作者服务总是在访问队列实例。因此将没有时间访问其他线程。那么这种做法对吗?
- 如果不使用单例生命周期并且 EventQueue 是静态的(至少 LinkedList 属性 是静态的),情况会有所不同吗?
- 你对这个结构有什么改进的建议吗?
对于这样的任务,我建议看一下 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
等待消息并相应地处理它。
我想问一个我想到的问题。这个关于内存访问的问题,包含 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>();
问题:
- 这个结构好用吗?我认为,如果存在对队列实例的多次访问,它不会很好地工作。或者更确切地说,工作者服务总是在访问队列实例。因此将没有时间访问其他线程。那么这种做法对吗?
- 如果不使用单例生命周期并且 EventQueue 是静态的(至少 LinkedList 属性 是静态的),情况会有所不同吗?
- 你对这个结构有什么改进的建议吗?
对于这样的任务,我建议看一下 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
等待消息并相应地处理它。