C言語でPOSIXスレッドのRead-Write Lockの使い方を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴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社で自分に合うスクールを見つけましょう.後悔はさせません!
本記事ではスレッド,プロセス,ミューテックス,セマフォを理解している前提で説明しますので,これらを学びたいあなたはこちらからどうぞ.
目次
POSIXスレッドのRead-Write Lock
POSIXスレッドのRead-Write Lockを紹介します.
Read-Write Lockとは,readers-writers問題の1つを解決する同期プリミティブです.
Read-Write Lockは,読み取り専用操作では同時アクセスが可能ですが,書き込み操作では排他的アクセスが必要です.
つまり,複数のスレッドが並行してデータを読むことができますが,データの書き込みや変更には排他的ロックが必要になります.
書き手がデータを書き込んでいるときは,書き手が書き込みを終えるまで,他のすべての書き手や読み手がブロックされます.
一般的な使用例としては,アトミックに更新できないメモリ上のデータ構造へのアクセスを制御し,更新が完了するまで無効とする(他のスレッドから読んではいけない)ような場合があります.
Read-Write Lockは通常,ミューテックスと条件変数の上,あるいはセマフォの上に構築されます.
pthread_rwlock_init/pthread_rwlock_destroy関数
1 2 3 |
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); |
pthread_rwlock_init/pthread_rwlock_destroy関数を紹介します.
pthread_rwlock_init関数は,rwlockが参照するRead-Write Lockを使用するために必要なリソースを割り当てて,attrが参照する属性を持つロックをアンロック状態に初期化します.
attrがNULLの場合,デフォルトの読み書きロック属性が使用され,デフォルトの読み書きロック属性のオブジェクトのアドレスが渡されるのと同じ効果があります.
一度初期化されたロックは,再初期化されることなく何度でも使用することができます.
pthread_rwlock_destroy関数は,rwlockによって参照される読み書きロックオブジェクトを破棄し,ロックによって使用されるリソースを解放します.
pthread_rwlock_init/pthread_rwlock_destroy関数は,成功した場合は0を返します.
pthread_rwlockattr_init/pthread_rwlockattr_destroy関数
1 2 |
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); |
pthread_rwlockattr_init/pthread_rwlockattr_destroy関数を紹介します.
pthread_rwlockattr_init関数は,Read/Write Lock属性オブジェクトattrを実装によって定義されたすべての属性のデフォルト値で初期化します.
pthread_rwlockattr_destroy関数は,Read/Write Lock属性オブジェクトを破棄します.
pthread_rwlock_init/pthread_rwlock_destroy関数は,成功した場合は0を返します.
pthread_rwlockattr_getpshared/pthread_rwlockattr_setpshared関数
1 2 |
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared); int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); |
pthread_rwlockattr_getpshared/pthread_rwlockattr_setpshared関数を紹介します.
pthread_rwlockattr_getpshared関数は,attrが参照する初期化済み属性オブジェクトからプロセス共有属性の値を取得します.
pthread_rwlockattr_setpshared関数は,attrが参照する初期化済み属性オブジェクトにプロセス共有属性を設定します.
pthread_rwlockattr_getpshared/pthread_rwlockattr_setpshared関数は,成功した場合は0を返します.
pthread_rwlock_rdlock/pthread_rwlock_tryrdlock関数
1 2 |
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); |
pthread_rwlock_rdlock/pthread_rwlock_tryrdlock関数を紹介します.
pthread_rwlock_rdlock関数は,rwlockが参照するRead-Write LockにRead Lockを適用します.
呼び出し側のスレッドは,ライターがロックを保持しておらず,かつロック上でブロックされているライターが存在しない場合,Read Lockを取得します.
スレッドは,rwlockに対して複数の同時読み取りロックを保持することができます.
つまり,pthread_rwlock_rdlock関数をn回正常に呼び出すことができます.
この場合,アプリケーションは,そのスレッドが一致するロック解除を行う(すなわち,pthread_rwlock_unlock関数をn回呼び出す)ことを保証しなければなりません.
pthread_rwlock_tryrdlock関数は,pthread_rwlock_rdlock関数と同様に読み込みロックをかけます.
ただし,同等のpthread_rwlock_rdlock関数が呼び出したスレッドをブロックしていた場合は失敗するという違いがあります.
pthread_rwlock_tryrdlock関数は,いかなる場合にもブロックしてはなりません.
pthread_rwlock_rdlock/pthread_rwlock_tryrdlock関数は,成功した場合は0を返します.
pthread_rwlock_wrlock/pthread_rwlock_trywrlock関数
1 2 |
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); |
pthread_rwlock_wrlock/pthread_rwlock_trywrlock関数を紹介します.
pthread_rwlock_wrlock関数は,rwlockが参照するRead-Write LockにWrite Lockを適用します.
他のスレッド(readerまたはwriter)がRead-Write Lockのrwlockを保持していない場合,呼び出したスレッドはWrite Lockを取得します.
pthread_rwlock_trywrlock関数は,pthread_rwlock_wrlock関数と同様にWrite Lockをかけるが,現在rwlockを(読み込みまたは書き込みのために)持っているスレッドがある場合は失敗することを除けば,この関数もWrite Lockをかけます.
そうでない場合,そのスレッドは,ロックを獲得できるまでブロックしなければなりません.
呼び出しが行われた時点で,Read LockまたはWrite Lockを保持している場合,呼び出し側のスレッドはデッドロックする可能性があります.
pthread_rwlock_wrlock/pthread_rwlock_trywrlock関数は,成功した場合は0を返します.
pthread_rwlock_unlock関数
1 |
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); |
pthread_rwlock_unlock関数は,rwlockが参照するRead-Write Lockオブジェクトに保持されているロックを解放しなければなりません.
Read-Write Lockオブジェクトからreadロックを解放するために呼ばれます.
Read-Write Lockオブジェクトに他のRead Lockが現在保持されている場合,Read-Write LockオブジェクトはRead Lock状態のままです.
Read-Write Lockオブジェクトに対する最後のRead Lockを解放する場合,Read-Write Lockオブジェクトは,所有者を持たないunlocked状態に置かれます.
Read-Write Lockオブジェクトに対するWrite Lockを解放するために呼ばれた場合,Read-Write Lockオブジェクトは,unlocked状態に置かれなければなりません.
POSIXスレッドのRead-Write Lockをスレッド間で利用
スレッド間のロックにpthread_rwlock_rdlock/pthread_rwlock_wrlock関数を利用
スレッド間のロックにpthread_rwlock_rdlock/pthread_rwlock_wrlock関数を利用するコードは以下になります.
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 |
/* * 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_rwlock_t *rwlock = (pthread_rwlock_t *)((void **) arg)[0]; #endif int *count = (int *)((void **) arg)[1]; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK if (pthread_rwlock_wrlock(rwlock) != 0) { fprintf(stderr, "Error: cannot lock\n"); exit(1); } /* critical section with lock. */ (*count)++; if (pthread_rwlock_unlock(rwlock) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(2); } if (pthread_rwlock_rdlock(rwlock) != 0) { fprintf(stderr, "Error: cannot lock\n"); exit(3); } #if COUNTUP printf("count = %d\n", *count); #endif if (pthread_rwlock_unlock(rwlock) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(4); } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #if COUNTUP printf("count = %d\n", *count); #endif #endif } return NULL; } int main(void) { pthread_t threads[NR_THREADS]; size_t i; pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; int count = 0; void *arg[] = {&rwlock, &count}; pthread_rwlock_init(&rwlock, 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_rwlock_destroy(&rwlock); return 0; } |
実行結果は以下になります.
countの数値がLOOP_NUM(=50000)の2倍,つまり50000 * 2 = 100000になることがわかります.
1 2 3 4 5 |
$ gcc rwlock_between_threads.c -lpthread $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 100000 |
-DCOUNTUPオプションを付けてコンパイルした場合の実行結果は以下になります.
countの値が正常に100000にインクリメントされる様子がわかります.
1 2 3 4 5 6 7 |
$ gcc rwlock_between_threads.c -lpthread -DCOUNTUP $ a.out execute pthread_join thread 1 count = 1 ... execute pthread_join thread 2 count = 100000 |
-DNO_LOCKオプションを付けてコンパイルした場合の実行結果は以下になります.
5行目の「count = 68359」で100000より小さい値になっていることがわかります.(100000や他の値になる場合もあります.)
この理由は,Linuxはプリエンプティブ・マルチタスクの実行が可能なため,countのインクリメント処理でインターリーブが発生してしまうからです.
1 2 3 4 5 |
$ gcc rwlock_between_threads.c -lpthread -DNO_LOCK $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 68359 |
-DCOUNTUPと-DNO_LOCKオプションを付けてコンパイルした場合の実行結果は以下になります.
以下の例では「count = 99979」で100000より小さい値になっていることがわかります.(100000や他の値になる場合もあります.)
1 2 3 4 5 6 7 |
$ gcc rwlock_between_threads.c -lpthread -DCOUNTUP -DNO_LOCK $ a.out execute pthread_join thread 1 count = 1 ... execute pthread_join thread 2 count = 99979 |
スレッド間のロックにpthread_rwlock_tryrdlock/pthread_rwlock_trywrlock関数を利用
スレッド間のロックにpthread_rwlock_tryrdlock/pthread_rwlock_trywrlock関数を利用するコードは以下になります.
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 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <pthread.h> #define LOOP_NUM 50000 #define NR_THREADS 2 void *func(void *arg) { size_t i; #ifndef NO_LOCK pthread_rwlock_t *rwlock = (pthread_rwlock_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_rwlock_trywrlock(rwlock) == 0) { /* critical section with lock. */ (*count)++; if (pthread_rwlock_unlock(rwlock) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(1); } break; } /* cannot lock and do next trial. */ } while (true) { if (pthread_rwlock_tryrdlock(rwlock) == 0) { /* critical section with lock. */ #if COUNTUP printf("count = %d\n", *count); #endif if (pthread_rwlock_unlock(rwlock) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(2); } break; } /* cannot lock and do next trial. */ } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #if COUNTUP printf("count = %d\n", *count); #endif #endif } return NULL; } int main(void) { pthread_t threads[NR_THREADS]; size_t i; pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; int count = 0; void *arg[] = {&rwlock, &count}; pthread_rwlock_init(&rwlock, 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_rwlock_destroy(&rwlock); return 0; } |
実行結果は以下になります.
1 2 3 4 5 |
$ gcc rwlock_between_threads2.c -lpthread $ a.out execute pthread_join thread 1 execute pthread_join thread 2 count = 100000 |
POSIXスレッドのRead-Write Lockをプロセス間で利用
プロセス間のロックにpthread_rwlock_rdlock/pthread_rwlock_wrlock関数を利用
プロセス間のロックにpthread_rwlock_rdlock/pthread_rwlock_wrlock関数を利用するコードは以下になります.
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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
/* * 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_rwlock_t *m, int *count) { size_t i; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK if (pthread_rwlock_wrlock(m) != 0) { fprintf(stderr, "Error: cannot lock\n"); exit(1); } /* critical section with lock. */ (*count)++; if (pthread_rwlock_unlock(m) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(2); } if (pthread_rwlock_rdlock(m) != 0) { fprintf(stderr, "Error: cannot lock\n"); exit(3); } #if COUNTUP printf("count = %d\n", *count); #endif if (pthread_rwlock_unlock(m) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(4); } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #if COUNTUP printf("count = %d\n", *count); #endif #endif } } int main(void) { pthread_rwlock_t *m; pthread_rwlockattr_t mat; pid_t pid; int *count; int fd; size_t size = sizeof(pthread_rwlock_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_rwlock_t)); *count = 0; pthread_rwlockattr_init(&mat); if (pthread_rwlockattr_setpshared(&mat, PTHREAD_PROCESS_SHARED) != 0) { perror("pthread_rwlockattr_setpshared"); exit(4); } pthread_rwlock_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); } } pthread_rwlock_destroy(m); return 0; } |
実行結果は以下になります.
shm_open/shm_unlink関数を利用する際は-lrtオプションが必要なことに注意して下さい.
6行目の「*count = 100000」で正常にカウントできていることがわかります.
※5行目の*countは先に実行終了した親プロセスまたは子プロセスの表示です.
1 2 3 4 5 6 |
$ gcc rwlock_between_processes.c -lpthread -lrt $ a.out execute parent execute child *count = 97985 *count = 100000 |
プロセス間のロックにpthread_rwlock_tryrdlock/pthread_rwlock_trywrlock関数を利用
プロセス間のロックにpthread_rwlock_tryrdlock/pthread_rwlock_trywrlock関数を利用するコードは以下になります.
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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
/* * 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_rwlock_t *m, int *count) { size_t i; for (i = 0; i < LOOP_NUM; i++) { #ifndef NO_LOCK while (true) { if (pthread_rwlock_trywrlock(m) == 0) { /* critical section with lock. */ (*count)++; if (pthread_rwlock_unlock(m) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(1); } break; } /* cannot lock and do next trial. */ } while (true) { if (pthread_rwlock_tryrdlock(m) == 0) { /* critical section with lock. */ #if COUNTUP printf("count = %d\n", *count); #endif if (pthread_rwlock_unlock(m) != 0) { fprintf(stderr, "Error: cannot unlock\n"); exit(2); } break; } /* cannot lock and do next trial. */ } #else /* critical section without lock (interleaving increment operations). */ (*count)++; #if COUNTUP printf("count = %d\n", *count); #endif #endif } } int main(void) { pthread_rwlock_t *m; pthread_rwlockattr_t mat; pid_t pid; int *count; int fd; size_t size = sizeof(pthread_rwlock_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_rwlock_t)); *count = 0; pthread_rwlockattr_init(&mat); if (pthread_rwlockattr_setpshared(&mat, PTHREAD_PROCESS_SHARED) != 0) { perror("pthread_rwlockattr_setpshared"); exit(4); } pthread_rwlock_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); } } pthread_rwlock_destroy(m); return 0; } |
実行結果は以下になります.
1 2 3 4 5 6 |
$ gcc rwlock_between_processes2.c -lpthread -lrt $ a.out execute parent execute child *count = 80342 *count = 100000 |
まとめ
C言語でPOSIXスレッドのRead-Write Lockの使い方を紹介しました.
Read-Write Lockの概念は結構難しいので,何度も読み直しましょう!
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!