C言語でミューテックスを教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
本記事ではスレッドとプロセスを理解している前提で説明します.
スレッドとプロセスを学びたいあなたはこちらからどうぞ.
目次
ミューテックス
ミューテックス(MUTEX:MUTual EXclusion)とは,共有資源にアクセスする際(クリティカルセクションに出入りする際),アトミックな処理を実行するための排他制御や同期機構の一つです.
ミューテックスはアンロック状態(どのスレッドやプロセスに保有されていない)とロック状態(1つのスレッドやプロセスに保有されている)の2つの状態をとります.
2つの異なるスレッドやプロセスが同時に1つのミューテックスを保有できません.
既に他のスレッドやプロセスによりロックされたミューテックスをロックしようとするスレッドやプロセスは,保有側のスレッドやプロセスが先にそのミューテックスをアンロックするまで実行を停止します.
ミューテックスとセマフォの違いは,ミューテックスはロックとアンロックの2つの状態しかとらないのに対して,セマフォは任意個の状態をとることが可能です.
セマフォを知りたいあなたはこちらからどうぞ.
LinuxにはPOSIXのミューテックス「POSIXミューテックス」が実装されていますので,本記事ではPOSIXミューテックスの使い方を解説していきます.
POSIXミューテックス
1 2 3 4 5 6 7 |
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER; int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); int pthread_mutex_lock(pthread_mutex_t *mutex)); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex); |
pthread_mutexは,POSIXのミューテックスです.
pthread_mutex_init関数は第1引数のmutexが指すミューテックスオブジェクトを,第2引数mutexattrで指定されたミューテックス属性オブジェクトに従って初期化します.
mutexattrがNULLの場合,デフォルトの属性が利用されます.
pthread_mutex_lock関数は,第1引数のmutexをロックします.
mutexが現在ロックされていなければ,それはロックされ,呼び出しスレッドにより所有されます.
この場合,pthread_mutex_lock関数は直ちに返ります.
mutexが他のスレッドによって既にロックされていた場合,pthread_mutex_lock関数はmutexがアンロックされるまで呼び出しスレッドの実行を停止させます.
pthread_mutex_trylock関数はpthread_mutex_lock関数と同様の振る舞いをしますが,第1引数のmutexが既に他のスレッドによりロックされている場合,呼び出しスレッドをブロックしません.
その代わり,pthread_mutex_trylock関数はエラーコードEBUSYで直ちに返ります.
pthread_mutex_unlock関数は,第1引数mutexをアンロックします.
pthread_mutex_destroy関数は,第1引数mutexオブジェクトを破壊し,それが保持している可能性のある資源を解放します.
1 2 |
int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); |
pthread_mutexattr_init関数は,第1引数のmutex属性オブジェクトattrを初期化し,すべての属性をデフォルトの値に設定します.
pthread_mutexattr_setpshared関数は,第2引数psharedで指し示される領域にPTHREAD_PROCESS_PRIVATEを格納し,常に0を返します.
POSIXミューテックスをスレッド間で利用
スレッド間のロックにpthread_mutex_lock関数を利用
スレッド間のロックにpthread_mutex_lock関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <pthread.h> #define LOOP_NUM 50000 #define NR_THREADS 2 void *func(void *arg) { size_t i; #ifndef NO_LOCK pthread_mutex_t *mutex = (pthread_mutex_t *)((void **) arg)[0]; #endif int *count = (int *)((void **) arg)[1]; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK if (pthread_mutex_lock(mutex) != 0) { fprintf(stderr, "Error: cannot lock\n"); exit(1); } /* critical section with lock. */ (*count)++; if (pthread_mutex_unlock(mutex) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(2); } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #endif } return NULL; } int main(void) { pthread_t threads[NR_THREADS]; size_t i; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int count = 0; void *arg[] = {&mutex, &count}; pthread_mutex_init(&mutex, NULL); for (i = 0; i < NR_THREADS; i++) { if (pthread_create(&threads[i], NULL, func, arg) != 0) { fprintf(stderr, "Error: cannot create thread %zu\n", i + 1); exit(1); } } for (i = 0; i < NR_THREADS; i++) { printf("execute pthread_join thread %zu\n", i + 1); if (pthread_join(threads[i], NULL) != 0) { fprintf(stderr, "Error: cannot join thread %zu\n", i + 1); exit(2); } } printf("count = %d\n", count); pthread_mutex_destroy(&mutex); return 0; } |
実行結果は以下になります.
countの数値がLOOP_NUM(=50000)の2倍,つまり50000 * 2 = 100000になることがわかります.
1 2 3 4 5 |
$ gcc mutex_between_threads.c -lpthread $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 100000 |
-DNO_LOCKオプションを付けてコンパイルした場合の実行結果は以下になります.
5行目の「count = 75048」で100000より小さい値になっていることがわかります.(100000や他の値になる場合もあります.)
この理由は,Linuxはプリエンプティブ・マルチタスクの実行が可能なため,countのインクリメント処理でインターリーブが発生してしまうからです.
1 2 3 4 5 |
$ gcc mutex_between_threads.c -lpthread -DNO_LOCK $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 75048 |
スレッド間のロックにpthread_mutex_trylock関数を利用
スレッド間のロックにpthread_mutex_trylock関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <stdbool.h> #define LOOP_NUM 50000 #define NR_THREADS 2 void *func(void *arg) { size_t i; #ifndef NO_LOCK pthread_mutex_t *mutex = (pthread_mutex_t *)((void **) arg)[0]; #endif int *count = (int *)((void **) arg)[1]; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK while (true) { if (pthread_mutex_trylock(mutex) == 0) { /* critical section with lock. */ (*count)++; if (pthread_mutex_unlock(mutex) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(1); } break; } /* cannot lock and do next trial. */ } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #endif } return NULL; } int main(void) { pthread_t threads[NR_THREADS]; size_t i; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int count = 0; void *arg[] = {&mutex, &count}; pthread_mutex_init(&mutex, NULL); for (i = 0; i < NR_THREADS; i++) { if (pthread_create(&threads[i], NULL, func, arg) != 0) { fprintf(stderr, "Error: cannot create thread %zu\n", i + 1); exit(1); } } for (i = 0; i < NR_THREADS; i++) { printf("execute pthread_join thread %zu\n", i + 1); if (pthread_join(threads[i], NULL) != 0) { fprintf(stderr, "Error: cannot join thread %zu\n", i + 1); exit(2); } } printf("count = %d\n", count); pthread_mutex_destroy(&mutex); return 0; } |
実行結果は以下になります.同様です.
1 2 3 4 5 |
$ gcc mutex_between_threads2.c -lpthread $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 100000 |
-DNO_LOCKオプションを付けてコンパイルした場合の実行結果は以下になります.同様です.
1 2 3 4 5 |
$ gcc mutex_between_threads2.c -lpthread -DNO_LOCK $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 73369 |
POSIXミューテックスをプロセス間で利用
POSIXミューテックスをプロセス間で利用するコードを紹介します.
結構難しいので読み飛ばしても構いませんが,理解したいあなたは是非挑戦しましょう!
プロセス間のロックにpthread_mutex_lock関数を利用
プロセス間のロックにpthread_mutex_lock関数を利用するコードは以下になります.
shm_open/shm_unlink関数でプロセス間の共有メモリの作成と削除を行っています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <sys/wait.h> #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #define LOOP_NUM 50000 #define SHM_NAME "/shared_memory" #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) void func(pthread_mutex_t *m, int *count) { size_t i; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK if (pthread_mutex_lock(m) != 0) { fprintf(stderr, "Error: cannot lock\n"); exit(1); } /* critical section with lock. */ (*count)++; if (pthread_mutex_unlock(m) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(2); } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #endif } } int main(void) { pthread_mutex_t *m; pthread_mutexattr_t mat; pid_t pid; int *count; int fd; size_t size = sizeof(pthread_mutex_t) + sizeof(int); if ((fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, FILE_MODE)) == -1) { perror("shm_open"); exit(1); } if (ftruncate(fd, size) == -1) { perror("ftruncate"); exit(2); } if ((m = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) { perror("mmap"); exit(3); } count = (int *)(m + sizeof(pthread_mutex_t)); *count = 0; pthread_mutexattr_init(&mat); if (pthread_mutexattr_setpshared(&mat, PTHREAD_PROCESS_SHARED) != 0) { perror("pthread_mutexattr_setpshared"); exit(4); } pthread_mutex_init(m, &mat); if ((pid = fork()) > 0) { printf("execute parent\n"); } else if (pid == 0) { printf("execute child\n"); } else { perror("fork"); exit(5); } func(m, count); printf("*count = %d\n", *count); if (munmap(m, size) == -1) { perror("munmap"); exit(6); } if (pid != 0) { wait(NULL); if (shm_unlink(SHM_NAME) != 0) { perror("shm_unlink"); exit(7); } } return 0; } |
実行結果は以下になります.
shm_open/shm_unlink関数を利用する際は-lrtオプションが必要なことに注意して下さい.
6行目の「*count = 100000」で正常にカウントできていることがわかります.
※5行目の*countは先に実行終了した親プロセスまたは子プロセスの表示です.
1 2 3 4 5 6 |
$ gcc mutex_between_processes.c -lpthread -lrt $ a.out execute parent execute child *count = 99400 *count = 100000 |
-DNO_LOCKオプションを付けてコンパイルした場合の実行結果は以下になります.
6行目の「*count = 85108」で100000より小さい値になっていることがわかります.
1 2 3 4 5 6 |
$ gcc mutex_between_processes.c -lpthread -lrt -DNO_LOCK $ a.out execute parent execute child *count = 50000 *count = 85108 |
プロセス間のロックにpthread_mutex_trylock関数を利用
プロセス間のロックにpthread_mutex_trylock関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <unistd.h> #include <pthread.h> #include <sys/wait.h> #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #define LOOP_NUM 50000 #define SHM_NAME "/shared_memory" #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) void func(pthread_mutex_t *m, int *count) { size_t i; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK while (true) { if (pthread_mutex_trylock(m) == 0) { /* critical section with lock. */ (*count)++; if (pthread_mutex_unlock(m) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(1); } break; } /* cannot lock and do next trial. */ } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #endif } } int main(void) { pthread_mutex_t *m; pthread_mutexattr_t mat; pid_t pid; int *count; int fd; size_t size = sizeof(pthread_mutex_t) + sizeof(int); if ((fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, FILE_MODE)) == -1) { perror("shm_open"); exit(1); } if (ftruncate(fd, size) == -1) { perror("ftruncate"); exit(2); } if ((m = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) { perror("mmap"); exit(3); } count = (int *)(m + sizeof(pthread_mutex_t)); *count = 0; pthread_mutexattr_init(&mat); if (pthread_mutexattr_setpshared(&mat, PTHREAD_PROCESS_SHARED) != 0) { perror("pthread_mutexattr_setpshared"); exit(4); } pthread_mutex_init(m, &mat); if ((pid = fork()) > 0) { printf("execute parent\n"); } else if (pid == 0) { printf("execute child\n"); } else { perror("fork"); exit(5); } func(m, count); printf("*count = %d\n", *count); if (munmap(m, size) == -1) { perror("munmap"); exit(6); } if (pid != 0) { wait(NULL); if (shm_unlink(SHM_NAME) != 0) { perror("shm_unlink"); exit(7); } } return 0; } |
実行結果は以下になります.同様です.
1 2 3 4 5 6 |
$ gcc mutex_between_processes2.c -lpthread -lrt $ a.out execute parent execute child *count = 88651 *count = 100000 |
-DNO_LOCKオプションを付けてコンパイルした場合の実行結果は以下になります.同様です.
1 2 3 4 5 6 |
$ gcc mutex_between_processes2.c -lpthread -lrt -DNO_LOCK $ a.out execute parent execute child *count = 50000 *count = 83402 |
まとめ
C言語でミューテックスを紹介しました.
具体的には,ミューテックスでスレッドやプロセス間でデータをアトミックに操作する方法を解説しました.
ミューテックスを利用しないとインクリメント処理が正常に実行されないことを確認しましょう.
C11規格でミューテックスを知りたいあなたはこちらからどうぞ.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!