在接受采访时我被问到以下问题:
有一个固定大小任务队列。线程想要排队任务。如果队列已满,他们应该等待。线程顺序应该保留:如果thread1带有task1,并且在thread2带有task2之后,task1应该在task2之前进入队列。
其他线程想要将任务出列并执行它。如果队列为空,他们应该等待,并且他们的顺序也应该保留:如果t3在t4之前,t3应该在t4之前将任务排队。
如何实现(伪代码)?
答案 0 :(得分:0)
要同步对有限数量资源的访问,通常使用信号量。谷歌让它得到你自己的想法。
困难的部分是保持阻塞线程的顺序。
我发现这个项目在C#中包含FifoSemaphore
:http://dcutilities.codeplex.com
答案 1 :(得分:0)
如果生产者线程正在等待numEmptySpaces
信号量以访问队列,那么这种行为可能会发生,因为信号量等待队列在FIFO之外的任何其他事件中实现是不合理的,但是不能保证在大多数信号量实现上。
保证这种行为非常尴尬,因为很难定义“线程顺序”的要求:
如何定义首先到达哪个线程?
如果'第一个线程'获得了某种阻止其他线程继续进行的锁定,那么后续线程就会立即将我的群体置于“立即”状态,因此操作系统会提供任何锁定队列排序。
我唯一能想到的是强制每个生产者线程在尝试锁定/排队任何东西之前获取无锁时间戳或序列号。这可以通过“正常”原子增量指令来完成。当一个制作人随后通过获取'numEmptySpaces'单元'进入'并锁定队列时,它可以按序列号顺序将自己排入队列。
我不确定是否可以使用标准BlockingCollection
。您可以通过序列号'OrderBy'条目,但我不确定此操作是否锁定队列 - 它应该这样做,但是..此外,sequenceNo必须作为私有成员添加到BlockingCollection中后代和原子增量结果保持为每个任务的状态 - 您必须将其添加到Task
成员。
我很想用自己的BlockingQueue
类构建一个'普通'队列,耦合信号量和互斥量来实现这一点,一旦numEmptySpaces单元和队列将序列号顺序的新任务插入队列mutex已被收购。然后可以将原子增量结果组装成堆栈/自动变量。
作为面试问题,这可能是合理的,但我必须受到解雇的威胁,才能在生产代码中实际实施。想到可能必不可少的情况是非常困难的。额外开销和争用的缺点超过了我能想到的一切可疑的好处。
我对尝试在出列/执行结束时显式维护任何排序有类似的保留。尝试确保按顺序编号顺序达到出列任务中的某些“检查点”会很麻烦。这将需要来自任务的合作,这需要私人同步对象成员在其到达其检查点时发出信号。不要尝试:)
答案 2 :(得分:0)
System.Collections.Concurrent
,其中的类工作正常 - 我无法从它们中获得一些错误。基于Jeffrey Richter's book信息的解决方案:
基本代码(C#):
internal sealed class SynchronizedQueue<T> {
private readonly Object m_lock = new Object();
private readonly Queue<T> m_queue = new Queue<T>();
public void Enqueue(T item) {
Monitor.Enter(m_lock);
// After enqueuing an item, wake up any/all waiters
m_queue.Enqueue(item);
Monitor.PulseAll(m_lock);
Monitor.Exit(m_lock);
}
public T Dequeue() {
Monitor.Enter(m_lock);
// Loop while the queue is empty (the condition)
while (m_queue.Count == 0)
Monitor.Wait(m_lock);
// Dequeue an item from the queue and return it for processing
T item = m_queue.Dequeue();
Monitor.Exit(m_lock);
return item;
}
}
这个类是线程安全的,但仍然没有检查顺序 - 这里有很多方法可以实现。来自同一本书:
ConcurrentQueue
和ConcurrentStack
无锁;这些都在内部使用Interlocked
方法来操纵集合。
因此,您必须删除Monitor
类使用情况,并检查您的线程是否为下一个排队项目。
这可以通过在私有字段中维护当前加法器和当前队列长度的数量来完成。您应该将此字段设为volatile
您应该使用Interlocked.Exchange来获取当前加法器和Interlocked.Read以获得当前队列长度。
之后,您的线程有唯一的编号 - 当前长度+当前加法器。使用SpinWait类来旋转,而当前长度不会等于你的数字,在该排队项目之后,并保留Enqueue方法。
我强烈建议您学习本书有关多线程和锁定的章节 - 您将为生活中的这类问题做更多的准备。也尝试在这里搜索类似的问题。例如: