『Linuxマルチスレッドサービス側プログラミング-muduo C++ネットワークライブラリを使用する』学習ノート——第二章

10070 ワード

本カタログ
  • 第2章スレッド同期要約
  • 反発量(mutex)
  • 非再帰的mutex
  • のみ
  • デッドロック
  • 条件変数
  • 読み書きロックと信号量
  • は使用しないでください.
  • スレッドの安全な単一のモードは
  • を実現する.
  • shared_を使用ptrはcopy-on-write
  • を実現する
    第2章スレッド同期要約
    スレッド同期の4つの原則は、重要度に基づいてソートされます.
  • は、できるだけオブジェクトを共有することなく、共有オブジェクトは、変更不可能なオブジェクト
  • を優先的に選択する.
  • 高度な同時プログラミングコンポーネント
  • を使用する.
  • 低級同期原語を用いる、非再帰(再入不可)の反発量と条件変数のみを用い、読み書きロック、信号量
  • を用いる.
  • 原子レベル整数を除いて、ロックフリーのコードは記述されず、カーネルレベル同期原語
  • は使用されない.
    はんぱつりょう
    著者の陳碩氏が提案した反発量の使用原則:
  • RAIIの原則に従ってmutexの作成、破棄、ロック、ロック解除
  • を行う
  • 非再帰(再入不可)のmutex
  • を用いる.
  • lock()とunlock()を手動で呼び出すことなく、すべてスタック上のGuardオブジェクトに渡し、ロック期間が臨界領域と同じ長さ
  • になるようにする.
  • Guardロックの順序に注意し、デッドロック
  • を防止する.
    副次的な原則:
  • はプロセス間mutexを使用せず、プロセス間ではtcp通信
  • のみを使用する.
  • ロック解除は同一スレッド(Guardは保証可能)
  • のみである.
  • PTHREAD_を使用可能MUTEX_ERRORCHECKによるエラー
  • 非再帰的なmutexのみ
    再帰的なmutexとは、同じスレッドでmutexを繰り返しロックできることを意味します.
    次のコードによるvectorの共有を考える
    void post(const Foo& f)
    {
        lock_guard lg(mutex_);
        vec.push_back(f);
    }
    void traverse()
    {
        lock_guard lg(mutex_);
        for (auto it = vec.begin(); it != vec.end(); ++it)
        {
            it->doSomeThing();
        }
    }
    

    mutexが再入力可能であれば、2つの関数を同時に実行できます.push_backは反復器の失効を引き起こす可能性がある.再読み込み不可の場合はdoSomeThingがpostを呼び出します.デッドロックが発生します.
    1つの機能関数は、ロックされたバージョンとロックされていないバージョンに分けられ、2つのエラーが発生する可能性があります.
  • 誤ってロックバージョンを使用し、デッドロックを招いた.
  • は、ロックされていないバージョンを誤用し、データを保護していません.

  • ++エラー2++の場合、現在のスレッドがmutexにロックされていないことを保証するために、関数の先頭に断言を加えることができます.
    //    muduo::MutexLock  
    assert(mutex_.isLockedByThisThread());
    

    ++エラー1++単独ディスカッション
    デッドロック
    デッドロックインスタンス
    class Request;
    class Inventory
    {
    public:
    	void add(Request* req)
    	{
    		lock_guard lock(mutex_);
    		requests_.insert(req);
    	}
    	void remove(Request* req)// __attribute__ ((noinline))
    	{
    		lock_guard lock(mutex_);
    		requests_.erase(req);
    	}
    	void printAll() const;
    private:
    	mutable mutex mutex_;
    	std::set requests_;
    };
    Inventory g_inventory;
    
    class Request
    {
    public:
    	void process() // __attribute__ ((noinline))
    	{
    		lock_guard lock(mutex_);
    		g_inventory.add(this);
    		// ...
    	}
    	~Request()// __attribute__ ((noinline))
    	{
    		lock_guard lock(mutex_);
    		this_thread::sleep_for(chrono::milliseconds(1000));
    		g_inventory.remove(this);
    	}
    	void print()// const __attribute__ ((noinline))
    	{
    		lock_guard lock(mutex_);
    		// ...
    	}
    
    private:
    	mutable mutex mutex_;
    };
    
    void Inventory::printAll() const
    {
    	lock_guard lock(mutex_);
    	this_thread::sleep_for(chrono::milliseconds(1000));
    
    	for (std::set::const_iterator it = requests_.begin();
    		 it != requests_.end();
    		 ++it)
    	{
    		(*it)->print();
    	}
    	printf("Inventory::printAll() unlocked
    "); } void threadFunc() { Request* req = new Request; req->process(); delete req; } int main() { thread thread(threadFunc); this_thread::sleep_for(chrono::milliseconds(500)); g_inventory.printAll(); thread.join(); }

    上記のコードでは,InventoryのprintAll()はRequestの~Requset()より0.5 s遅れて起動し,両者とも1 s後に関数を呼び出し,自分のmutexを申請し,ループ待ちをもたらす.
    じょうけんへんすう
    mutexはロックにのみ使用され、条件変数はある条件の達成(ブール式が真である)を待つために使用され、学名管程(monitor).条件変数の使い方は1つだけです.
    wait側の場合:
  • はmutex保護ブール式を使用する必要があります.
  • mutexロック後にwait()が呼び出されます.
  • ブール判断と*wait()*はwhileに入れます.
  • mutex mutex_;
    deque dq;
    condition_variable cv;
    int number = 0;
    
    void consumer()
    {
    	unique_lock lock(mutex_);
    	cv.wait(lock, [] {return !dq.empty(); });
    	cout << dq.front() << endl;
    	dq.pop_front();
    }
    

    注意:
    cv.wait(lock, [] {return !dq.empty(); });
    

    mutexがロック解除され、現在のスレッドがブロックされます.lambdaがtrueを返すまで、ブロックが終了し、mutexが再ロックされます.while式を使用したのと同じです.unique_lockは汎用反発包装器であり、遅延ロック、ロックの期限付き試行、再帰ロック、所有権移転、および条件変数とともに使用することを可能にする.
    signal/broadcastエンドの場合:
  • 理論上、signalは必ずしも鍵をかける必要はない.
  • 一般的にsignalの前にブール式(条件が真である)を変更します.
  • ブール式を変更するにはmutex保護が必要です.
  • signalとbroadcastは異なり、前者は1つのスレッドを起動し、資源は直接利用可能であり、後者はすべてのスレッドを起動し、状態が変化したことを示すしかないため、whileを使用して判断条件を絶えず判断し、虚偽の起動を防止しなければならない.
  • void productor()
    {
    	lock_guard lock(mutex_);
    	dq.push_back(number++);
    	cv.notify_all();
    }
    

    条件変数の場合,下位層の同期原語は直接使用することは少なく,一般に上位層の同期措置を実現するために用いられる.カウントダウンはよく使われる同期手段で、2つの用途があります.
  • プライマリ・スレッドは、各スレッドが一定のタスクを完了するまで、複数のサブスレッドを開始し、プライマリ・スレッドは実行を続行します.
  • メインスレッドは複数のサブスレッドを開始し、メインスレッドが一定のタスクを完了すると、サブスレッドが実行を開始する.
  • class CountDownLatch
    {
    public:
    	explicit CountDownLatch(int count) : cnt_(count) {};
    	void wait()
    	{
    		unique_lock lock(mutex_);
    		cv.wait(lock, [=] {return cnt_ == 0; });
    	}
    	void countDown()
    	{
    		lock_guard lock(mutex_);
    		cnt_--;
    		if (cnt_ == 0)
    		{
    			cv.notify_all();
    		}
    	}
    private:
    	mutable mutex mutex_;
    	condition_variable cv;
    	int cnt_;
    };
    

    読み書きロックや信号量は使わないでください
    初心者はたくさん読んで、少ないシーンを書くことに出会って、mutexをrwlockに置き換えて、正確ではありません:
  • read lockで誤って共有データを書き込み操作し、新機能の場合によく見られる.
  • 臨界領域が小さい場合、ロック競合が大きくない場合、mutexはrwlockよりも効率的である可能性があります.rwlockはreader数を更新するためです.
  • 一部のread lockでは、write lockに昇格できる場合、データ破損やデッドロックなどの一連の問題が導入されます.
  • 一部のwrite lockはread lockをブロックし、読み取り遅延を増加させる.

  • 信号量(Semaphore)は条件変数に取って代わることができ,著者らは経験がない.
    スレッドセキュリティの単一モード実装
    template
    class Singleton : boost::noncopyable
    {
     public:
      static T& instance()
      {
        pthread_once(&ponce_, &Singleton::init);
        return *value_;
      }
    
     private:
      Singleton();
      ~Singleton();
    
      static void init()
      {
        value_ = new T();
        ::atexit(destroy);
      }
    
      static void destroy()
      {
        typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
        delete value_;
      }
    
     private:
      static pthread_once_t ponce_;
      static T*             value_;
    };
    
    template
    pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
    
    template
    T* Singleton::value_ = NULL;
    

    上記コードではプログラム終了時の破棄機能を実現するためにatexitを用いており,この関数の解釈は以下の通りである.
    関数名:atexit機能:登録終了関数(main実行終了後に呼び出される関数)用法:int atexit(void(*func)(void);注意:atexit()に登録されている関数タイプは、パラメータを受け入れないvoid関数であり、exitがこれらの登録関数を呼び出す順序は、登録時の順序とは逆です.作者:Quinn 0918原文:https://blog.csdn.net/Quinn0918/article/details/70457370
    pthread_once(&ponce_, &Singleton::init);
    

    マルチスレッドがpthreadをどのように呼び出しても、登録されたinit関数を保証できます.once,initは一度だけ呼び出されます.
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
    

    このコードは、コンパイル期間において、Tタイプが不完全なタイプ(サイズ0)であるか否かを検査することを目的とする.本質はtypedefを用いてchar配列別名を定義することであり、Tタイプが完全なタイプである場合、char[1]配列別名はT_と定義されるmust_be_complete_type、そうでなければコンパイラがエラーを報告します(char[-1]).githubを参照してください.
    https://github.com/chenshuo/muduo/issues/301
    shared_の使用ptr実現copy-on-write
    前文postとtraverseのデッドロック問題、解決構想:
  • 読みはロックされていません.書くには臨界領域で、繰り返しロックを避ける必要があります.
  • 書く時どのように渋滞しないで読みますか?shared_の使用ptrは占有されているかどうかを観測し,占有されている場合はコピーして書き込む.
  • リードはどのように占有を表しますか?強引にシェアを増やすptrの参照数.
  • mutex mutex_;
    shared_ptr> g_foos;
    
    void traverse()
    {
    	shared_ptr> foos;
    	{
    		lock_guard lock(mutex_);
    		foos = g_foos;//      
    		assert(!g_foos.unique());//      
    	}
    
    	for (auto it = foos->begin(); it != foos->end(); ++it)
    	{
    		it->doSomeThing();
    	}
    }
    
    void post(const Foo& f)
    {
    	lock_guard lock(mutex_);
    	if (!g_foos.unique())
    	{
    		g_foos.reset(new vector(*g_foos));//    ,  copy     
    	}
    	assert(g_foos.unique());//    
    	g_foos->push_back(f);
    }
    

    前文RequestとInventoryのデッドロック解決の考え方:
  • printをprintAllの臨界領域外に除去する.
  • printAllをリードとして、参照カウント表示占有量を増やします.
  • add,removeを書き込み端とし,占有を検出した場合,再書き込みをコピーする.
  • class Request;
    
    class Inventory
    {
     public:
      Inventory()
        : requests_(new RequestList)
      {
      }
    
      void add(Request* req)
      {
        muduo::MutexLockGuard lock(mutex_);//     ,       
        if (!requests_.unique())//     ,    
        {
          requests_.reset(new RequestList(*requests_));
          printf("Inventory::add() copy the whole list
    "); } assert(requests_.unique()); requests_->insert(req); } void remove(Request* req) // __attribute__ ((noinline)) { muduo::MutexLockGuard lock(mutex_); if (!requests_.unique()) { requests_.reset(new RequestList(*requests_)); printf("Inventory::remove() copy the whole list
    "); } assert(requests_.unique()); requests_->erase(req); } void printAll() const; private: typedef std::set RequestList; typedef boost::shared_ptr RequestListPtr; RequestListPtr getData() const { muduo::MutexLockGuard lock(mutex_); return requests_; } mutable muduo::MutexLock mutex_; RequestListPtr requests_; }; Inventory g_inventory; class Request { public: Request() : x_(0) { } ~Request() __attribute__ ((noinline)) { muduo::MutexLockGuard lock(mutex_); x_ = -1; sleep(1); g_inventory.remove(this); } void process() // __attribute__ ((noinline)) { muduo::MutexLockGuard lock(mutex_); g_inventory.add(this); // ... } void print() const __attribute__ ((noinline)) { muduo::MutexLockGuard lock(mutex_); // ... printf("print Request %p x=%d
    ", this, x_); } private: mutable muduo::MutexLock mutex_; int x_; }; void Inventory::printAll() const { RequestListPtr requests = getData();// 1 sleep(1); for (std::set::const_iterator it = requests->begin(); it != requests->end(); ++it) { (*it)->print();// } } void threadFunc() { Request* req = new Request; req->process(); delete req; } int main() { muduo::Thread thread(threadFunc); thread.start(); usleep(500*1000); g_inventory.printAll(); thread.join(); }