[Lv4]Philosopher

https://github.com/syamashi/philosopher

・[mandatory] pthread / mutexを使いこなす
新しいコマンドを知る。
sand.c

#include <stdio.h>
#include <pthread.h>

/*
** mainで宣言したcntを、10000カウントアップするだけの関数。
*/
void *f(void *p)
{
	int	*cnt;

	cnt = p;
	for (int i = 0; i < 10000; ++i)
		++(*cnt);
	return (NULL);
}

int main()
{
	int cnt = 0;
	pthread_t thread1;
	pthread_t thread2;

	// mainの処理とは別に働かせる。並列処理。
	pthread_create(&thread1, NULL, &f, &cnt);
	pthread_create(&thread2, NULL, &f, &cnt);

	// 終了するまで待機。pthread_createとセット
	pthread_join(thread1, NULL);
	pthread_join(thread2, NULL);

	printf("cnt: %d\n", cnt); // 20000がでると思った?
}

pthread_t、並列処理の数だけ作る。

pthread_create()、並列処理開始。
第3引数に渡す関数が仕事。これを作るのがメインタスク。
第4引数が引き継げる値。4byteのデータだけしか引き継げない。から、&でポインタ渡せばいいね。

create()したら、join()かdetach()して終了処理が必要。

pthread_join()、create()が終了するまでmainが待機する。

pthraed_detach()。join()のバリエーション。mainは待機せず先に進む。

Q. pthread~~が見つからなくてコンパイルできない?
A. -pthreadをつけてコンパイル。

gcc -pthread *.c && ./a.out
出力例

 ./a.out
cnt: 20000
 ./a.out
cnt: 13022
 ./a.out
cnt: 19152

pthread_1とpthread_2でカウントアップのタイミングが重なってしまうと、

1. pthread_1が、*cnt=10 を見る。
2. pthread_2が、*cnt=10を見てしまう。
3. pthread_1が、*cnt=11にカウントアップする。
4. pthread_2が、*cnt=11に上書きする。

20000回加算してるのに、20000UPしない事件が発生。

カウントアップが重ならないように、信号を設置します。追記。

sand.c

#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>

typedef struct s_mut
{
	pthread_mutex_t	*mutex;
	int	*cnt;
}	t_mut;

void *f(void *p)
{
	t_mut *t;
	t = p;

	for (int i = 0; i < 10000; ++i)
	{
        // lock ~ unlock の中の処理を動かせるのは1人だけ。lock()で待機する。
		pthread_mutex_lock(t->mutex);
		++*t->cnt;
		pthread_mutex_unlock(t->mutex);
	}
	return (NULL);
}

int main()
{
	int cnt = 0;
	pthread_t thread1;
	pthread_t thread2;

	// 歩行者信号の設置 initで、最初unlock状態がセットされる。単純なboolではなさそう。
	pthread_mutex_t mutex;
	pthread_mutex_init(&mutex, NULL);

    // pthreadに、 cnt と mutex を渡したいので構造体にいれる。
	t_mut t;
	t.mutex = &mutex;
	t.cnt = &cnt;

	// 関数を勝手に走らせる。並列処理。
	pthread_create(&thread1, NULL, &f, &t);
	pthread_create(&thread2, NULL, &f, &t);

	// 終了するまで待機。createとセット
	pthread_join(thread1, NULL);
	pthread_join(thread2, NULL);
	printf("*t.cnt: %d\n", *t.cnt);
}

pthread_mutex_tが歩行者信号。フラグの数だけ作る。ON/OFFしか管理しない。

pthread_mutex_init(&mutex, NULL);、mutexの初期値いれ。

pthread_mutex_lock()、ロック処理。もしpthread1がロックした場合、pthread2はロックで待機する。

pthread_mutex_unlock()、アンロック処理。もしpthread1がアンロックしたら、ロック待ちしてたpthread2が動きだす。pthread2がロックする。

Q. pthread_mutex_tって、もはやbool?
A. 俺もそう思う。値の確認はできなそう

