2009-10-06 239 views
3

這裏是我的線程類的骨架:的一種方式摧毀「線程」類

class MyThread { 
public: 
    virutal ~MyThread(); 

    // will start thread with svc() as thread entry point 
    void start() = 0;   

    // derive class will specialize what the thread should do 
    virtual void svc() = 0;     
}; 

某處在代碼中,我創建的MyThread一個實例,後來我要摧毀它。 在這種情況下,調用MyThread~MyThread()MyThread:svc()仍在運行並使用該對象的數據成員。所以我需要一個禮貌的方式告知MyThread:svc()停止旋轉,然後繼續析構。

破壞線程對象的可接受方式是什麼?

注:我正在尋找平臺不可知的解決方案。

UPD:很明顯,問題的根源是,有℃之間沒有關係++對象表示線程和OS線程。所以,問題是:在對象destuction的情況下,有沒有辦法讓線程對象像普通的C++對象能夠接受的方式還是應該把它作爲一個不尋常的(例如,我們應該調用join()方法destoying之前進行治療

+1

你或許應該讓DSTR虛擬,只要你想這個類是派生自。 – chollida 2009-10-06 20:43:28

+0

你是對的,更正:) – dimba 2009-10-06 21:08:54

+0

你對我的答案的評論是不準確的。我已更新以使其更清楚。請重新閱讀。 – 2009-10-06 23:38:47

回答

7

考慮您的附加要求張貼到跳棋回覆意見(這是做到這一點的 最直接的方式):

我同意加盟析構函數是因各種原因存在問題。但是,線程對象的生命週期與OS線程對象的生命週期無關。


首先,你需要分離線程線程對象本身使用的數據。它們是不同的實體,具有不同的生命期要求。

一種方法是對數據進行重新計算,並讓任何想要訪問的線程都對數據有強烈的參考。這樣,沒有線程會突然陷入虛空,但只要沒有人接觸到它,數據就會被銷燬。


其次,有關線程對象的時候,螺紋連接被破壞:
我不知道這是否是一個好主意。線程對象通常是查詢線程狀態的一種方式 - 但是一旦線程完成就會死掉的線程對象,沒有人能告訴你線程已完成。

一般來說,我會完全分離線程對象的生命週期與操作系統線程的生命週期:銷燬線程對象不應該影響線程本身。我看到兩種基本方法:

  1. 線程處理對象 - 線程創建者返回的再次計算的引用可以在不影響操作系統線程的情況下儘快發佈。它會暴露諸如Join,IsFinished等方法,並且可以授予訪問線程共享數據的權限。

