10. キャンセル(遅延取り消し)
10.2 遅延取り消し
スレッドのキャンセルには下記のとおり二種類あります。
- 非同期取り消し(PTHREAD_CANCEL_ASYNCHRONOUS):キャンセルは即座に行われる
- 遅延取り消し(PTHREAD_CANCEL_DEFERRED):キャンセルはスレッドの処理が「取り消しポイント」に達するまで遅延される。
下記サンプルは遅延取り消し方式によるキャンセル処理です。デフォルトでは遅延取り消し方式が選択されるようです。
/* gcc cancel.c -o cancel -W -Wall -g -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> static int counter; void * thread_func( void * arg ) ; int main( int argc, char ** argv ) { pthread_t thread_id; void * result = 0; if( argc < 2 ) return 1; pthread_create( &thread_id, 0, thread_func, 0 ); fprintf( stdout, "sleep %dsec.\n", atoi( argv[1] )); sleep( atoi( argv[1] )); pthread_cancel( thread_id ); fprintf( stdout, "Thread cancell call!! %d\n", counter ); pthread_join( thread_id, &result ); if( result == PTHREAD_CANCELED ) fprintf( stdout, "cancelled at iteration %d\n", counter ); else fprintf( stdout, "Thread was not cancelled\n" ); return 0; } void * thread_func( void * arg ) { ( void )arg; for( counter = 0; ; counter ++ ) { if(( counter % 1000 ) == 0 ) { pthread_testcancel( ); } } return 0; }
10.3 キャンセルされたくない時
スレッドによってはキャンセルされては困る事もあるでしょうし、対処しなくてはいけなません。またキャンセル可能な取り消しポイントはpthread_testcancel
だけではありません。規格を参照していただければ一覧があります。
これら取り消しポイントを持つ関数で実行中にキャンセル処理が行われると、まずい場合があります。
例えばpthread_mutex_lock
をしていて取り消しポイントを持つ関数(例えばread)を行っている場合、lockされたままスレッドがキャンセル処理されてしまうと、他のスレッドはずっとサスペンドしたままという事になります。
このような場合、ある処理を行っている時はキャンセル禁止にする事で対処します。
/* gcc cancel_disable.c -o cancel_disable -W -Wall -g -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> static int counter; void * thread_func( void * arg ) ; int main( int argc, char ** argv ) { pthread_t thread_id; void * result = 0; if( argc < 2 ) return 1; pthread_create( &thread_id, 0, thread_func, 0 ); fprintf( stdout, "sleep %dsec.\n", atoi( argv[1] )); sleep( atoi( argv[1] )); pthread_cancel( thread_id ); fprintf( stdout, "Thread cancell call!! %d\n", counter ); pthread_join( thread_id, &result ); if( result == PTHREAD_CANCELED ) fprintf( stdout, "cancelled at iteration %d\n", counter ); else fprintf( stdout, "Thread was not cancelled\n" ); return 0; } void * thread_func( void * arg ) { ( void )arg; int cancel_type = 0; int state = 0; int i = 0; pthread_setcanceltype( PTHREAD_CANCEL_DEFERRED, &cancel_type ); pthread_setcancelstate( PTHREAD_CANCEL_ENABLE, &state ); for( counter = 0; ; counter ++ ) { if( counter % 1000 == 0 && counter != 0 ) { if( i == 0 ) { pthread_setcancelstate( PTHREAD_CANCEL_DISABLE, &state ); } sleep( 1 ); if( i == 0 ) { pthread_setcancelstate( state, &state ); } i ^= 1; } } return 0; }
上記サンプルは先のサンプルとほとんど同じです。違うのはスレッド内部で1000回まわるたびにキャンセル可能/不可能を入れ替えている事くらいです。引数の数値が整数の時は止める時の数字と止まった時の数字は等しいですが、奇数の時は数値がずれるはずです。
例えば、
- $ ./cancel_disable 4 <--- 両方とも4000という値。
- $ ./cancel_disable 5 <--- 5000と6000という値になってて、ずれている。
このような感じでキャンセル処理を使いますが、呼び出し元はpthread_cancel
を投げるだけです。対象スレッドは取り消しポイントに対しキャンセル可能/不可能を設定できるため、キャンセル処理自体を選択することができます。取り消し処理をポーリングすることなく、よってオーバーヘッドがないので容易な実装が可能です。
10.4 クリーンアップ
前述しましたが、例えばmutex_lock
された状態で取り消しポイントを持つ関数でキャンセル処理をされてしまうと、誰もunlockできず、他の全スレッドが再開できない状態になります。例えばpthread_cond_wait
は取り消しポイントですが、この時にキャンセル処理をされるとmutexをlockされた状態で起動します。
このような場合、クリーンアップ処理でmutex_unlock
することでサスペンド状態を回避することができます。
クリーンアップ処理は、それ以外にもスレッド間の共通情報を操作する事も可能です。
/* gcc cancel_cleanup.c -o cancel_cleanup -W -Wall -g -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #define THREAD_MAX 8 typedef struct control_tag { int counter; int busy; pthread_mutex_t mutex; pthread_cond_t cond; } control_t; void * thread_func ( void * arg ); void cleanup_handler ( void * arg ); int main( ) { control_t control; pthread_t id[THREAD_MAX]; void * result; int i; control.counter = 0; control.busy = 1; pthread_mutex_init( &control.mutex, 0 ); pthread_cond_init ( &control.cond, 0 ); for( i = 0; i < THREAD_MAX; i ++ ) { pthread_create( &id[i], 0, thread_func, ( void * )&control ); } sleep( 2 ); for( i = 0; i < THREAD_MAX; i ++ ) { pthread_cancel( id[i] ); pthread_join( id[i], &result ); if( result == PTHREAD_CANCELED ) fprintf( stdout, "thread %d cancelled.\n", i ); else fprintf( stdout, "thread %d was not cancelled.\n", i ); } return 0; } void * thread_func( void * arg ) { control_t * control = ( control_t * )arg; pthread_cleanup_push( cleanup_handler, arg ); pthread_mutex_lock( &control -> mutex ); control -> counter ++; while( control -> busy ) { fprintf( stdout, "thread waiting... \n" ); pthread_cond_wait( &control -> cond, &control -> mutex ); } pthread_mutex_unlock( &control -> mutex ); pthread_cleanup_pop( 1 ); return 0; } void cleanup_handler( void * arg ) { control_t * control = ( control_t * )arg; control -> counter --; fprintf( stdout, "cleanup_handler: counter[%d]\n", control -> counter ); pthread_mutex_unlock( &control -> mutex ); return ; }
pthread_creanup_push
にてクリーンアップハンドラを登録し、pthread_creanup_pop
にて取り除きます。環境によってはpthread_creanup_pop
を省略できるらしいですが、汎用性を失う要因になりますので記述した方がよいです。
クリーンアップハンドラは引数を1つ取るので、スレッド固有の情報に対して操作することができ、かなり使い道がありそうです。キャンセル処理対策には必須な機能でしょう。