工作线程队列的最轻同步原语

时间:2010-09-28 11:56:21

标签: c++ multithreading winapi synchronization

我即将实现一个带有工作项排队的工作线程,当我在思考这个问题时,我想知道我是否做得最好。

有问题的线程必须有一些线程本地数据(在构造时预先初始化)并将循环工作项,直到满足某些条件。

伪代码:

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。:我可能需要预先处理整个队列,并在出队前阶段删除过时的工作项。

9 个答案:

答案 0 :(得分:5)

有很多方法可以做到这一点。

一种选择是使用信号量进行等待。每次在队列上按下值时都会发信号通知信号量,因此只有队列中没有项目时,工作线程才会阻塞。这仍然需要在队列本身上单独同步。

第二个选项是使用手动重置事件,该事件在队列中有项目时设置,在队列为空时清除。同样,您需要在队列上进行单独的同步。

第三个选项是在线程上创建一个不可见的仅消息窗口,并使用特殊的WM_USERWM_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有一个很好的可移植实现,快速用户空间自旋睡眠锁和条件变量:

http://www.boost.org/doc/libs/1_44_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref

使用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; 
} 

兴趣点:

  1. ScopeLock是一个RAII类,可以使关键部分的使用更加安全。
  2. 阻止事件直到工作项(可能)准备就绪 - 然后锁定,而尝试将其取消。
  3. 不要使用全局“IsDone”标记,将特殊quitmessage WorkItem排入队列。

答案 5 :(得分:1)

你可以看看这里使用C ++ 0x原子操作的另一种方法

http://www.drdobbs.com/high-performance-computing/210604448

答案 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个工作线程。您的工作线程作业可能是独立的,因此您可以一次处理多个作业?如果是这样的话:

  • 在主线程中调用CreateIOCompletionPort以创建io完成端口对象。
  • 创建工作线程池。您需要创建的数量取决于您可能希望并行服务的作业数量。 CPU核心数量的一些是一个良好的开端。
  • 每次调用作业时,PostQueuedCompletionStatus()都会将指针作为lpOverlapped结构传递给作业结构。
  • 每个工作线程调用GetQueuedCompletionItem() - 从lpOverlapped指针检索工作项并在返回GetQueuedCompletionStatus之前完成工作。

这看起来很重,但是io完成端口是在内核模式下实现的,并且表示可以反序列化到与队列关联的任何工作线程的队列(即等待对GetQueuedCompletionStatus的调用)。 io完成端口知道处理项目的线程中有多少实际上在IO调用中使用了CPU与阻塞 - 并且将从池中释放更多工作线程以确保满足并发计数。

因此,它不是轻量级的,但它非常高效...例如,完成端口可以与管道和套接字句柄相关联,并且可以使这些句柄上的异步操作的结果出列。 io完成端口设计可扩展到在单个服务器上处理数十万个套接字连接 - 但在世界桌面方面,可以非常方便地在桌面PC中常见的2或4个核心上扩展作业处理。< / p>