如何在迭代列表时最小化互斥锁,同时能够从另一个线程添加元素?

时间:2012-03-01 18:47:31

标签: c++ multithreading

在工作线程中,我迭代需要更新的项目列表。在主线程中,用户经常调用一个函数来向该列表添加元素。我正在使用互斥锁来防止列表在迭代时被修改。通常,伪代码如下所示:

// called by the client to add an element.
void AddElementToList(element)
{
    // lock mutex
    // add element to the list
    // unlock mutex
}

// this function is run by the worker thread
void WorkerThreadUpdate()
{
    // lock mutex
    // for each element in the list
    //     update the element
    //     mark the element for deletion if finished
    // for every finished element
    //     remove element from list
    // unlock mutex
}

更新元素时​​出现问题。元素的更新功能可能需要很长时间,在某些情况下最多可达一秒钟。发生这种情况时,用户下次调用AddElementToList会阻止其应用程序冻结,直到所有元素都已更新。经常调用AddElementToList函数,这是一个非常明显的问题。

所以我正在寻找一种更好的方法。我想在保护列表的同时更新元素,但要保持用户的主线程响应。我敢肯定这是多线程101,但是我无法找到正确的术语来找到我正在寻找的例子。

我在想一个使用两个列表的解决方案,一个“请求”列表和一个“工作”列表。

用户调用AddElementToList,将元素添加到请求列表中。在工作线程中,在迭代工作列表之后,它将遍历请求列表并将其元素添加到下一帧的工作列表中。它仅在实际修改列表时锁定。

// called by the client to add an element.
void AddElementToList(element)
{
    // lock mutex
    // add element to the request list
    // unlock mutex
}

// this function is run by the worker thread
void WorkerThreadUpdate()
{
    // for each element in the work list
    //     update the element
    //     mark the element for deletion if finished

    // for every finished element
    //     lock mutex
    //     remove element from work list
    //     unlock mutex

    // lock mutex
    // for every element in the request list
    //     add element to the work list
    // clear the request list
    // unlock mutex
}

我认为这应该有用,但我不完全确定。这是一种可接受的方式吗?有没有更好的方法来处理这个?

3 个答案:

答案 0 :(得分:1)

您排队添加的计划应该有效。它是否可接受取决于在线程执行下一次“更新”传递之前等待队列添加是否可接受。

甚至有必要在更新期间锁定列表吗?还有什么是访问此列表,何时?你能锁定列表,制作复制向量/引用列表,解锁列表然后逐个运行每个复制引用的更新吗?

答案 1 :(得分:0)

让我先问一下:

  • 实施中的实际列表是什么?一个动态阵列?链表?一些哈希表? - 如果它是一个列表,对一个元素的操作应仅影响下一个前一个元素,并且可能影响头部和尾部。
  • 如何在列表中添加元素?总是在最后?或者插入可以发生在中间某处?

我们假设:

  • 这是一个单向或双向链表,其头部和尾部都有其他变量。
  • 您只在最后添加元素。

如果是这种情况,我建议以某种方式与此类似:

void AddElementToList(element) {
    mutex_lock("push");
    list.push_back(element);
    mutex_release("push");
}

void WorkerThreadUpdate() {
    AddElementToList(specialElement);
    /*
      at this point we are guaranteed that other threads,
      which can only add to the list,
      will not affect us, because we have a "guard" between us and them
    */
    iterator del=NULL;
    iterator i=list.begin();
    for(; *i!=specialElement; ++i) {
         if (del) {
             del->remove();
             del=NULL;
         }
         i->update();
         if (some_condition)
             del=i;
    }
    //*i == specialElement now
    /*
      We are now operating on the guard, so we have to be careful.
      "specialElement" could still be the last element in the list.
    */
    mutex_lock("push");
    i->remove();
    mutex_release("push");
}

我不确定标准STL是否是线程安全的,如果你可以使用它们的列表实现。阅读他们的规格。如果不是 - 只需实现自己的列表容器。

答案 2 :(得分:0)

是否有其他线程会尝试访问该元素 你正在更新。如果没有,您可以释放列表中的锁定 启动更新,并在您想要继续时重新获取 迭代(或删除对象)。有点像:

void
WorkerThreadUpdate()
{
    // lock mutex
    ListType::iterator current = list.begin();
    while ( current != list.end() ) {
        ListType::value_type& obj = *current;
        //  unlock mutex
        //  do modifications...
        //  lock mutex
        if ( needsDeleting ) {
            current = list.erase( current );
        } else {
            ++ current;
        }
    }
    //  unlock mutex
}

重要的是你必须在实际上持有锁 访问迭代器(至少使用任何标准容器)。

当然,为了以防万一,你会想要使用某种范围的锁。 (虽然这可能没有必要;我认为所有的代码都在 锁定的区域应该是没有扔的。)我没试过,但我想你 如果你有C ++ 11,可以使用std::unique_lock

恕我直言,虽然这有效,但并不是特别优雅。我可能 将列表完全保留在工作者端,并使用消息队列 传递要插入的对象,工作线程从中读取 消息队列,以便获取要插入的对象。