2017-05-20 10 views
1

std :: thread에 대한 OO 래퍼를 쓰고 있습니다. 다음은 코드의 단순화 된 버전입니다. 이 클래스의 문제점은 클래스가 즉시 파기 될 때 스레드에서 호출되기 때문에 오류가 발생할 수 있다는 것입니다 (순수 가상 메서드가 호출 됨).std :: thread 래퍼 클래스의 즉시 처리 처리

테스트 케이스가 아래쪽에 표시됩니다.

어떻게이 클래스를보다 안전하게 만들 수 있습니까? MyConThread가 MyConThread :: doWork에서 사용되는 멤버 변수를 가지고 있다면 더 복잡한 예제가 더 나 빠질 것입니다.

파생 된 클래스가 생성되기 전에 doWork가 호출 될 수있는 유사한 문제가 있음을 알고 있습니다.

#include <thread> 

class ConThread { 
public: 
    ConThread() 
    :t_ (doWorkInternal, this) 
    {} 
    ~ConThread() 
    { 
     if (t_.joinable()) { 
      t_.join();//avoid a crash because std::thread will terminate the app if the thread is still running in it's destructor 
     } 
    } 

    std::thread& get() {return t_;}; 
    protected: 
    virtual void doWork()=0; 
private: 
    static void doWorkInternal (ConThread* t) 
    { 
     try { 
      t->doWork(); 
     } catch (...) 
     {}; 

    } 
    std::thread t_; 
}; 

내가으로 실행하고 문제는 아래의 테스트 케이스이다 : 모든

class MyConThread: public ConThread 
{ 
public: 
    long i=0; 
protected:  
    void doWork() override 
    { 
     for (long j=0; j<1000000_ && requestedToTerminate_==false; j++) 
     { 
      ++i; 
     } 
    } 
}; 
TEST(MyConThreadTest, TestThatCanBeDestroyed) 
{ 
    MyConThread mct(); //<== crashes when being destroyed because thread calls t->doWork() 
} 
+0

'에서 오는 requestedToTerminate_'는 무엇입니까? – Quentin

+0

이것은 "OO"의 문제가 아닙니다. 기본 기능을위한 다른 API를 제공하는 래퍼를 작성하는 것입니다. – Hurkyl

+0

그리고 덧붙여 말하자면 가상 함수는'std :: function' 메카니즘'std :: thread'가 사용하는 것보다 훨씬 덜 편리하고 유용하다고 생각합니다. – Hurkyl

답변

3

첫째, 프로그램에 관계없이 스레드 객체가 파괴 여부에 충돌합니다. 그것은 확인하기가 매우 쉽습니다, 그냥 객체의 생성 후에 약간의 지연을 삽입 : 당신은 일반적으로 아주 나쁜 생각이다 생성자에서 가상 메소드를 호출하고 있기 때문에

using namespace std::chrono_literals; 

TEST(MyConThreadTest, TestThatCanBeDestroyed) 
{ 
    MyConThread mct(); 
    std::this_thread::sleep_for(100s); 
} 

충돌이 발생합니다. 기본적으로 C++ 객체는 기본에서 파생 된 순서로 생성되며, ctor에서 순수 가상 메서드를 호출하면 오버로드를 아직 처리 할 수 ​​없습니다 (파생물이 아직 생성되지 않았기 때문에). 이 answer도 참조하십시오.

그래서 첫 번째 규칙 : 생성자 또는 소멸자에서 가상 메서드 (아무리 순수하거나 정의 된)를 호출하지 마십시오.

이 문제를 해결하는 가장 쉬운 방법은 실제로 스레드를 시작하는 start 메서드를 추가하는 것입니다. 이처럼 : 일반적으로

ConThread() 
{ 
} 

void start() 
{ 
    t_ = std::thread(doWorkInternal, this); 
} 

, 나는 논리를 혼합의 아이디어를 좋아하지 않아이 작업을 수행하여 사용자가 단일 책임의 원칙을 위반하고 있기 때문에 스레드가 함께 객체. 객체는 두 가지 작업을 수행합니다. 즉 스레드이며, 또한 자신 만의 논리를 가지고 있습니다. 일반적으로 이들을 별도로 처리하는 것이 더 좋습니다. 따라서 std::thread은 생성을 통해 로직을 "전달"하는 수단을 제공하며 기본 클래스로 사용하도록 설계되지 않았습니다. 나는 이것에 대해 좋은 article을 찾았습니다. 스레드는 std 스레드가 아니라 Qt 스레드에 관한 것이지만 개념은 같습니다.

나는 보통 (도 적합하지 않습니다 만, 청소기하는) 내 코드에서 무엇을 : 당신이 dtor에 자동으로 스레드에 가입하려면, 단지 래퍼 주위 std::thread을 만들

std::thread readerThread([] 
{ 
    DatasetReader reader; 
    reader.init(); 
    reader.run(); 
}); 
std::thread mesherThread([] 
{ 
    Mesher mesher; 
    mesher.init(); 
    mesher.run(); 
}); 
readerThread.join(); 
mesherThread.join(); 

하지만 유지

1

두 가지 문제점이 있습니다. 1. 컴파일러가 속임수가 아닌 함수를 호출하도록 속이고 있습니다. 2. 생성자가 남아 있습니다 (예 : 람다 또는 함수 포인터와 매개 변수 등). 스레드가 실제로 시작하는지 확인하기 전에.

