入門者向け!C言語でのマルチスレッドをわかりやすく解説

このページにはプロモーションが含まれています

プログラミングをしていると、処理を並列して実行したいと思うことが出てきます。

この、処理を並列実行する一つの手段が “マルチスレッド” です。

このページではまずマルチスレッドについて、プログラミング初心者でも理解できるようにわかりやすく解説し、続いてC言語で pthread を用いたマルチスレッドのプログラムの紹介&プログラムの解説を行います。

マルチスレッドとは

まず、マルチスレッドがどのようなものであるかのイメージを固めていきます。続いてマルチスレッドのプログラム例を紹介し、そのイメージを具体的なものに落とし込んでいきたいと思います。

スレッドとは

まずはスレッドから説明します。スレッドとは仕事のことです。

プログラムを起動すると、必ず1つのスレッドが生成されます。

具体的にいうと、C言語プログラムの場合は “main 関数を実行するという仕事(スレッド)” が生成されます(例えば Python などであれば、Python スクリプトを上から順に実行するスレッドが生成されます)。このスレッドをメインスレッドなどと呼ぶことがあります。

スポンサーリンク

CPUのコア

その仕事(スレッド)をするのが CPU のコです。スレッドを仕事として例えるのであれば、コアは仕事をする人です。

コンピュータの脳を司る CPU には複数のコアが搭載されています。つまり仕事をする人が複数います。ですので、同時に複数の仕事をこなすことが可能です。

また、CPU のスペック表を見ると、コア数と一緒にスレッド数が記載されているケースが多いです。こちらのスレッド数は CPU 全体で同時に処理できる仕事(スレッド)の数を表しています。例えばコア数が4つなのにスレッド数が8つの場合は、1つのコアが同時に2つの仕事(スレッド)を処理できることになります。

ただし、スレッド(仕事)1つに対して動作するコアはせいぜい1つだけです。

また、コアは仕事があれば率先して仕事をこなすのですが、仕事がなければ基本的にサボっている状態になります。

もしスレッド(仕事)が1つしかないのであれば、コアが同時に動作するのは1つだけで、他のコアはサボっていることになります。

せっかく手が空いている人がいるのに(コアがあるのに)サボらせているのは勿体無いですよね。

あなたが作成するプログラムを起動した場合、前述の通り main 関数を実行するスレッドが生成されます(もちろん main関数から呼び出される関数の処理も行ってくれます)。

なので、プログラムを起動するだけで、CPU のコアが自動的に main 関数を実行することになります。

ただし、マルチスレッドプログラミングを行わない限り、実行時に生成される仕事(スレッド)は基本的にはその1つのみです。ですので、コアが複数あったとしても、コアを1つしか使用しないプログラムになっている場合が多いです。

スレッドを複数に分割したものがマルチスレッド

サボっているコアに仕事をさせるために、仕事を小さな単位に分割し、サボっているコアに仕事を割り振るというのがマルチスレッド処理の考え方です。

具体的には、マルチスレッドというのは、仕事(スレッド)を複数の仕事に分割した細分化された仕事です。

仕事(スレッド)が一つしかなければ、その仕事に対して動作するコアは1つのみです。

しかし仕事さえあれば、コアは自分から率先して仕事を見つけてその仕事をこなしますので、仕事が複数あれば複数のコアを動作させることができ、並列して仕事をさせることが可能です。

これによりサボっているコアを有効活用することができ、仕事を完了するのに必要な時間を短縮することも可能です。

なので、マルチスレッドプログラミングでは、まずプログラムで行うメインの仕事を小さな仕事に分割することを考える必要があります。

メインの仕事とは、要は main 関数で実行される処理です。この処理を、どのように小さな仕事に分割するか、要は、どの処理を並列に実行するかについて考える必要があります。

例えば1から100までの数字をそれぞれ2倍することがメインの仕事なのであれば、

  • 1から50までの数字をそれぞれ2倍する
  • 51から100までの数字をそれぞれ2倍する

