【问题标题】:C# Threading without locking Producer or Consumer不锁定生产者或消费者的 C# 线程
【发布时间】:2013-04-01 09:28:50
【问题描述】:

TLDR;主要问题的版本:

  1. 在使用线程时,使用 1 个线程读取列表的内容是否安全,而另一个线程写入它,只要您不删除列表内容(重新组织顺序)并且仅在新对象之后读取新对象已完全添加

  2. 当一个线程正在将一个 Int 从“旧值”更新为“新值”时,如果另一个线程读取此 Int,则返回的值既不是“旧值”也不是“新价值”

  3. 线程是否可以在繁忙的情况下“跳过”关键区域,而不是仅仅进入休眠状态并等待区域释放?

我有 2 段代码在单独的线程中运行,我想让其中一个充当另一个的生产者。我不希望任何一个线程在等待访问时“休眠”,而是如果另一个线程正在访问它,则在其内部代码中向前跳过。

我最初的计划是通过这种方法共享数据(一旦计数器足够高,就切换到辅助列表以避免溢出)。


流程的伪代码,正如我最初的意图。

Producer
{
Int counterProducer;
bufferedObject newlyProducedObject;
List <buffered_Object> objectsProducer;
    while(true) 
    {
        <Do stuff until a new product is created and added to newlyProducedObject>;
        objectsProducer.add(newlyProducedObject_Object);
        counterProducer++
    }
}


Consumer
{
Int counterConsumer;
Producer objectProducer; (contains reference to Producer class)
List <buffered_Object> personalQueue
    while(true)
        <Do useful work, such as working on personal queue, and polish nails if no personal queue>
        //get all outstanding requests and move to personal queue
        while (counterConsumer < objectProducer.GetcounterProducer())
        {
            personalQueue.add(objectProducer.GetItem(counterconsumer+1));
            counterConsumer++;
        }
}

看到这里,乍一看一切都很好,我知道我不会从队列中检索半构建的产品,因此即使线程切换,列表的状态无论在哪里都应该不是问题在 Producer 添加新对象时发生。这个假设是正确的,还是这里有问题? (我的猜测是因为消费者要求列表中的特定位置,并且新对象被添加到末尾,并且对象永远不会被删除,这不会是一个问题)

但是引起我注意的是,如果“counterProducer”是“counterProducer++”,是否会出现类似的问题?这会导致临时值是“null”还是某个未知值?这会是一个潜在的问题吗?

我的目标是在等待互斥锁时不让两个线程锁定,而是继续它们的循环,这就是为什么我首先制作上述内容,因为没有锁定。

如果使用列表会出现问题,我的解决方法是做一个链表实现,并在两个类之间共享,仍然使用计数器查看是否添加了新工作并保留最后一个位置,而personalQueue 将新内容移动到个人队列。所以生产者添加新链接,消费者读取它们,并删除以前的链接。 (列表上没有计数器,只是外部计数器知道添加和删除了多少)


避免 counterConsumer++ 风险的替代伪代码(需要帮助)。

Producer
{
Int publicCounterProducer;
Int privateCounterProducer;
bufferedObject newlyProducedObject;
List <buffered_Object> objectsProducer;
    while(true) 
    {
        <Do stuff until a new product is created and added to newlyProducedObject>;
        objectsProducer.add(newlyProducedObject_Object);
        privateCounterProducer++
        <Need Help: Some code that updates the publicCounterProducer to the privateCounterProducer if that variable is not 

locked, else skips ahead, and the counter will get updated at next pass, at some point the consumer must be done reading stuff, and 

new stuff is prepared already>      
    }
}


Consumer
{
Int counterConsumer;
Producer objectProducer; (contains reference to Producer class)
List <buffered_Object> personalQueue
    while(true)
        <Do useful work, such as working on personal queue, and polish nails if no personal queue>
        //get all outstanding requests and move to personal queue
        <Need Help: tries to read the publicProducerCounter and set readProducerCounter to this, else skips this code>
        while (counterConsumer < readProducerCounter)
        {
            personalQueue.add(objectProducer.GetItem(counterconsumer+1));
            counterConsumer++;
        }
}