1의 경우 템플릿을 사용하십시오. void run() 2 : bool을 사용하여 스레드가 시작되었는지 확인하십시오.

결과 void run(bool * started);은 (주 부울없이 버전이 준우승을 주입) : 당신도 당신의 주자에게 줄 수

파생 클래스가 자동으로 설정 한 후 기본 클래스는 코드를 실행할 수 없습니다 C++에서
template < class runner_class> 
    class ConThread { 
    public: 
     ConThread() // respect order of init 
      :started_(false) 
      , runner_() 
      , t_([this] 
     { 
      started_ = true; 
      runner_.run(); 
     }) 
     { 
      while (!started_); // wait thread is REALLY started ... 
     } 
     ~ConThread() 
     { 
      if (t_.joinable()) { 
       t_.join(); 
      } 
     } 

     std::thread& get() { return t_; }; 
    private: // beware: order of declaration is important here 
     std::atomic_bool started_; 
     runner_class runner_; 
     std::thread t_; 
    }; 
0

; 기본적으로 "post construct"호출은 없습니다.

파생 클래스가 완전히 구성 될 때까지 파생 클래스의 작업 함수가 유효하지 않으므로 파생 클래스가 준비 및 완료 될 때 명시 적으로 명시하지 않으면 기본 클래스가 실행되도록 예약 할 수 없습니다.

std는 std 스레드로이 문제를 해결했습니다. 스레드의 생성자에 인수로 원하는 동작을 주입하는 것이 었습니다. 이제 바하 비어는 스레드가있는 시점까지 완전히 construx되어 있으므로 스레드는 자유롭게 실행하도록 예약 할 수 있습니다.

이것은 상속을 사용하지 않는다는 것을 의미합니다. 적어도 C++ 가상 함수 테이블 기반 상속을 기본으로하지는 않습니다. 그러나 언어 제공 OO를 사용하지 않는다고해서 코드가 OO가 아님을 의미하지는 않습니다.

우리는 경적을 몇 가지 방법으로 OO 제공 언어로 구울 수 있습니다. 스레드 인터페이스에서 상속받을 필요가 있으며, 물건을 걷어차는 유형에서 도우미 유도 템플릿을 가져야합니다.

하지만 C++ 표준 패턴을 따르고 실행 된 개체를 실행 개체에서 분할하는 것이 훨씬 더 좋습니다. 실행 가능한 객체 ("operator()"또는보다 복잡한 것)와 이러한 실행 가능 객체를 사용하는 스레딩 추상화 개념이 있습니다. 두 문제 모두 코드가 더 깔끔해질만큼 복잡합니다.

0

모든 의견을 보내 주셔서 감사합니다. 이것이 제가 끝내 준 것입니다.

// 
// Created by pbeerken on 5/18/17. 
// 

#ifndef LIBCONNECT_CONTHREAD_H 
#define LIBCONNECT_CONTHREAD_H 

#include <thread> 
#include <future> 

class ConRunnable2 
{ 
public: 
    virtual void doWork()=0; 
    void requestToTerminate() {requestedToTerminate_=true;}; 
protected: 
    bool requestedToTerminate_=false; 
}; 

template <class ClassToRun> 
class ConThread2 
{ 
public: 
    //constructor forwards arguments to the ClassToRun constructor 
    template <typename ... Arguments > ConThread2 (Arguments ... args) 
      :toRun_ (args ...) 
      , t_ ([this] { 
       started_.set_value(); 
       toRun_.doWork(); 
      }) 
    { 
     started_.get_future().wait(); //wait till the thread is really started 
    } 

    ~ConThread2() 
    { 
     toRun_.requestToTerminate(); 
     if (t_.joinable()){ 
      t_.join(); 
     } 
    } 
    void requestToTerminate() {toRun_.requestToTerminate();}; 

    std::thread& getThread() {return t_;}; 

    ClassToRun& get() {return toRun_;}; 
private: 
    std::promise <void> started_; 
    ClassToRun toRun_; 
    std::thread t_; 
}; 

#endif //LIBCONNECT_CONTHREAD_H 

이 테스트를 통과하는 :

#include <iostream> 

#include <gtest/gtest.h> 
#include "../ConThread.h" 
#include <chrono> 
#include <future> 


class MyClassToBeRun: public ConRunnable2 
{ 
public: 
    MyClassToBeRun (int loopSize) 
      :loopSize_ (loopSize) 
    {}; 

    void doWork() override 
    { 
     for (long j=0; j<loopSize_ && requestedToTerminate_==false; j++) 
     { 
      ++i_; 
     } 

     p_.set_value(); 
    } 
    long i_=0; 
    long loopSize_=0; 

    std::promise <void> p_; 
}; 


TEST(MyConThread2Test, TestThatItRunsInASeparateThread) 
{ 
    ConThread2<MyClassToBeRun> ct (10000); 
    ct.get().p_.get_future().wait(); 
    EXPECT_EQ (10000,ct.get().i_); 
} 

TEST(MyConThread2Test, TestThatCanBeDestroyed) 
{ 
    ConThread2<MyClassToBeRun> ct (1'000'000'000); 
}