我即将实现一个带有工作项排队的工作线程,当我在思考这个问题时,我想知道我是否做得最好。
有问题的线程必须有一些线程本地数据(在构造时预先初始化)并将循环工作项,直到满足某些条件。
伪代码:
volatile bool run = true;
int WorkerThread(param)
{
localclassinstance c1 = new c1();
[other initialization]
while(true) {
[LOCK]
[unqueue work item]
[UNLOCK]
if([hasWorkItem]) {
[process data]
[PostMessage with pointer to data]
}
[Sleep]
if(!run)
break;
}
[uninitialize]
return 0;
}
我想我会通过关键部分进行锁定,因为队列将是std :: vector或std :: queue,但也许有更好的方法。
具有睡眠功能的部分看起来不太好,因为睡眠值很大会有很多额外的睡眠,或者当睡眠值很小时会有很多额外的锁定,这绝对没必要。
但我想不出我可以使用的WaitForSingleObject友好原语而不是临界区,因为可能有两个线程同时排队工作项。因此事件似乎是最佳候选者,如果已经设置了事件,则可以放弃第二个工作项,并且它不能保证互斥。
使用InterlockedExchange类型的函数甚至可以采用更好的方法来减少序列化。
P.S。:我可能需要预先处理整个队列,并在出队前阶段删除过时的工作项。
答案 0 :(得分:5)
有很多方法可以做到这一点。
一种选择是使用信号量进行等待。每次在队列上按下值时都会发信号通知信号量,因此只有队列中没有项目时,工作线程才会阻塞。这仍然需要在队列本身上单独同步。
第二个选项是使用手动重置事件,该事件在队列中有项目时设置,在队列为空时清除。同样,您需要在队列上进行单独的同步。
第三个选项是在线程上创建一个不可见的仅消息窗口,并使用特殊的WM_USER
或WM_APP
消息将项目发布到队列,通过以下方式将项目附加到消息指针。
另一种选择是使用condition variables。本机Windows条件变量仅在您使用Windows Vista或Windows 7时才有效,但条件变量也可用于带有Boost的Windows XP或C ++ 0x线程库的实现。我的博客上提供了使用提升条件变量的示例队列:http://www.justsoftwaresolutions.co.uk/threading/implementing-a-thread-safe-queue-using-condition-variables.html
答案 1 :(得分:3)
如果您的方案满足特定要求,则可以在不使用阻塞锁的情况下在线程之间共享资源。
您需要一个原子指针交换原语,例如Win32的InterlockedExchange。大多数处理器体系结构提供某种原子交换,并且通常比获取正式锁定要便宜得多。
您可以将工作项队列存储在一个指针变量中,该变量可供所有对其感兴趣的线程访问。 (全局var,或所有线程都可以访问的对象的字段)
此方案假定所涉及的线程始终有事可做,并且只是偶尔“浏览”共享资源。如果您想要线程阻塞等待输入的设计,请使用传统的阻塞事件对象。
在开始之前,创建队列或工作项列表对象并将其分配给共享指针变量。
现在,当生产者想要将某些内容推送到队列时,他们通过使用InterlockedExchange将null交换到共享指针变量来“获取”对队列对象的独占访问权。如果交换的结果返回null,则其他人当前正在修改队列对象。 Sleep(0)释放线程的其余时间片,然后循环重试交换,直到它返回非null。即使你最终循环几次,这也很多。比内核调用获取互斥对象要快许多倍。内核调用需要数百个时钟周期才能转换为内核模式。
成功获取指针后,对队列进行修改,然后将队列指针交换回共享指针。
当从队列中使用项目时,你会做同样的事情:将null交换到共享指针并循环,直到获得非null结果,对本地var中的对象进行操作,然后将其交换回共享指针var。
这种技术是原子交换和简短旋转循环的组合。它适用于所涉及的线程未被阻塞且冲突很少的情况。大多数情况下,交换将在第一次尝试时为您提供对共享对象的独占访问权限,并且只要队列对象由任何线程独占持有的时间长度非常短,那么没有线程应该循环多于一个在队列对象再次可用之前几次。
如果您希望场景中的线程之间存在大量争用,或者您希望线程花费大部分时间阻止等待工作到达的设计,那么正式的互斥同步对象可能会为您提供更好的服务。
答案 2 :(得分:2)
最快的锁定原语通常是自旋锁或自旋睡眠锁。 CRITICAL_SECTION就是这样一个(用户空间)spin-sleep-lock。 (好吧,除了当然没有使用锁定原语。但这意味着使用无锁数据结构,这些确实很难做对。)
至于避免睡眠:看一下条件变量。它们被设计为与“互斥体”一起使用,我认为它们比Windows的EVENT更容易正确使用。
Boost.Thread有一个很好的可移植实现,快速用户空间自旋睡眠锁和条件变量:
使用Boost.Thread的工作队列可能如下所示:
template <class T>
class Queue : private boost::noncopyable
{
public:
void Enqueue(T const& t)
{
unique_lock lock(m_mutex);
// wait until the queue is not full
while (m_backingStore.size() >= m_maxSize)
m_queueNotFullCondition.wait(lock); // releases the lock temporarily
m_backingStore.push_back(t);
m_queueNotEmptyCondition.notify_all(); // notify waiters that the queue is not empty
}
T DequeueOrBlock()
{
unique_lock lock(m_mutex);
// wait until the queue is not empty
while (m_backingStore.empty())
m_queueNotEmptyCondition.wait(lock); // releases the lock temporarily
T t = m_backingStore.front();
m_backingStore.pop_front();
m_queueNotFullCondition.notify_all(); // notify waiters that the queue is not full
return t;
}
private:
typedef boost::recursive_mutex mutex;
typedef boost::unique_lock<boost::recursive_mutex> unique_lock;
size_t const m_maxSize;
mutex mutable m_mutex;
boost::condition_variable_any m_queueNotEmptyCondition;
boost::condition_variable_any m_queueNotFullCondition;
std::deque<T> m_backingStore;
};
答案 3 :(得分:1)
有多种方法可以做到这一点
对于一个你可以创建一个名为'run'的事件,然后使用它来检测线程应该何时终止,然后主线程发出信号。而不是睡眠,然后你会使用WaitForSingleObject超时,这样你就可以直接退出而不是等待睡眠时间。
另一种方法是接受循环中的消息,然后发明一个用户定义的消息,并将其发布到线程
编辑:根据情况,有一个监视此线程的另一个线程检查它是否已经死也可能是明智的,这可以通过上面提到的消息队列完成,以便在x ms内回复某个消息意味着线程没有锁定。
答案 4 :(得分:1)
我重组了一下:
WorkItem GetWorkItem()
{
while(true)
{
WaitForSingleObject(queue.Ready);
{
ScopeLock lock(queue.Lock);
if(!queue.IsEmpty())
{
return queue.GetItem();
}
}
}
}
int WorkerThread(param)
{
bool done = false;
do
{
WorkItem work = GetWorkItem();
if( work.IsQuitMessage() )
{
done = true;
}
else
{
work.Process();
}
} while(!done);
return 0;
}
兴趣点:
ScopeLock
是一个RAII类,可以使关键部分的使用更加安全。WorkItem
排入队列。答案 5 :(得分:1)
你可以看看这里使用C ++ 0x原子操作的另一种方法
答案 6 :(得分:0)
使用信号量而不是事件。
答案 7 :(得分:0)
保持信号和同步分开。这些方面的东西......
// in main thread
HANDLE events[2];
events[0] = CreateEvent(...); // for shutdown
events[1] = CreateEvent(...); // for work to do
// start thread and pass the events
// in worker thread
DWORD ret;
while (true)
{
ret = WaitForMultipleObjects(2, events, FALSE, <timeout val or INFINITE>);
if shutdown
return
else if do-work
enter crit sec
unqueue work
leave crit sec
etc.
else if timeout
do something else that has to be done
}
答案 8 :(得分:0)
鉴于这个问题被标记为窗口,我会回答:
不要创建1个工作线程。您的工作线程作业可能是独立的,因此您可以一次处理多个作业?如果是这样的话:
这看起来很重,但是io完成端口是在内核模式下实现的,并且表示可以反序列化到与队列关联的任何工作线程的队列(即等待对GetQueuedCompletionStatus的调用)。 io完成端口知道处理项目的线程中有多少实际上在IO调用中使用了CPU与阻塞 - 并且将从池中释放更多工作线程以确保满足并发计数。
因此,它不是轻量级的,但它非常高效...例如,完成端口可以与管道和套接字句柄相关联,并且可以使这些句柄上的异步操作的结果出列。 io完成端口设计可扩展到在单个服务器上处理数十万个套接字连接 - 但在世界桌面方面,可以非常方便地在桌面PC中常见的2或4个核心上扩展作业处理。< / p>