所以代码的第二部分的目标是让两个类不等待另一个类,以防另一个类处于更新的“关键区域”公共计数器生产者。如果我正确阅读了锁定功能,线程将进入睡眠状态等待释放,这不是我想要的。可能最终不得不使用它,在这种情况下,第一个伪代码会做到这一点,并在获取值时设置一个“锁定”。

希望你能帮我解决我的许多问题。

【问题讨论】:

    标签: c# multithreading


    【解决方案1】:
    1. 不,这不安全。在List 添加对象之后,但在List 更新内部数据结构之前,.Add 内可能会发生上下文切换。

    2. 如果是int32,或者是int64,并且您在x64 进程中运行,则没有风险。但如果您有任何疑问,请使用Interlocked 类。

    3. 是的,您可以使用Semaphore,当需要进入临界区时,使用需要超时的WaitOne 过载。通过超时0。如果WaitOne返回true,那么你成功获得了锁,可以进入。如果它返回false,那么你没有获得锁,不应该进入。

    你真的应该看看System.Collections.Concurrent 命名空间。特别是看BlockingCollection。它有一堆 Try* 运算符,您可以使用它们从集合中添加/删除项目而不会阻塞。

    【讨论】:

    • 感谢您的意见。我将使用没有我提到的内部计数器的“链表”实现作为存储数据的一种方式。将尝试围绕 3 的答案展开我的头脑。非常感谢您向我提供有关在何处查找更多详细信息的信息
    • 如果你不建立内存屏障,你可能会导致每个线程永远看不到另一个线程更新的计数。说真的,使用已经正确的并发集合。或者至少,如果您觉得自己必须实现自己的数据结构,请使用 Interlocked 类来读取/写入计数变量。
    • 我肯定会仔细阅读您列出的信息,否则会浪费您回答的时间,以及我写问题的时间:)
    • 也就是说我正在重新考虑如何正确完成这项工作,如果我使用信号量正确阅读,我实际上可以让消费者和生产者拥有准备好的任务和要执行的任务的私人列表。在临界区创建第三个列表,并通过标准列表函数使用简单的列表命令将私有列表从生产者转储到其中,并以类似的方式在消费者将任务移动到其“线程”并清除时检索信息“缓冲区”。
    • 是的,如果您将时间保持在关键部分内,那么您在等待访问该区域时可能不必担心“阻塞”。如果您在关键部分所做的只是从共享列表中添加/删除项目,那么您将永远不会长时间持有锁。那时,我什至不会使用信号量。只需使用通过 c#lock statement 实现的监视器锁。并发集合实现的正是这种模式。他们的实现是正确的,并且优化得很好。
    【解决方案2】:

    在使用线程时,使用 1 个线程读取列表的内容,而另一个线程写入它是否安全,只要您不删除列表内容(重新组织顺序)并且仅在新对象完全添加后读取新对象

    不,不是。将项目添加到列表的副作用可能是重新分配其底层数组。 List&lt;T&gt; 的当前实现在将旧数据复制到内部引用之前会更新内部引用,因此多个线程可能会观察到正确大小但不包含数据的列表。

    当一个线程正在将一个 Int 从“旧值”更新为“新值”时,如果另一个线程读取该 Int 时返回的值既不是“旧值”也不是“新值”,是否存在风险

    不,int 更新是原子的。但是如果两个线程同时增加counterProducer,就会出错。您应该使用Interlocked.Increment() 来增加它。

    线程是否可以在繁忙时“跳过”关键区域,而不是仅仅进入休眠状态并等待区域释放?

    不,但您可以使用(例如)WaitHandle.WaitOne(int) 来查看等待是否成功,并相应地进行分支。 WaitHandle由几个同步类实现,如ManualResetEvent

    顺便说一句,您不使用内置的生产者/消费者类(例如BlockingCollection&lt;T&gt;)是否有原因? BlockingCollection 易于使用(在您阅读文档之后!),我建议您改用它。

    【讨论】:

    • 感谢 Matthew 的输入,只能选择 1 个正确答案。 :(。正如在另一个答案中所说,这提供了非常好的信息,并为我提供了要搜索的“关键字”的详细信息。
    猜你喜欢
    • 2018-05-22
    • 2018-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-06
    • 1970-01-01
    • 1970-01-01
    • 2017-02-01
    相关资源
    最近更新 更多