出力例

./a.out 
*t.cnt: 20000
./a.out 
*t.cnt: 20000
./a.out 
*t.cnt: 20000

これで、並列処理と、待機処理が実装できました。

Q. 無限ロック?
A. たとえばlockを2回連続で書きます。
pthread_mutex_lock()
pthread_mutex_lock()
これをすると、unlock()に進む道が途絶え、完全に処理が進まなくなり、ゲーム終了。
pthread_create()の処理の中でたくさんlock()すると思いますが、絶対に、全部unlock()してからタスクを終わらせるようにします。


・[bonus] fork / semaphore を使いこなす

semaphoreの新しいコマンドを知る。5回くらい読んだ。
sem_overview - 約束事その他の説明 - Linux コマンド集 一覧表

sand.c

# include <stdio.h>
# include <string.h>
# include <stdlib.h>
# include <unistd.h>
# include <pthread.h>
# include <stdbool.h>
# include <semaphore.h>
# include <sys/time.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <sys/wait.h>
# include <fcntl.h>
# include <signal.h>

void *func(void *p)
{
	sem_t *sem2 = p;

	for (int i = 0; i < 5; ++i){
		sem_wait(sem2);
		printf("[func]%d called\n", i);
	}
	return (NULL);
}

int main()
{
	sem_t *sem;
	sem_t *sem2;
	pid_t pid;
	int status;
	pthread_t thread;

	sem_unlink("/aaa");
	sem = sem_open("/aaa", O_CREAT, 0600, 2); // "/" から始まれば名前付きsem。同じ名前で共有できる。
	sem2 = sem; // アドレス共有するだけで実態は同じ
	sem_unlink("/aaa");// 全プロセスでこれがクローズされたら、自動でセマフォ削除される
	pid = fork();
	if (pid == 0)
	{
		pthread_create(&thread, NULL, &func, sem2);
		pthread_join(thread, NULL);
		exit(0);
	}
	int microsecond = 1.5 * 1000000;
	for (int i = 0; i < 3; ++i){
		usleep(microsecond);
		sem_post(sem);
	}
	waitpid(-1, &status, 0);
	sem_close(sem);
	return (0);
}

関数ごとのイメージ。
sem_open()で信号の設置。同時処理の許容数がいれられる。
- 第4引数が許容数。
- 第1引数の先頭 "/" で名前付きセマフォ。
- /dev/shm(Linux環境)に sem.xxx が作られる。これが記憶領域なのだろう。

sem_waitでタスク発動。許容数が1減る。

sem_postでタスク補充。許容数が1増える。

sem_t*の中にタスク回数入ってる。参照はできなそう。

sem_unlink()でclose()されているセマフォを削除できる。

sem_close()でセマフォの停止?削除はしていなそう。

出力例

// sem_open(2) で初期値2なので、とりあえず2回すぐに出力
[func]0 called
[func]1 called

// usleep + sem_post()毎に動くため、1.5秒おきに出力
[func]2 called
[func]3 called
[func]4 called

Q. sem_open()の許容数を変更したのに適応されないんだけど?
A. 2回目のコンパイルでしょうか。
すでに指定名のセマフォが作られている場合、許容数の変更は適応されません。
- [Linux] /dev/shm に作成される。
- [Mac] lsof -c <command> | grep PSXSEM で実行中コマンドのセマフォが分かる
- 参考
- https://stackoverflow.com/questions/11393411/how-to-list-posix-semaphores-on-mac-os-x

sem_open()の前後の行でsem_unlink()して、初期化 + 削除を指示すると、毎回新しいsemを作成できます。(それがベストプラクティスかは知らない)

Q. sem_open()の直後にsem_unlink()?
A. すぐに消えるわけじゃないです。sem_close()した後に削除する、という指示のようです。

スクリーンショット 2021-06-15 124122

超絶有能visualizer。もし結果出力したらここに貼るだけ。
https://nafuka11.github.io/philosophers-visualizer/


この記事が気に入ったらサポートをしてみませんか?