という小さな仕事に分割することができます。

これらをスレッドとして生成すれば、1から50までの計算と51から100までの計算を並列に実行することができることになります。

また、画像の回転がメインの仕事であれば、

  • 画像の上半分を回転する仕事
  • 画像の下半分を回転する仕事

という小さな仕事に分割することができます。

これらをスレッドとして生成すれば、画像の上半分と画像の下半分の回転処理を並列に実行することができるようになります。

どう仕事を分割するかが決まれば、次はその仕事を関数として作成します。

例えば先ほどの例の1つ目で考えると、下記の関数が必要になることになります。

  • 1から50までの数字をそれぞれ2倍する関数
  • 51から100までの数字をそれぞれ2倍する関数

ただし、実際にスレッドの数だけ関数を用意する必要はなく、関数に渡す引数によって仕事を分割するようにしても良いです。例えば上記の場合は、start から end までの数字をそれぞれ2倍する関数を作成し、引数で startend を指定するようにすれば、1つの関数だけで実現することが可能です。

仕事を関数として作成することができれば、あとはその仕事の生成を行います。

この仕事の生成は、関数実行により行うことができます。例えば pthread が使用できる環境であれば pthread_create 関数によりスレッド(仕事)を生成することができます(Windows なんかだと CreateThread 関数でスレッド生成できたはずです)。

前述の通り、プログラムは実行すると必ず main 関数が実行されますので(main 関数を実行するスレッドが生成される)、main 関数の中、もしくは、main 関数から呼び出す関数の中で上記の関数を実行する必要があります。

さらに、これらの関数(pthread_createCreateThread)では、引数として関数を指定することが可能です。この引数に、先ほど作成した “分割した仕事を記述した関数” を指定します。

また、引数には仕事で参照すべきデータを渡したりすることもできます。

で、仕事を生成すれば(pthread_create 等を実行すれば)、引数として指定した関数を実行するスレッドが生成されますので、自動的にコアがその関数を実行してくれることになります。

ここまで説明したことをまとめておきます。

マルチスレッドとは
  • マルチスレッドとは大きな仕事を小さな仕事に分割することで、複数のコアで並列動作できるようにするもの
  • スレッド(仕事)の内容・やることは関数として記述する
  • スレッド(仕事)は pthread_create 関数・CreateThread 関数などにより作成できる
  • スレッド(仕事)を作れば、空いているコアが勝手に処理してくれる

マルチスレッドの同期

マルチスレッドの同期とは、他のスレッドとの待ち合わせを行う処理です。

基本的にそれぞれのコアは仕事を独立に自分のペースで行います。しかし、仕事を自分のペースで行われると困るパターンがあります。

例えば鍋料理について考えてみましょう。ここでは鍋料理するというスレッドを「具材を切るスレッド」、「ダシをとるスレッド」、「具材を煮込むスレッド」の3つのスレッドに分割したいと思います。

これらのスレッドは、それぞれのコアが独立して自分のペースで処理されてしまうと困ってしまいます。

「具材を切るスレッド」と「ダシをとるスレッド」はそれぞれ並列して行って良いスレッドです。

しかし「具材を煮込むスレッド」は、「具材を切るスレッド」と「ダシをとるスレッド」の両方が終わってから開始する必要があるスレッドです。

もし具材が切られていなければ具材をまるまる鍋に入れることになります。また、ダシが取れていない状態で「具材を煮込むスレッド」が開始されると、具材に味が染みなくて出来が悪くなってしまいます。

そこで用いるのが同期です。同期とは前述の通り他のスレッドとの待ち合わせを行う処理です。

同期を用いることで、「具材を煮込むスレッド」を「具材を切るスレッド」と「ダシをとるスレッド」の終了するまで待たせ、それぞれが終了してから煮込みを開始させるようにすることができます。これにより美味しいお鍋を作ることができます。

スポンサーリンク

マルチスレッドのプログラム1(スレッド生成)

