std :: map insert / erase的并发问题

时间:2012-04-26 15:21:21

标签: c++ concurrency pthreads

我正在编写一个线程应用程序来处理资源列表,可能会也可能不会将结果项放在每个资源的容器(std :: map)中。 资源处理在多个线程中进行。

将遍历结果容器并且每个项目由一个单独的线程执行,该线程接受一个项目并更新MySQL数据库(使用mysqlcppconn API),然后从容器中删除该项目并继续。

为了简单起见,这里是逻辑概述:

queueWorker() - thread
    getResourcesList() - seeds the global queue

databaseWorker() - thread
    commitProcessedResources() - commits results to a database every n seconds

processResources() - thread x <# of processor cores>
    processResource()
    queueResultItem()

伪实现以显示我正在做什么。

/* not the actual stucts, but just for simplicities sake */
struct queue_item_t {
    int id;
    string hash;
    string text;
};

struct result_item_t {
    string hash; // hexadecimal sha1 digest
    int state;
}

std::map< string, queue_item_t > queue;
std::map< string, result_item_t > results;

bool processResource (queue_item_t *item)
{
    result_item_t result;

    if (some_stuff_that_doesnt_apply_to_all_resources)
    {
        result.hash = item->hash;
        result.state = 1;

        /* PROBLEM IS HERE */
        queueResultItem(result);
    }
}

void commitProcessedResources ()
{
    pthread_mutex_lock(&resultQueueMutex);

    // this can take a while since there

    for (std::map< string, result_item_t >::iterator it = results.begin; it != results.end();)
    {
        // do mysql stuff that takes a while

        results.erase(it++);
    }

    pthread_mutex_unlock(&resultQueueMutex);
}

void queueResultItem (result_item_t result)
{
    pthread_mutex_lock(&resultQueueMutex);

    results.insert(make_pair(result.hash, result));

    pthread_mutex_unlock(&resultQueueMutex);
}

正如processResource()所示,问题在于,当commitProcessedResources()正在运行且resultQueueMutex被锁定时,我们将在此等待queueResultItem()返回,因为它将尝试锁定相同的互斥锁并且因此会等到它完成,这可能需要一段时间。

由于显然有一定数量的线程在运行,所以只要所有线程都在等待queueResultItem()完成,在发布互斥锁并且可用于queueResultItem()之前,不会再做任何工作了。 / p>

所以,我的问题是我最好如何实现这个目标?是否存在可以同时插入和删除的特定类型的标准容器,或者是否存在我不知道的东西?

严格必须确保每个队列项都有自己的唯一键,就像这里使用std :: map一样,但我更喜欢它,因为有几个资源可以产生相同的结果我宁愿只向数据库发送一个唯一的结果,即使 使用INSERT IGNORE忽略任何重复项。

我对C ++很陌生,所以我不知道在谷歌上要找什么。 :(

3 个答案:

答案 0 :(得分:7)

commitProcessedResources ()处理期间,您不必一直保持队列的锁定。您可以使用空的队列交换队列:

void commitProcessedResources ()
{
    std::map< string, result_item_t > queue2;
    pthread_mutex_lock(&resultQueueMutex);
    // XXX Do a quick swap.
    queue2.swap (results);
    pthread_mutex_unlock(&resultQueueMutex);

    // this can take a while since there

    for (std::map< string, result_item_t >::iterator it = queue2.begin();
        it != queue2.end();)
    {
        // do mysql stuff that takes a while

        // XXX You do not need this.
        //results.erase(it++);
    }   
}

答案 1 :(得分:0)

您需要使用同步方法(即互斥锁)才能使其正常工作。但是,并行编程的目标是最小化关键部分(即在您持有锁时执行的代码量)。

也就是说,如果您的MySQL查询可以在没有同步的情况下并行运行(即多个调用不会相互冲突),请将它们从临界区中取出。这将极大地减少开销。例如,如下的简单重构可以做到这一点

void commitProcessedResources ()
{
    // MOVING THIS LOCK

    // this can take a while since there
    pthread_mutex_lock(&resultQueueMutex);
    std::map<string, result_item_t>::iterator end = results.end();
    std::map<string, result_item_t>::iterator begin = results.begin();
    pthread_mutex_unlock(&resultQueueMutex);

    for (std::map< string, result_item_t >::iterator it = begin; it != end;)
    {
        // do mysql stuff that takes a while

        pthread_mutex_lock(&resultQueueMutex); // Is this the only place we need it?
        // This is a MUCH smaller critical section
        results.erase(it++);
        pthread_mutex_unlock(&resultQueueMutex); // Unlock or everything will block until end of loop
    }

    // MOVED UNLOCK
}

这将为您提供跨多个线程的数据并发“实时”访问。也就是说,每次写入完成后,地图都会更新,并可以使用当前信息在其他地方读取。

答案 2 :(得分:0)

通过C ++ 03,该标准根本没有定义任何关于线程或线程安全的内容(因为你使用的是pthread s,我猜这几乎就是你所使用的)。

因此,您可以在共享地图上进行锁定,以确保在任何给定时间只有一个线程尝试访问地图。如果没有它,你可能会破坏其内部数据结构,因此地图根本不再有效。

或者(我通常更喜欢这个)你可以让你的多线程将他们的数据放入一个线程安全的队列,并有一个线程从该队列中获取数据并将其放入映射。由于它是单线程的,因此您无需在使用时锁定地图。

在将地图刷新到磁盘时,有一些合理的可能性来处理延迟。可能最简单的方法是从队列中读取相同的线程,插入映射,并定期将映射刷新到磁盘。在这种情况下,传入的数据只是在将映射刷新到磁盘时位于队列中。这使得对地图的访问变得简单 - 因为只有一个线程直接触摸它,它可以使用地图而不需要任何锁定。

另一个是有两张地图。在任何给定时间,刷新到磁盘的线程都会获得一个映射,从队列中检索并插入到映射中的线程将获得另一个映射。当冲洗线程需要做它的事情时,它只是交换两者的角色。就个人而言,我认为我更喜欢第一种 - 消除地图周围的所有锁定都具有很大的吸引力,至少对我而言。

还有另一种变体可以保持简单,就是让队列 - >地图线程创建地图,填充它,当它足够时(即,在适当的时间长度之后)将其填入另一个队列,然后从头开始重复(即创建新映射等)。刷新线程从其传入队列中检索映射,将其刷新到磁盘并销毁它。虽然这会增加一些创建和销毁地图的开销,但是你并没有经常这么做太多关心。您仍然可以随时保持对任何映射的单线程访问,并且仍然将所有数据库访问与其他所有映射分开。