问题描述
我正在编写一个简单的生产者-消费者程序,它应该基于固定大小的队列。
我正在使用列表来实现它。
我的两个问题是:
- 在下面的一段代码中,我正在检查列表是否为空,以了解消费者是否应该并且有什么东西可以消费。
我的指示是: 使用链表+只有一个互斥量,消费者应该做一段时间(不为空..)
/* start function: handler function of consumer: */
/* while program is running */
/* while linked list is not empty: */
/* lock mutex - critical section entrance */
/* pop the last element and save its data in a variable */
/* unlock mutex - critical section exit */
/* print the popped data */
/* end of while is not empty */
/* end of while */
/* end function */
有人告诉我:
- “你不应该花时间在临界区。你不需要在那里打印东西。我们在临界区的目标是 尽快退出。做与数据相关的一切 结构,在这种情况下,关键部分内的列表并获得 离开它,然后才进行消费,在这种情况下,就是 打印出被删除的数据。”
他告诉我,我必须检查关键部分内的列表是否为空,因为它是与数据结构直接相关的操作。
我的问题是:为什么?为什么我不能检查列表是否为空,然后才进入临界区?
/* start function: handler function of consumer: */
/* while program is running */
/* lock mutex - critical section entrance */
/* while linked list is not empty: */
/* set global flag is_empty as 1 */
/* break */
/* end of while is not empty */
/* if is_empty: */
/* pop from the back of the list and save the popped data */
/* end if */
/* unlock mutex - critical section exit */
/* consume - print popped data */
/* set is_empty as 0 */
/* end of while */
/* end function */
我的第二个问题是:
如果我们使用信号量,我们应该在临界区内部还是外部更改它们的值?为什么?
例如,在下面的一段代码中:
/* start function: handler function of a producer: */
/* while program is running: */
/* decrement the value of the semaphore num_of_empty_bins */
/* lock mutex_producer - critical section entrance */
/* enqueue data */
/* increment the value of the semaphore num_of_full_bins */
/* unlock mutex_producer - critical section exit */
/* end of while */
/* end function */
如您所见,临界区中有递增和递减函数。 本来是在section外面写的,在互斥锁之前和之后。 但是我的朋友告诉我,他认为应该在锁定/解锁之前和之后。
我们在任何地方都找不到答案,所以我们不知道是否有区别或这样做的理由。
而且这些递增/递减操作是否必须是原子的,即使它在临界区中?或者只有在部分之外它们才应该是原子的?
谢谢。
解决方法
看看:Threads and data races (cppreference.com)
当一个表达式的求值写入一个内存位置而另一个求值读取或修改同一个内存位置时,表达式被称为冲突。有两个相互冲突的评估的程序会发生数据竞争,除非其中之一
- 两个相互矛盾的评估都是 atomic operations
- 一个相互冲突的评估发生在另一个之前(见 memory_order)
如果发生数据竞争,程序的行为是不确定的。
您的第一个问题:“为什么?为什么我不能检查列表是否为空,然后才进入临界区?”
另一个线程可以在您进入临界区之前修改列表的状态。您认为该列表是空的,但实际上并非如此(反之亦然)。因此,每个线程都必须在读取或写入共享变量之前进入临界区。
第二个问题:
这里也一样。只要不使用原子数据类型,就必须进入临界区。
,我的问题是:为什么?为什么我不能检查列表是否为空,然后才进入临界区?
因为对共享变量(例如您的列表及其成员)的非原子访问必须仅受足以防止多个线程同时访问的同步措施的约束。这是比限制对关键部分的访问更严格的约束。例如,必须防止一个线程从一个临界区访问共享对象,而另一个线程从不同的临界区访问同一个共享对象。
如果未满足此同步要求,则程序行为未定义。在实践中,这种不确定性可以表现为线程看不到彼此的更新、数据损坏和其他非常真实和有影响的问题。这些往往也很难调试,因此从一开始就仔细注意正确的同步是值得的。
如果我们使用信号量,我们应该在临界区内部还是外部更改它们的值?为什么?
信号量是同步对象,可以并且经常充当互斥锁。因此,它们与您描述的特定任务无关,在该任务中,除了互斥锁之外,我还认为使用信号量至少违反了“只有一个互斥锁”约束的精神。如果您不想要它的同步属性,那么普通整数可以用于相同的目的。
话虽如此,如果您确实在程序中添加了信号量,那么主要关注的是使用互斥量和信号量的多个线程是否构成死锁风险。您应该能够通过遵循以下一般原则来确保它们不会发生,即每个打算同时持有一对给定锁的线程必须以相同的相对顺序获取它们。锁定互斥锁和递减信号量一次都算作获取锁。只要您保持一致,您应该可以选择任一顺序。特别是,操纵信号量本身并不需要保护不同的同步对象。
而且这些递增/递减操作是否必须是原子的,即使它在临界区中?或者只有在部分之外它们才应该是原子的?
如果共享变量的增量和减量操作(在任何地方)正确同步,则它们不需要是原子的,如上所述。在某些情况下,可以在没有其他同步的情况下替换原子修改,但这需要结合更大的上下文来考虑。尽管任何临界区之外的原子修改本身可能没问题,但它可能会产生或允许不需要的程序行为。