我正在尝试建模一个系统,其中有多个线程产生数据,一个线程消耗数据。诀窍是我不希望专用线程使用数据,因为所有线程都存在于池中。相反,我希望其中一个生产者在有工作时清空队列,如果另一个生产者已经清空队列则产生。
基本思想是有一个工作队列,并锁定处理。每个生产者将其有效负载推送到队列,然后尝试进入锁定。该尝试是非阻塞的,并返回true(获取锁定)或false(锁定由其他人持有)。
如果获取了锁,则该线程处理队列中的所有数据,直到它为空(包括其他生成器在处理期间引入的任何新有效负载)。处理完所有工作后,线程将释放锁并退出。
以下是该算法的C ++代码:
void Process(ITask *task) {
// queue is a thread safe implementation of a regular queue
queue.push(task);
// crit_sec is some handle to a critical section like object
// try_scoped_lock uses RAII to attempt to acquire the lock in the constructor
// if the lock was acquired, it will release the lock in the
// destructor
try_scoped_lock lock(crit_sec);
// See if this thread won the lottery. Prize is doing all of the dishes
if (!lock.Acquired())
return;
// This thread got the lock, so it needs to do the work
ITask *currTask;
while (queue.try_pop(currTask)) {
... execute task ...
}
}
一般来说,这段代码工作正常,我从未亲眼目睹过我将在下面描述的行为,但这种实现让我感到不安。理所当然的是,在线程退出while循环和释放临界区之间引入了竞争条件。
整个算法依赖于这样的假设,即如果保持锁定,则线程正在为队列提供服务。
我基本上是在寻找两个问题的启示:
答案 0 :(得分:2)
是的,有竞争条件。
线程A添加任务,获取lock
,自行处理,然后从queue
请求任务。它被拒绝了。
此时线程B向queue
添加任务。然后它尝试获取锁定并失败,因为线程A具有锁定。线程B退出。
线程A退出,queue
非空,并且没有人处理任务。
这很难找到,因为那个窗口相对较窄。为了使它更容易找到,在while
循环后引入“睡眠10秒”。在调用代码中,插入任务,等待5秒,然后插入第二个任务。再过10秒钟,检查两个插入任务是否都已完成,还有一个任务要在queue
处理。
解决此问题的一种方法是将try_pop
更改为try_pop_or_unlock
,然后将lock
传递给它。 try_pop_or_unlock
然后以原子方式检查空queue
,如果是,则解锁lock
并返回false。
另一种方法是改进线程池。添加一个基于计数信号量的“消耗”任务启动器。
semaphore_bool bTaskActive;
counting_semaphore counter;
when (counter || !bTaskActive)
if (bTaskActive)
return
bTaskActive = true
--counter
launch_task( process_one_off_queue, when_done( [&]{ bTaskActive=false ) );
当计数信号量处于活动状态时,或者当完成的消耗任务调用时,如果没有消耗任务处于活动状态,它将启动一个消耗任务。
但这只是我的头脑。