ここまでの説明で、マルチスレッドがどのようなものであるかがイメージ出来てきたのではないかと思います。

ここからはマルチスレッドの具体的なプログラムを用いて、そのイメージをもっと具体的なものに落とし込んでいきたいと思います。

このページではC言語で pthreadPOSIXスレッド)を用いたマルチスレッドのプログラムを紹介します。動作確認は Mac OSX で行っていますが、おそらく Linux 等でも動作すると思います。

プログラムの仕様

このプログラムは0から39までの40個の整数の4乗を計算し、結果を配列に格納するプログラムになります

。つまり、「0から39までの40個の整数の4乗を計算し、結果を配列に格納する」がメインのスレッド(仕事)になります。

シングルスレッドのプログラム

マルチスレッドのプログラムと対比するためにまずはシングルスレッドのプログラムを載せておきます。

singlethread.c
#include <stdio.h>
  
#define NUM 40

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;
    /* 結果を格納する配列 */
    int r[NUM];

    /* 0からNUM-1の4乗を計算して配列に格納 */
    for(i = 0; i < NUM; i++){
        r[i] = i * i * i * i;
    }

   return 0;
}

スポンサーリンク

マルチスレッドのプログラム

続いて上記のシングルスレッドのプログラムをマルチスレッドにした時のプログラムを紹介します。

このプログラムでは、「0から39までの40個の整数の4乗を計算し、結果を配列に格納する」というスレッドを、「0から19までの20個の整数の4乗を計算し、結果を配列に格納する」と「20から39までの20個の整数の4乗を計算し、結果を配列に格納する」という2つのスレッドに分割しています。

仕事を分割する処理も仕事でありスレッドですので、このプログラムでは3つのスレッドが実行されることになります。

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

#define NUM 40
#define NUM_THREAD 2

/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

void *func(void *arg);


/* 関数func:スレッドでやること */
/* 引数arg:スレッド(仕事)を行う上で必要な情報 */
void *func(void *arg){
    int i;
    struct data *pd = (struct data *)arg;

    /* argで指定された情報に基づいて処理 */
    for(i = pd->start; i < pd->start + pd->num; i++){
        pd->result[i] = i * i * i * i;
    }

    return NULL;
}

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;

    /* 結果を格納する配列 */
    int r[NUM] = {0};

    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];
    
    /* スレッドを実行する上で必要な情報を格納する配列 */
    struct data d[NUM_THREAD];

    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

    /* 1つ目のスレッドを作成 */
    pthread_create(&t[0], NULL, func, &d[0]);

    /* 2つ目のスレッドを行う上で必要な情報を格納 */
    d[1].start = NUM / 2; /* 計算開始する数 */
    d[1].num = NUM / 2; /* 計算を行う回数 */
    d[1].result = r; /* 計算結果を格納するアドレス */

    /* 2つ目のスレッドを作成 */
    pthread_create(&t[1], NULL, func, &d[1]);

    /* スレッドの終了を待機 */
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);

    return 0;
}
pthreadライブラリ

pthread を使用するためには pthread ライブラリへのリンクが必要です

コンパイル時(リンク時)に -lpthread を追記してやることで pthread ライブラリへのリンクを行うことができます。

プログラムの解説

ではマルチスレッドプログラムの解説をしていきたいと思います。

スレッドの変数定義

main 関数内の下記でスレッド配列の定義を行っています。

pthread_t型の変数宣言
    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];

pthread_tがスレッドを格納する型となります。

ここではスレッドの数の分(NUM_THREAD 分)の配列として変数を定義しています。

この pthread_t 型や後述する pthread_create 関数は pthread.h で宣言されています。ですので、 pthread によるマルチスレッドプログラミングを行うためには pthread.h をインクルードする必要があります。

スレッドの生成(pthread_create 関数)

main 関数内の下記で1つ目のスレッドの生成を行っています。

スレッドの生成
/* 1つ目のスレッドを作成 */
pthread_create(&t[0], NULL, func, &d[0]);

