避免多线程应用程序中潜在的死锁/内存泄漏

时间:2014-05-11 17:15:23

标签: c++ multithreading memory-leaks pthreads deadlock

简短版

如何处理产生一组线程的非原子性,运行一些自定义(在实现时未指定)回调?下面描述了几种可能的解决方案,似乎使用线程池是唯一的好解决方案。有没有一种标准的方法来处理它?无需发布完整的C ++解决方案,伪代码或简要描述就足够了。性能是这里的一个重要方面。

尽管看起来似乎微不足道,但我相信下面的代码片段出现在许多现有的应用程序中,许多(开始,可能还有一些高级)程序员可能会编写类似的结构,甚至没有意识到危险。 pthread / C ++ 11 std::thread / WinAPI以及许多其他低级多线程库的问题都是相同的。因此这是一个重要的问题。

长版

我正在设计一些多线程应用程序,我决定创建一个实用程序函数,其中生成了多个线程。这可能是一个非常常见的代码,它出现在我的许多应用程序中(除非他们使用的是OpenMP):

void ParallelCall(void (*function)(int, int), int numThreads)
{
    Thread *threads = new Thread[numThreads - 1];
    for(int i = 1; i < numThreads; ++ i) {
        if(threads[i - 1].start(&function, i, numThreads)) // this might fail
            abort(); // what now?
    }

    (*function)(0, numThreads);
    // use the calling thread as thread 0

    for(int i = 1; i < numThreads; ++ i)
        threads[i - 1].join();
    delete[] threads;
}

这是一个用于说明问题的伪代码。正在创建并生成一堆线程(Thread对象包装了一个pthread线程)。然后他们做了一些事情,最后他们加入了。

现在的问题是:如果由于某种原因,某些线程无法启动(可能是资源耗尽或每用户限制)会怎么样?我知道如何发现它发生了,但我不确定如何处理它。

我想我应该等待成功启动的线程完成然后抛出异常。但是,如果function中的代码包含某些同步(例如屏障),则很容易导致死锁,因为其余的预期线程永远不会产生。

或者,我可以立即抛出异常,忽略正在运行的线程,但随后我将分配包装器对象,导致内存泄漏(并且从不加入生成的线程)。

