【问题标题】:Multiple accessing to singleton object in .net在.net中多次访问单例对象
【发布时间】:2021-12-03 20:53:39
【问题描述】:

我想问一个我想到的问题。这个关于内存访问的问题,包含在 asp.net 核心中具有单例生命周期的对象。所以,假设这个结构中存在两个线程。其中之一是在 asp net 中使用的普通请求/响应线程。另一个线程在后台持续运行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;
            }
    }

此代码分区包含工作人员服务。它在队列任务中一一执行

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 属性是静态的),情况会有所不同吗?
  • 您对此结构有何改进建议?

【问题讨论】:

  • @quain 已经为您提供了解决方案,所以我只是在代码示例中指出了一些问题。 LinkedList 不是线程安全的,这意味着如果多个线程进入它的成员(例如调用 Add/Remove 方法),它们可能会弄乱它的内部结构。这是主要问题。但是在您的 GetNextEvent 方法中使用了第二个模式:获取最后一个值并将其从列表中单独删除也不是线程安全的!第二个线程也可能在第一个线程删除它之前获得最后一个值!
  • 我明白了。我将使用线程安全对象(带锁机制)。根据下面的答案,我必须使用并发数据结构或 Task.Factory.StartNew()

标签: c# asp.net .net multithreading memory-management


【解决方案1】:

有一个Channel<T> 类是专门为这样的生产者/消费者场景设计的。对于您的用例,假设您有一个消息类要发送到您的队列,例如:

class Message { public int AmountToIncrement { get; set; } }

您可以将Channel&lt;Message&gt;ChannelReader&lt;Message&gt;ChannelWriter&lt;Message&gt; 注册为单例:

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&lt;Message&gt; 注入您要发送消息的位置,并将ChannelReader&lt;Message&gt; 注入您的后台服务(例如,通过构造函数)。您的ExecuteAsync 可以使用ChannelReader&lt;T&gt;.ReadAsync 等待消息并进行相应处理。

【讨论】:

    【解决方案2】:

    我会建议不要在一个单独的队列线程上处理任务,而工作人员会在后台处理一些事情。

    请考虑使用异步任务,.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

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

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

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

    【讨论】:

    • Now say you wanted a worker type object, does it have to be a singleton? 在这种情况下,工作服务不是单例的。所以我使用 EventQueue 作为单例。无论如何,无论这种情况如何,我都在问。创建任务队列并一一执行或使用Task.Factory.StartNew()创建新任务。哪种方法是最好的方法?您可以评论安全、性能、健康吗?请
    • 我认为,创建队列是另一种方式更真实。第二种方式是创建任务实例太多会直接影响性能。
    • @mrpetrisah 不确定我是否完全理解,但是是的,所以当您添加任务时,与普通线程不同,您将方法排队到应用程序池中,并且在您创建它后立即开始,无论它是否坐在队列中。从某种意义上说,这是一个很好的方法,您可以控制线程池中可能有多少工作人员,但队列仅用于检查结果是否有效并可能重新排队。所以你的处理队列有点丢失。异步等待意味着如果您指定较少的工作人员,则请求将排队,但如果您不希望进行处理,请改用线程
    • @mrpetrisah 如果你的网络服务器有太多的请求,http 协议有一个积压的概念,所以它对请求进行排队,你可以用这种方式决定你的并发性,也可以定义你的网络服务器可以使用多少线程进程如果您的运行时间很长,可能会导致拥塞。因此,为了提高性能,我建议查看 ThreadPool.MinThreads 和 ThreadPool.MaxThreads。并且网络服务器通常还可以指定主要卸载的线程,例如如果 Kestrel: UseKestrel(opts => opts.ThreadCount = 4) 但有多个影响 DI 所以实验。
    【解决方案3】:

    对于此类任务,我建议查看ConcurrentQueue 收藏。

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

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

    至于改进,你总是可以尝试改造这个服务来利用事件,所以当有东西被添加到队列中时,后台服务将开始处理,直到什么都没有。

    【讨论】:

    • 感谢您的回答。问题很明显。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多