pthread_create 関数はスレッドを生成する関数です。

第1引数はスレッド変数へのポインタを指定します。上記を実行すれば、pthread_create を実行すると、生成したスレッドの ID が t[0] に格納されます。

第2引数はスレッドの属性へのポインタを指定します。属性とは例えばスレッドの優先度(仕事の優先度)などを設定するものです。特に属性を設定しないのであれば NULL を指定するので良いです。

第3引数はスレッドで実行する関数へのポインタを指定します。これは仕事でやる事を指示するようなものです。このプログラムでは func 関数へのポインタを渡しています。

pthreadに渡す関数の型
void *func(void *arg);

引数に指定する関数の型は、戻り値は void* 型、かつ引数は void* 型1つである必要があります。関数名はなんでもオーケーです。

第4引数はスレッドを実行する上で参照するデータへのポインタを指定します。これは仕事を行う上で必要になる情報を渡すようなものです。

pthread_create 関数を実行するとスレッドが生成されますので、サボっている(余っている)コアがそのスレッドを実行してくれます。具体的には、pthread_create 関数の第3引数で指定される関数を実行します。

第3引数で指定された関数が終了すると、スレッドも終了することになります。

スレッドが参照するデータの生成

下記で1つ目のスレッドが参照するデータの生成を行っています。

1つ目のスレッド用のデータ
    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

pthread_create 関数の第4引数で指定できる関数に渡せるデータは1つのみです。

ですので、必要なデータを格納できるように構造体を自分で定義し、その構造体のポインタを渡すのが一般的です。

このプログラムでは、スレッドで計算を開始する数と計算を行う回数、そして計算結果を格納するアドレスを格納できる struct data 構造体を定義し、情報を詰め込んでから pthread_create 関数の第4引数に指定しています。

スレッド処理用のデータ
/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

pthread_create 関数の第3引数の関数が実行される際には、上記のデータが引数として渡されますので、その関数内でそれらのデータを参照しながら処理を行わせることができます。

マルチスレッドのプログラム2(同期)

続いてプログラムを用いて同期の必要性について説明していきたいと思います。

実は先ほど紹介したマルチスレッドのプログラムの中で使用している pthread_join は同期を行う関数の1つになります。

この pthread_join の有無でどのようにプログラムの動きが変わるかを確認していきましょう。

スポンサーリンク

同期なしのプログラム

マルチスレッドのプログラムで紹介したプログラムを少し変更し、main 関数の最後で配列の中身を表示するようにしてみました。

まずは pthread_join なしの場合の動きを確認していきましょう。

同期なし
#include <stdio.h>
#include <pthread.h>

#define NUM 40
#define NUM_THREAD 2

/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

void *func(void *arg);


/* 関数func:スレッドでやること */
/* 引数arg:スレッド(仕事)を行う上で必要な情報 */
void *func(void *arg){
    int i;
    struct data *pd = (struct data *)arg;

    /* argで指定された情報に基づいて処理 */
    for(i = pd->start; i < pd->start + pd->num; i++){
        pd->result[i] = i * i * i * i;
    }

    return NULL;
}

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;

    /* 結果を格納する配列 */
    int r[NUM] = {0};

    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];
    
    /* スレッドを実行する上で必要な情報を格納する配列 */
    struct data d[NUM_THREAD];

    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

    /* 1つ目のスレッドを作成 */
    pthread_create(&t[0], NULL, func, &d[0]);

    /* 2つ目のスレッドを行う上で必要な情報を格納 */
    d[1].start = NUM / 2; /* 計算開始する数 */
    d[1].num = NUM / 2; /* 計算を行う回数 */
    d[1].result = r; /* 計算結果を格納するアドレス */

    /* 2つ目のスレッドを作成 */
    pthread_create(&t[1], NULL, func, &d[1]);

    for (i = 0; i < NUM; i++) {
        printf("%d : %d\n", i, r[i]);
    }
    
    return 0;
}

配列に格納された結果を表示すると下記のようになりました。

0 : 0
1 : 0
2 : 0
3 : 0
4 : 0
5 : 0
6 : 0
7 : 0
8 : 0
9 : 0
10 : 0
11 : 0
12 : 0
13 : 0
14 : 0
15 : 0
16 : 0
17 : 0
18 : 0
19 : 0
20 : 0
21 : 0
22 : 0
23 : 0
24 : 0
25 : 0
26 : 0
27 : 0
28 : 0
29 : 0
30 : 0
31 : 0
32 : 0
33 : 0
34 : 0
35 : 0
36 : 0
37 : 0
38 : 0
39 : 0

同じような結果にならなかった方も多いかもしれません。この結果は実行するたびに異なります。

これは、pthread_create で生成されたスレッドと同時に main 関数の処理も実行されているためです。

たまたま pthread_create で生成されたスレッドでの配列への値の格納が “完了した後” に配列の表示が行われた場合は配列の全要素にちゃんとした計算結果が格納されて表示されます。

ただし、pthread_create で生成されたスレッドでの配列への値の格納が “完了する前” に配列の表示が行われた場合は、上記の結果のように計算結果が格納される前に配列の中身が表示されてしまうことになります。

ここで重要なのは、前述の同期なしのプログラムでは下記の順序が保証されていないところです。

  1. 配列に値が格納される
  2. 配列の中身が表示される

マルチスレッドでは、各スレッドが同時に実行されますので、上記の 1. と 2. が同時に実行されます。ですので、1. の方が先に実行されることももちろんありますが、2. の方が先に実行されたり、1. が実行されている間に 2. が実行されてしまうこともあります。

これによってプログラムが意図しない動きをすることがあり、場合によってはバグになります。

このような順序を保証するために便利なのが同期制御です。

同期ありのプログラム

pthread_join により同期制御を行うようにしたのが下記プログラムになります。

同期なし
#include <stdio.h>
#include <pthread.h>

#define NUM 40
#define NUM_THREAD 2

/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

void *func(void *arg);


/* 関数func:スレッドでやること */
/* 引数arg:スレッド(仕事)を行う上で必要な情報 */
void *func(void *arg){
    int i;
    struct data *pd = (struct data *)arg;

    /* argで指定された情報に基づいて処理 */
    for(i = pd->start; i < pd->start + pd->num; i++){
        pd->result[i] = i * i * i * i;
    }

    return NULL;
}

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;

    /* 結果を格納する配列 */
    int r[NUM] = {0};

    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];
    
    /* スレッドを実行する上で必要な情報を格納する配列 */
    struct data d[NUM_THREAD];

    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

    /* 1つ目のスレッドを作成 */
    pthread_create(&t[0], NULL, func, &d[0]);

    /* 2つ目のスレッドを行う上で必要な情報を格納 */
    d[1].start = NUM / 2; /* 計算開始する数 */
    d[1].num = NUM / 2; /* 計算を行う回数 */
    d[1].result = r; /* 計算結果を格納するアドレス */

    /* 2つ目のスレッドを作成 */
    pthread_create(&t[1], NULL, func, &d[1]);

    /* スレッドの終了を待機 */
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);
    
    for (i = 0; i < NUM; i++) {
        printf("%d : %d\n", i, r[i]);
    }
    
    return 0;
}

このプログラムの実行結果は下記の通りです。今度は実行するたびに結果が変わることがなく、確実に下記の結果を表示することができます。

0 : 0
1 : 1
2 : 16
3 : 81
4 : 256
5 : 625
6 : 1296
7 : 2401
8 : 4096
9 : 6561
10 : 10000
11 : 14641
12 : 20736
13 : 28561
14 : 38416
15 : 50625
16 : 65536
17 : 83521
18 : 104976
19 : 130321
20 : 160000
21 : 194481
22 : 234256
23 : 279841
24 : 331776
25 : 390625
26 : 456976
27 : 531441
28 : 614656
29 : 707281
30 : 810000
31 : 923521
32 : 1048576
33 : 1185921
34 : 1336336
35 : 1500625
36 : 1679616
37 : 1874161
38 : 2085136
39 : 2313441

