Interruptible SleepをC++のmutexで

IGGG Advent Calendar 2016 13日目の記事です

蛇足的イントロ

Expand

さて

昨日のsakuragiさんの記事と比べるとかなり有用性は低いです

別スレッドから中断可能な最大時間指定スリープを作りたい。

C++11使用。

 

 Thread間の調停なので

まずはmutexについて。

mutex(mutual exclusion)について

排他制御を行なう機構です。

電車の閉塞を例にとって説明してみます。

ある区間に複数の電車が入っていると正面衝突、追突など事故の原因になります。
そこで電車はある区間に1編成のみしか入れないようにしています。

閉塞システムの一つであるタブレット閉塞でmutexを説明します。
この方式では「タブレット」を持っている電車のみ線路に進入できます。 タブレットは機械を操作して取り出せます。ひとつ取り出すと戻すまで新しいタブレットは取り出せません。詳しくはタブレット閉塞で検索してみてください

mutexと比較して考えてみると、閉塞機を操作してタブレットを取り出すことがlock(mutex)に、unlockがタブレットを戻す操作になります。unlockし忘れることはタブレットを持ったまま運転士が逃走することと等価でしょう。

電車の話はここで終わりにして

mutexをスレッドで使用したときの流れ

Thread AThread B
std::mutex myheart
Lock(myheart)
*リソースを使用する処理 owns mutex
*リソースを使用する処理 owns mutex Lock(myheart) blocked
*リソースを使用する処理 owns mutex -blocked
Unlock(myheart) -blocked
*リソースを使用する処理 owns mutex
Unlock(myheart)

こんなふうにリソースの排他制御ができます。スコープを抜けたら自動でアンロックするためにstd::unique_lockやstd::lock_guardを使います。

std::condition_variable

条件が満たされるまで待ったりするためのクラス。

wait_for、wait_untilなんてメソッドがあります。

これを使って中断可能スリープを実現します。

condition_variableを利用したコード

class Test {
    public:
    std::mutex mutex_;
    std::thread thread_;
    std::condition_variable cond_;
    ElapsedTimeMeasure<std::chrono::steady_clock> measure; 
    void worker(int wait_sec){
        std::cout<<"Thread start"<<std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        measure.start();
        std::unique_lock<std::mutex> lock(mutex_);
        std::cv_status result = cond_.wait_for(lock,std::chrono::seconds(wait_sec)); 
        measure.stop();
        if (result == std::cv_status::timeout) {
            std::cout << "Timedout " ; 
        }else{
            std::cout << "Interrupped " ; 
        }
        std::cout<<"Slept for:"<<measure.get_seconds<double>()<<std::endl;
    } 
    void notify(){
        cond_.notify_all(); 
    } 
    Test(int waitsec):thread_(&Test::worker,this,waitsec){ }
    ~Test(){ 
        thread_.join(); 
        std::cout<<"Destructor"<<std::endl;
    } 
};
int main(){
    { 
        Test t(3);
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        //Call notify before calling wait_for in worker -> No effect 
        t.notify(); 
        std::this_thread::sleep_for(std::chrono::milliseconds(500-200));
        //The thread may be waiting 
        std::this_thread::sleep_for(std::chrono::seconds(1));
        t.notify();
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); 
    {
        Test t(2);
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        //The thread may be waiting 
        //wait_for may be timed out 
        std::this_thread::sleep_for(std::chrono::seconds(3)); 
        t.notify();
    }
    return 0; 
} 
Expand
Thread start
Interrupped Slept for:1.00007
Destructor
Thread start
Timedout Slept for:2.00009
Destructor

いいかんじに動きました

ちょっといじわる

int main(){
        Test t(10);
        return 0;
}

時計をステップで戻してみると

PC1

Expand
PC1 $ ./a.out&sleep 1&&sudo date --set="30 seconds ago"
[1] 7560
Thread start
Timedout Slept for:40.0009
Destructor

[1]  + 7560 done       ./a.out
PC1 $ ldd ./a.out
        linux-gate.so.1 =>  (0xb7772000)
        libstdc++.so.6 => /usr/lib/i386-linux-gnu/libstdc++.so.6 (0xb7649000)
        libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb7603000)
        libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb75e4000)
        libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb75c8000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb741a000)
        /lib/ld-linux.so.2 (0xb7773000)