(如果線程對象持有相關的執行狀態,threafFunc本身可以保持它的一個引用,從而保證了實例的線程結束之前將不會被釋放)

  • Thin Wrapper - 您只需在OS線程句柄周圍創建臨時文件。你不能容易地爲線程保留額外的狀態,但它可能足以讓它工作:在任何地方,你都可以將一個OS線程句柄變成一個線程對象。大部分的交流 - 例如告訴線程終止 - 將通過共享數據。

  • 爲了您的代碼示例,這意味着:獨立於svc()

    start()你會大致與此API工作(XxxxPtr可能是如提高:: shared_ptr的):

    class Thread 
    { 
        public: 
        bool IsFinished(); 
        void Join(); 
        bool TryJoin(long timeout); 
    
        WorkerPtr GetWorker(); 
    
        static ThreadPtr Start(WorkerPtr worker); // creates the thread 
    }; 
    
    
    class Worker 
    { 
    private: 
        virtual void Svc() = 0; 
    
        friend class Thread; // so thread can run Svc() 
    } 
    

    Worker可以包含一個ThreadPtr本身,從而保證在執行Svc()期間線程對象存在。如果允許多個線程在相同的數據上工作,則這必須是一個線程列表。否則,Thread::Start將不得不拒絕已經與線程關聯的工人。


    動機:做什麼用的阻斷流氓線程?
    假設線程由於某種原因未能及時終止,即使您告訴它。你只有三個選擇:

    • 死鎖,你的應用程序掛起。如果你加入析構函數,通常會發生這種情況。
    • 暴力終止線程。這可能是一個暴力終止的應用程序。
    • 讓線程運行完成它自己的數據 - 你可以通知用戶,誰可以安全地保存&退出。或者你只是讓流氓線程在它自己的數據副本上跳舞(不再由主線程引用)直到它完成。
    +0

    我不確定我完全理解你的答案。是可能的,在你的例子線程將被摧毀,而關聯的工人不是? 很多情況下,數據不會在幾個線程之間共享,對於這些情況,您的解決方案可能過於冗長。 – dimba 2009-10-06 20:32:31

    +1

    +1,用於將線程對象與其上運行的函數使用的數據分開的建議 – 2009-10-06 21:19:17

    +0

    正確。我添加了一些「動機」段落。請不要混淆我的答覆的冗長度和必要代碼的冗長度。 在請求操作的線程和執行它的線程之間總是有一些*數據共享。我的關鍵是,一個*線程*與它所操作的* data *是一個不同的實體,並且線程啓動器和線程本身都不會單方面決定銷燬數據。 – peterchen 2009-10-06 21:22:15

    1

    你可以havee財產以後像這樣在您的SVC方法

    while (alive){ //loops} 
    //free resources after while. 
    

    在你的析構函數,你可以在活着的成員設置爲false。或者,你可以有一個pleaseDie()方法,該活的成員設置爲false,並且可以從外部請求的線程實例停止處理被調用。

    void 
    Thread::pleaseDie() 
    { 
    
    this->alive = false; 
    } 
    
    +1

    僅在d-tor中設置標誌恕我直言並不能解決問題,因爲當s-c仍在運行時,我很可能會在d-tor完全/部分執行的情況下運行。你的第二個建議 - 在調用Thread :: pleaseDie()之後,我不能破壞線程對象,因爲我將再次運行相同的問題。 – dimba 2009-10-06 19:25:24

    +0

    這就是爲什麼你需要peterchen的解決方案:從操作系統級別線程分離C++線程對象。 – jmucchiello 2009-10-06 20:56:00

    4

    通常,任何特定於操作系統的線程API都將允許您「加入」一個線程。也就是說,無限期地阻塞線程句柄,直到線程函數返回。

    所以,

    1. 信號線程函數到(例如,通過在其環路中設置標誌來false)返回。
    2. 加入線程,以確保實際線程終止試圖刪除線程對象之前。
    3. 然後你可以繼續破壞線程對象(你也可以加入析構函數,雖然有些人反對阻止析構函數)。

    我有一個項目之前,類似的「線工人」級和相應的「工作項」級(A-LA Java的ThreadRunnable,除了線程不會終止,而是等待一個新的Runnable對象被執行)。

    最終,沒有什麼區別,如果你在一個單獨的「關機」功能,或在析構函數加入,除了一個單獨的函數是一個比較明顯的。

    1. 如果你加入一個析構函數和一個線程塊,你將無限期地等待。
    2. 如果你加入一個單獨的函數和一個線程塊,你將無限期地等待。
    3. 如果您將線程分離並讓它自行完成,它通常會阻止應用程序退出,因此您將無限期地等待。

    所以有使線程的行爲像一個普通的C++對象,而忽略它的操作系統線程的語義,除非你能保證你的線程代碼幾乎可以立即終止通知這樣做時,沒有直接的方法。

    +0

    這就是我試圖避免的事情,加入了d-tor。它看起來很奇怪並且不直觀。然而,在這種情況下,與外部邏輯相反,線程對象的邏輯是獨立的(當連接返回時,會破壞對象)。 – dimba 2009-10-06 19:31:53

    +2

    在線程對象上創建一個方法,類似'shutdown'。此方法將'run'標誌設置爲false並加入該線程。 現在線程已停止,並且可以重新啓動或刪除。 – gnud 2009-10-06 19:37:22

    0

    你應該添加專門的線程管理類(即MyThreadMngr),它處理這個和其他任務,比如記賬,擁有線程句柄等。線程本身應該以某種方式向線程管理器發出信號,表示它將終止並且MyThreadMngr應該像Tom提出的那樣有一個循環。

    可能會有更多操作套入這樣的線程管理器類中。

    1

    您首先需要一種方式與線程通信以告訴它關閉。最好的機制取決於svc()在做什麼。例如,如果它在消息隊列中循環,則可以在該隊列中插入「請停止」消息。否則,您可以簡單地添加一個由svc()定期檢查的成員bool變量(並對其進行同步訪問),並由希望銷燬該對象的線程設置。你可以添加一個純虛擬stop()函數給你的基類,給實現者一個明確的信號,它必須實現svc()來使其類可以運行,實現stop()使其「可停止」。

    請求線程停止後,必須等待它退出後再銷燬該對象。再次,有幾種方法可以做到這一點。一種是阻止stop()函數。例如,它可以等待一個「確定,我真的停止」條件變量,由運行svc()的線程設置。或者,調用者可以在運行svc()的線程上「等待」。 「等待」的方式取決於平臺。

    +0

    我喜歡那個被阻止的函數是一個普通的類方法而不是d-tor。這種解決方案的優點是我不像普通的C++對象那樣處理線程對象 - 我需要在拋棄對象之前調用stop()函數。 – dimba 2009-10-06 19:38:18

    +0

    請注意,如果線程函數有任何機會訪問線程類的子類的數據成員或虛擬方法,那麼您從線程類的析構函數中調用stop()將不足以保證安全性。將在子類的成員值被銷燬後的一段時間內出現爭用情況,但在〜thread()調用stop()之前,線程仍在運行,這會對您造成問題。 – 2009-10-07 03:13:51

    1

    大多數線程系統允許您發送信號給一個thead。

    例子:並行線程

    pthread_kill(pthread_t thread, int sig); 
    

    這會signall發送到線程。 你可以用它來殺死線程。儘管這可能會使一些資源處於未定義狀態。

    解決資源問題的方法是安裝信號處理程序。
    因此,當調用信號處理程序時,它會引發異常。這將導致線程堆棧展開到入口點,然後您可以獲取線程以檢查它是否存在的天氣變量。

    注意:你不應該允許一個異常傳播出一個線程(這是如此不明確,我的眼睛流血考慮)。基本上在線程入口處捕獲異常,然後檢查一些狀態變量以查看線程是否應該真正退出。

    與此同時,發送信號的線程應通過加入等待線程死亡。

    唯一的問題是,當你丟棄信號處理函數時,你需要小心。您不應該使用異步信號(即可能由另一個線程中的信號生成的信號)。一個很好的使用是SIGSEGV。如果這種情況正常發生,那麼你已經訪問了無效內存,無論如何,你應該考慮退出!

    您可能還需要在某些系統上指定一個額外的標誌來應對。
    See This article

    工作示例使用並行線程:

    #include <pthread.h> 
    #include <iostream> 
    
    extern "C" void* startThread(void*); 
    extern "C" void shouldIexit(int sig); 
    
    class Thread 
    { 
        public: 
         Thread(); 
         virtual ~Thread(); 
        private: 
         friend void* startThread(void*); 
    
         void start(); 
         virtual void run() = 0; 
    
         bool  running; 
         pthread_t thread; 
    }; 
    
    
    // I have seen a lot of implementations use a static class method to do this. 
    // DON'T. It is not portable. This is because the C++ ABI is not defined. 
    // 
    // It currently works on several compilers but will break if these compilers 
    // change the ABI they use. To gurantee this to work you should use a 
    // function that is declared as extern "C" this guarantees that the ABI is 
    // correct for the callback. (Note this is true for all C callback functions) 
    void* startThread(void* data) 
    { 
        Thread* thread = reinterpret_cast<Thread*>(data); 
        thread->start(); 
    } 
    void shouldIexit(int sig) 
    { 
        // You should not use std::cout in signal handler. 
        // This is for Demo purposes only. 
        std::cout << "Signal" << std::endl; 
    
        signal(sig,shouldIexit); 
        // The default handler would kill the thread. 
        // But by returning you can continue your code where you left off. 
        // Or by throwing you can cause the stack to unwind (if the exception is caught). 
        // If you do not catch the exception it is implementation defined weather the 
        // stack is unwound. 
        throw int(3); // use int for simplicity in demo 
    } 
    
    
    Thread::Thread() 
        :running(true) 
    { 
        // Note starting the thread in the constructor means that the thread may 
        // start before the derived classes constructor finishes. This may potentially 
        // be a problem. It is started here to make the code succinct and the derived 
        // class used has no constructor so it does not matter. 
        if (pthread_create(&thread,NULL,startThread,this) != 0) 
        { 
         throw int(5); // use int for simplicity in demo. 
        } 
    } 
    
    Thread::~Thread() 
    { 
        void* ignore; 
    
        running = false; 
        pthread_kill(thread,SIGSEGV); // Tell thread it may want to exit. 
        pthread_join(thread,&ignore); // Wait for it to finish. 
    
        // Do NOT leave before thread has exited. 
    
        std::cout << "Thread Object Destroyed" << std::endl; 
    } 
    
    void Thread::start() 
    { 
        while(running) 
        { 
         try 
         { 
          this->run(); 
         } 
         catch(...) 
         {} 
        } 
        std::cout << "Thread exiting" << std::endl; 
    } 
    class MyTestThread:public Thread 
    { 
        public: 
         virtual void run() 
         { 
          // Unless the signal causes an exception 
          // this loop will never exit. 
          while(true) 
          { 
           sleep(5); 
          } 
         } 
    
    }; 
    
    struct Info 
    { 
        Info() {std::cout << "Info" << std::endl;} 
        ~Info() {std::cout << "Done: The thread Should have exited before this" << std::endl;} 
    }; 
    
    int main() 
    { 
        signal(SIGSEGV,shouldIexit); 
    
        Info    info; 
        MyTestThread  test; 
    
        sleep(4); 
        std::cout << "Exiting About to Exit" << std::endl; 
    
    } 
    
    
    > ./a.exe 
    Info 
    Exiting About to Exit 
    Signal 
    Thread exiting 
    Thread Object Destroyed 
    Done: The thread Should have exited before this 
    > 
    
    +0

    我認爲,當你使用信號發送線程時,你立即終止任何這些 - > run(),並因此讓線程的數據處於未定義狀態,這是不好的(參見方法1 ddj.com/architect/207100682)。也許如果你從你的解決方案中忽略了信號,並且在線程函數(Thread :: startup())中將Thread :: running標誌與d-tor中的pthread_join()一起使用,這也將解決問題,正如一些海報在這裏已經提出的那樣。在這種情況下,您將在進入下一次迭代的同時離開線程函數。 – dimba 2009-10-06 20:56:19

    +0

    @idimba:你部分正確。如果你沒有默認安裝一個信號處理程序,線程就會終止。這不太好,將按照DDJ文章中的描述以及我上面的初步評論進行操作。但是如果你定義了你自己的信號處理程序,線程不會自動終止,直到處理程序。如果你查看代碼,你會看到我安裝了一個引發異常的信號處理程序。這將導致堆棧正確放開(因爲異常被捕獲)。只要你寫出異常安全的RAII代碼,這種方法是可以接受的。 – 2009-10-06 22:34:42

    +0

    你對RAII代碼是正確的,這是異常安全的,這將工作。我也喜歡在你的解決方案中,用戶提供的線程函數沒有暴露給實現細節 - 它只是旋轉:)而恕我直言,強制線程函數使用RAII數據並不總是現實的。編寫exceptioon安全代碼對於avarage程序員來說甚至是一個更大的挑戰。 如果我的評論不正確,我會刪除它:) – dimba 2009-10-07 09:33:05

    0

    我認爲這樣做最簡單的方法是包裝線程執行的代碼在一個循環

    while(isRunning()) 
    { 
        ... thread implementation ... 
    } 
    

    您也可以停止你的通過執行特定的調用進行線程,例如,當您使用WIN32線程時,可以在析構函數的線程句柄上調用TerminateThread。

    +0

    但是,如果我實現一個基類,嵌入線程邏輯,比我需要等待線程退出最派生類析構函數?如果我不會在基類析構函數中執行此操作,那麼派生類析構函數將事先執行,並且線程函數(此運行時)將有機會使用已銷燬的數據成員。 - idimba 0秒前 – dimba 2009-10-11 07:23:53

    0

    我給了一個簡單和乾淨的設計,沒有信號,沒有同步,沒有殺死需要。

    按您的MyThread的,我建議重命名並添加如下:

    class MyThread { 
    public: 
        virutal ~MyThread(); 
    
        // will be called when starting a thread, 
        // could do some initial operations 
        virtual bool OnStart() = 0; 
    
        // will be called when stopping a thread, say calling join(). 
        virtual bool OnStop() = 0; 
    
        // derive class will specialize what the thread should do, 
        // say the thread loop such as 
        // while (bRunning) { 
        // do the job. 
        // } 
        virtual int OnRun() = 0;     
    }; 
    

    螺紋接口的用戶將控制MyThread的壽命。

    ,實際上真正的線程對象是如下:

    class IThread 
        { 
        public: 
         virtual API ~IThread() {} 
    
         /* The real destructor. */ 
         virtual void Destroy(void) = 0; 
    
         /* Starts this thread, it will call MyThread::OnStart() 
          * and then call MyThread::OnRun() just after created 
         * the thread. */ 
         virtual bool Start(void) = 0; 
    
         /* Stops a thread. will call MyThread::OnStop(). */ 
         virtual void Stop(void) = 0; 
    
         /* If Wait() called, thread won't call MyThread::OnStop(). 
         * If could, it returns the value of MyThread::OnRun() 
         * returned */ 
         virtual int Wait(void) = 0; 
    
         /* your staff */ 
         virtual MyThread * Command(void) = 0; 
    
        }; 
    
    /* The interface to create a thread */ 
    extern IThread * ThrdCreate(MyThread *p); 
    

    參閱完整的接口

    http://effoaddon.googlecode.com/svn/trunk/devel/effo/codebase/addons/thrd/include/thrd_i.h 
    

    代碼示例

    案例1.控制線環

    class ThreadLoop : public MyThread 
    { 
    private: 
        bool m_bRunning; 
    public: 
        virtual bool OnStart() { m_bRunning = true; } 
    
        virtual bool OnStop() { m_bRunning = false; } 
    
        virtual int OnRun() 
        { 
         while (m_bRunning) { 
           do your job; 
         } 
        }     
    }; 
    
    int main(int argc, char **argv) 
    { 
         ThreadLoop oLoop; 
    
         IThread *pThread = ThrdCreate(&oLoop); 
         // Start the thread, it will call Loop::OnStart() 
         //and then call Loop::OnRun() internally. 
         pThread->Start(); 
         do your things here. when it is time to stop the thread, call stop(). 
         // Stop the thread, it will call Loop::OnStop(), 
         // so Loop::OnRun() will go to the end 
         pThread->Stop(); 
         // done, destroy the thread 
         pThread->Destroy(); 
    } 
    

    案例2.不知道什麼時候該線程將停止

    class ThreadLoop : public MyThread 
    { 
    public: 
        virtual bool OnStart() { } 
    
        virtual bool OnStop() { } 
    
        virtual int OnRun() 
        { 
         do your job until finish. 
        }     
    }; 
    
    int main(int argc, char **argv) 
    { 
         ThreadLoop oLoop; 
    
         IThread *pThread = ThrdCreate(&oLoop); 
         // Start the thread, it will call Loop::OnStart() 
         //and then call Loop::OnRun() internally. 
         pThread->Start(); 
         do your things here. Since you don't know when the job will 
         finish in the thread loop. call wait(). 
         // Wait the thread, it doesn't call Loop::OnStop() 
         pThread->Wait(); 
         // done, destroy the thread 
         pThread->Destroy(); 
    } 
    

    一個完整IThread實現:

    看到

    http://effoaddon.googlecode.com/svn/trunk/devel/effo/codebase/addons/thrd/src/thrd/thrd.cpp 
    
    +0

    完整的設計文檔可以從http://code.google.com/p/effoaddon/downloads/list下載,名稱爲EffoAddons.pdf,「多線程」部分。 – Test 2009-10-13 05:20:16