プログラムの解説

先ほどの同期なしのプログラムとほぼ同じプログラムになります。

スレッドの同期(pthread_join 関数)

ただし、今回のプログラムではスレッドの同期を下記で行っています。

pthread_joinによる同期
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);

pthread_join 関数は他のスレッドが終わるのを待つ関数です。第1引数にはスレッドを指定し、第2引数はスレッドの実行結果を格納するデータへのポインタです。

スレッド実行結果が不要であれば第2引数は NULL でもオーケーです。

pthread_join 関数は前述の通り第1引数に指定したスレッドの処理が終了するまで待つ関数です。

ですので、上記の pthread_join 関数の実行により pthread_create で生成した2つのスレッドの処理が終了しないと、pthread_join 関数実行以降に記述された処理は実行されません。

したがって、pthread_join 関数実行以降に記述された配列の中身を表示する処理は、配列への値の格納を行う2つのスレッドの処理が完了しないと実行されないことになります。

これにより下記の順序で処理が実行されることが保証されることにになります。

  1. 配列に値が格納される
  2. 配列の中身が表示される

ですので、実行するたびに結果が変わってしまうようなこともなく、確実に意図した結果を表示することができます。

今回は同期に pthread_join 関数を用いましたが、他にもメッセージキューや MUTEX など様々な同期方法があります。

スポンサーリンク

マルチスレッドのメリット/デメリット

最後にマルチスレッドを行うことによるメリット/デメリットについて解説します。

メリット

まずはメリットについて解説します。

処理の高速化が可能

マルチスレッドのメリットの一つは高速化です。

計算時間のかかる処理を複数のコアで並列して処理できるようになりますので処理時間が短くなります。

複数の処理の同時実行

また、マルチスレッド を利用することにより、複数の別々の処理を同時に実行することが可能です。

例えば1つのスレッドで scanf 関数でユーザーからの入力待ちを行いながら、他のスレッドで別の処理を同時に実行するようなことも簡単に実現することができます。

デメリット

次にデメリットについて解説します。

プログラムが難しくなる

デメリットの一つはプログラムが難しくなることです。

仕事を細分化するためのプログラムを記述する必要があるのでプログラムが難しくなります。またマルチスレッドプログラミングでは同期処理が必要になり、同期制御のプログラミングも必要になります。

遅くなる可能性がある

デメリットのもう一つはむしろ遅くなる可能性があることです。

マルチスレッドではシングルスレッドに比べて仕事を細分化する処理や同期制御が追加で必要になります。簡単ですぐ終わる仕事をマルチスレッドにしたりすると、これらの処理・制御が追加された分処理時間が増加してしまう必要があります。

スポンサーリンク

まとめ

スレッドとは仕事のことあり、マルチスレッドはスレッドを複数に細分化したものです。マルチスレッドにより複数のコアで並列に処理を実行することが可能です。同期を行う必要がある場合はむしろ遅くなる可能性もありますので注意しましょう。

下のページでは画像の拡大縮小をマルチスレッドで処理するプログラムを解説しています。マルチスレッドのイメージがさらにつきやすくなると思いますのでよろしければ読んでみてください。

【C言語】マルチスレッドで画像の拡大縮小を高速化

同じカテゴリのページ一覧を表示

4 COMMENTS

y.k

サンプルコードも理解するために簡単すぎず難しすぎずちょうどよく、大変わかりやすかったです。ありがとう。

daeu

y.k さん

コメントありがとうございます!
そう言っていただけると大変嬉しいです!

今後も皆さんのタメになる記事を書いていきたいと思いますので、
気が向いたときにでもまた立ち寄っていただけると幸いです。

現在コメントは受け付けておりません。