PC1 $ ls -l /usr/lib/i386-linux-gnu/libstdc++.so.6  
/usr/lib/i386-linux-gnu/libstdc++.so.6 -> libstdc++.so.6.0.21

失敗:10秒スリープ指示に対して40秒待っている

PC2

Expand
PC2 $ ./a.out &sleep 1&&sudo date --set="30 seconds ago"
[2] 19928
Thread start
Timedout Slept for:10.0074
Destructor

[2]-  Done                    /dev/shm/a.out
PC2 $ ldd ./a.out
        linux-vdso.so.1 =>  (0x00007ffe139c1000)
        libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fd3e0704000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fd3e04ee000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd3e02d0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd3dff07000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fd3dfbfe000)
        /lib64/ld-linux-x86-64.so.2 (0x000055fd00479000)
PC2 $ ls -l /usr/lib/x86_64-linux-gnu/libstdc++.so.6
/usr/lib/x86_64-linux-gnu/libstdc++.so.6 -> libstdc++.so.6.0.21

成功

PC1では失敗PC2では成功

難解スパイラル

こうなったらpthreadを直接触って問題回避

Expand
class InterruptibleSleep{
	pthread_condattr_t attr;
	pthread_mutex_t mutex;
	pthread_cond_t cond;

	public:
	public:
	enum SleepResult {
		TIMEDOUT,INTERRUPTED
	};                                InterruptibleSleep(){
		pthread_condattr_init(&attr);
		if(int err=pthread_condattr_setclock(&attr,CLOCK_MONOTONIC)){
			std::cerr<<"Failed to set clock."<<err<<std::endl;
		}
		mutex=PTHREAD_MUTEX_INITIALIZER;
		pthread_cond_init(&cond,&attr);
	}
	~InterruptibleSleep(){
		pthread_cond_destroy(&cond);
		pthread_condattr_destroy(&attr);
	}
	template<class Rep,class period>
		SleepResult sleep(std::chrono::duration<Rep,Period> rel_time){
			pthread_mutex_lock(&mutex);
			SleepResult     result=INTERRUPTED;
			struct timespec timeout;
			struct timespec now;
			clock_gettime(CLOCK_MONOTONIC,&now);
			int sleep_reltime=(std::chrono::duration_cast<std::chrono::duration<int,std::ratio<1,1>>&gt (rel_time)).count();
			timeout.tv_sec=now.tv_sec+ sleep_reltime;
			timeout.tv_nsec=now.tv_nsec;
			int rc=pthread_cond_timedwait(&cond,&mutex,&timeout);
			if(rc==ETIMEDOUT){
				result=TIMEDOUT;
			}else{
				result=INTERRUPTED;
			}
			pthread_mutex_unlock(&mutex);
			return result;
		}
	void notify(){
		pthread_cond_broadcast(&cond);
	}
};
int main(){
	{
		ElapsedTimeMeasure<std::chrono::steady_clock> measure;
		InterruptibleSleep t;
		std::thread thread([&]{
				measure.start();
				InterruptibleSleep::SleepResult result=t.sleep(std::chrono::seconds(10));
				measure.stop();
				if(result==InterruptibleSleep::SleepResult::TIMEDOUT){
				std::cout<<"Timedout ";

				}else{
				std::cout<<"Interrupted ";

				}
				std::cout<<"Slept for:"<<measure.get_seconds<double>()<<std::endl;
				});
		thread.join();
	}
	return 0;
}


Expand
PC1 $ ./a.out&sleep 1&&sudo date --set="30 seconds ago"
[1] 25678 
Timedout Slept for:10.0408 
[1] + 25678 done ./a.out

CLOCK_MONOTONICがミソ。

C++関係なくなった

おまけ

昨日の記事についてのリアクションをどこに書けばいいのか分からないのでここに

[sudo] apt install openconnect
[sudo] openconnect --juniper <SERVER>

これでも行けるかもしれません(未検証)。 1日目の方法と同じくtun関係で詰まったりするのかな?

on Windowsではビルドが必要かも

嘘だったらごめんなさい

追記:sakuragiさんが検証してくださいました。できたそうです。sakuragiさんありがとうございます。詳しくはコメント欄を参照ください。

Omake2

Diffie-Hellman 鍵交換のデモ

明日はnoobさんです。Marp初耳ですが個人的に楽しみにしています。

Castorb100 orb