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

How to minimize mutex locks when iterating a list while being able to add elements from another thread?

本文关键字:添加 线程 另一个 元素 迭代 列表 最小化      更新时间:2023-10-16

在工作线程中,我迭代需要更新的项目列表。在主线程中,用户经常调用一个函数来向该列表中添加元素。我使用互斥锁来防止列表在迭代时被修改。一般来说,伪代码看起来像这样:

// 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
}

我认为这应该有效,但我不完全确定。这是可以接受的方式吗?有更好的方法来处理这个问题吗?

您对添加内容进行排队的计划应该会奏效。它是否可接受取决于等待队列添加直到线程进行下一次"更新"过程是否可接受

在更新过程中是否有必要锁定列表?还有什么正在访问此列表,何时访问?你们能锁定列表,制作一个引用的复制向量/列表,解锁列表,然后逐个对每个复制引用运行更新吗?

让我先问一下:

  • 您的实现中的列表实际上是什么?一个动态数组?链接列表?一些哈希表?--如果它是一个列表,对一个元素的操作应该只影响下一个、前一个元素,可能还会影响头部和尾部
  • 如何将元素添加到列表中?总是在最后?或者插入可以发生在中间的某个地方

让我们假设:

  • 它是一个单向或双向的链表,有指向其头部和尾部的附加变量
  • 只能在末尾添加元素

如果是这样的话,我建议做类似于这样的事情:

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是否是线程安全的,以及您是否可以使用它们的列表实现。阅读他们的规格。如果没有,只需实现您自己的列表容器。

是否有其他线程尝试访问该元素你正在更新。如果没有,您可以在启动更新,并在想要继续时重新获取迭代(或删除对象)。大致如下:

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,则可以使用CCD_ 1。

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