像杀死正在运行的线程这样的东西似乎不是一个好主意(我坦率地不太确定强行杀死多线程应用程序的线程的结果是什么 - 似乎记忆将会处于未定义状态,这种情况大多难以处理 - 如果回调function分配内存,本身可能会导致更多内存泄漏。

在让他们进入回调function之前插入等待所有线程的等待似乎无法忍受性能(尽管它可以轻松解决问题)。另一个选择是拥有一个带有相关FIFO的生成线程池,等待任务,但是线程数存在问题(我会生成与逻辑CPU一样多的线程,但是如果{{1} }更大?我本质上是在我的代码中重新实现OS&#39;调度程序。)

这是如何解决的?有没有更好的办法?如果没有,那么潜在的(取决于回调numThreads中的内容)死锁是否比内存泄漏更好?

2 个答案:

答案 0 :(得分:1)

如何解决此问题:

创建每个线程,使其在允许开始用户工作功能之前等待哨兵(你需要一个调用它的lambda) 如果任何线程无法启动,请设置一个标志以指示现有线程应立即完成而不是执行用户的功能。 在错误情况下,加入已启动的线程。然后根据需要退出错误代码或异常(异常更好)。

现在你的函数是线程安全的,不会泄漏内存。

编辑:这里有一些代码可以满足您的需求,包括测试。 如果要强制模拟线程失败,请使用定义为INTRODUCE_FAILURE的{​​{1}}重新编译

1

成功输出示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <memory>
#include <atomic>
#include <system_error>
#include <condition_variable>

#define INTRODUCE_FAILURE 0
// implementation

void ParallelCall(void (*function)(int, int), int numThreads)
{
    std::vector<std::thread> threads;
    threads.reserve(numThreads-1);

    std::atomic<bool> mustAbort ( false );
    std::atomic<bool> mayRun ( false );
    std::mutex conditionMutex;
    std::condition_variable runCondition;

    for(int i = 1; i < numThreads; ++ i) {
        try {
            #if INTRODUCE_FAILURE == 1
            if (i == 3) {
                throw std::system_error(99, std::generic_category(),  "the test deliberately failed a thread");
            }
            #endif
            threads.emplace_back( std::thread{ [i, numThreads, function
                                , &mustAbort
                                , &conditionMutex
                                , &runCondition
                                , &mayRun]()->int {
                std::unique_lock<std::mutex> myLock(conditionMutex);
                runCondition.wait(myLock, [&mayRun]()->bool { 
                    return mayRun;
                });
                myLock.unlock();
                // wait for permission
                if (!mustAbort) {
                    function(i, numThreads);
                }
                return 0;
            }} );
        }
        catch(std::exception& e) { // will be a std::system_error
            mustAbort = true;
            std::unique_lock<std::mutex> myLock(conditionMutex);
            mayRun = true;
            conditionMutex.unlock();
            runCondition.notify_all();
            for(auto& t : threads) {
                t.join();
            }
            throw;
        }
    }

    std::unique_lock<std::mutex> myLock(conditionMutex);
    mayRun = true;
    conditionMutex.unlock();
    runCondition.notify_all();

    function(0, numThreads);
    // use the calling thread as thread 0

    for(auto& t : threads) {
        t.join();
    }
}

// test

using namespace std;

void testFunc(int index, int extent) {
    static std::mutex outputMutex;

    unique_lock<mutex> myLock(outputMutex);
    cout << "Executing " << index << " of " << extent << endl;
    myLock.unlock();

    this_thread::sleep_for( chrono::milliseconds(2000) );

    myLock.lock();
    cout << "Finishing " << index << " of " << extent << endl;
    myLock.unlock();
}

int main()
{
    try {
        cout << "initiating parallel call" << endl;
        ParallelCall(testFunc, 10);
        cout << "parallel call complete" << endl;
    }
    catch(std::exception& e) {
        cout << "Parallel call failed because: " << e.what() << endl;
    }
   return 0;
}

失败时的输出示例:

Compiling the source code....
$g++ -std=c++11 main.cpp -o demo -lm -pthread -lgmpxx -lgmp -lreadline 2>&1

Executing the program....
$demo 
initiating parallel call
Executing 0 of 10
Executing 1 of 10
Executing 4 of 10
Executing 5 of 10
Executing 8 of 10
Executing 2 of 10
Executing 7 of 10
Executing 6 of 10
Executing 9 of 10
Executing 3 of 10
Finishing 1 of 10
Finishing 5 of 10
Finishing 2 of 10
Finishing 9 of 10
Finishing 8 of 10
Finishing 4 of 10
Finishing 3 of 10
Finishing 0 of 10
Finishing 6 of 10
Finishing 7 of 10
parallel call complete

最后请求 - 不要在世界上释放你的图书馆。 std :: thread库非常全面,如果还不够,我们有OpenMP,TBB等等。

答案 1 :(得分:1)

让它们创建的线程在退出threadproc之前完成丢失的工作会怎样?

List _StillBornWork;

void ParallelCall(void (*function)(int, int), int numThreads)
{
    Thread *threads = new Thread[numThreads - 1];
    for(int i = 1; i < numThreads; ++ i) {
        if(threads[i - 1].start(&function, i, numThreads)) {
            _StillBornWork.Push(i);
        }
    }

    (*function)(0, numThreads);
    // use the calling thread as thread 0

    for(int i = 1; i < numThreads; ++ i)
        threads[i - 1].join();
    delete[] threads;
}

ThreadProc(int i) {

  while(1) {
    do work

    // Here we see if there was any work that didn't get done because its thread
    // was stilborn.  In your case, the work is indicated by the integer i.
    // If we get work, loop again, else break.
    if (!_StillBornWork.Pop(&i))
      break;  // no more work that wasn't done.
  }

}