簡體   English   中英

避免多線程應用程序中潛在的死鎖/內存泄漏

[英]Avoiding a potential deadlock / memory leak in a multithreaded application

簡短版本

如何處理生成一組線程,運行一些自定義(在實現時未指定)回調的非原子性? 下面描述了幾種可能的解決方案,似乎使用線程池是唯一的好解決方案。 有沒有標准的處理方法? 無需發布完整的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一樣多,但是如果numThreads更大,該怎么辦?我實際上將在我的代碼中重新實現OS的調度程序。

通常如何解決? 有沒有更好的辦法? 如果不是,潛在的死鎖(取決於回調function )是否比內存泄漏更好?

您如何解決此問題:

創建每個線程,使其在等待哨兵之前等待開始用戶的工作功能(您需要一個調用它的lambda),如果其中任何一個線程均無法啟動,請設置一個標志以指示現有線程應該立即完成而不是執行用戶的功能。 在錯誤情況下,請加入確實啟動的線程。 然后根據需要返回錯誤代碼或異常(異常更好)。

現在,您的函數是線程安全的,不會泄漏內存。

編輯:這是一些您想要執行的代碼,包括測試。 如果要強制模擬線程失敗,請使用定義為1 INTRODUCE_FAILURE重新編譯

#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

失敗時的示例輸出:

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
Parallel call failed because: the test deliberately failed a thread: Cannot assign requested address

最后請-不要在世界上釋放您的圖書館。 std :: thread庫非常全面,如果還不夠,我們還有OpenMP,TBB等。

如何讓確實創建的線程在退出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.
  }

}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM