2023年03月26日くいなちゃん


6さいからのプログラミング」第10話では、同時に複数の処理を実行する「マルチスレッド」と、効率良くエラーを処理する「例外」について解説します!

1マルチスレッド

マルチスレッド」とは、プログラム中の複数の処理を同時に実行することです。 処理を順番に実行していく流れのことを「スレッド」といい、今まで解説してきたような同時に1つだけ処理が実行される場合は「シングルスレッド」と呼びます(図1-1)。
シングルスレッドとマルチスレッド
図1-1: シングルスレッドとマルチスレッド
マルチスレッドの利点はいくつかあります。 例えば「ファイルをダウンロードする」というプログラムを書くと、ダウンロードに10分かかった場合は10分間操作ができなくなりますが、「ファイルをダウンロードする」と「画面にダウンロード待ちのアニメーションを表示する」のようなプログラムをマルチスレッドで実行させると、ダウンロードしながら別の処理を同時に行うことができます。
多くのCPUには同時に複数の処理を行う能力があり、マルチスレッドを活用すると計算を並列化させて高速化させることもできます。

1.1ロック



さて、そのような便利なマルチスレッドですが、スレッドが複数あるとそれぞれの処理のタイミングが予測できないため、例えば図1-2のように、同じ変数にアクセスしている場合などに予期しない動作をすることがあります。
同じ変数にアクセスする
図1-2: 同じ変数にアクセスする
この図では、スレッド1では「aが0である」という前提で処理したいにもかかわらず、スレッド2の処理のタイミングによってif文の中でaが1になってしまうことがあります。
このような問題を解決するために、マルチスレッドには「ロック」という仕組みが用意されています。 「ロック」とは、あるスレッドがロック状態になっているときに他のスレッドがロックしようとすると、後からロックしようとしたスレッドはロック状態が解除されるまで待ち続けるという機能です(図1-3)。
ロック
図1-3: ロック
この図では、スレッド2がロックしようとしたときにスレッド1が既にロック状態だった場合、スレッド1のロックが解除されるまでスレッド2は待ち続けるので、if文内でaが1になることはなく、画面には必ず「0」が表示されます。
このようなロックの方法は、一般的に「ミューテックス(mutex)」と呼ばれます。 このほかにも同時にロックできるスレッドの数を複数指定できる「セマフォ(semaphore)」など、いろいろなロックの方法があります。

1.2プログラム上で書くには



プログラムで実際にマルチスレッド処理を書くには、C言語や古いC++の規格には標準でこの仕組みが用意されていないため、OSが提供する機能を利用して実現することになります。
例えば、Windows上でVisual C++を用いて先ほどのようなプログラムを書く場合、Windows.hに含まれる関数を用いて図1-4のように書きます。 説明なしには細部がよく解らないと思いますが、全体の流れや雰囲気を掴んでください。
  1. #include <stdio.h>
  2. #include <Windows.h>
  3.  
  4. int a;
  5. CRITICAL_SECTION cs; /* ロックの機能 */
  6.  
  7. /* スレッド1 */
  8. DWORD WINAPI Thread1(LPVOID param)
  9. {
  10.   EnterCriticalSection(&cs); /* ロック */
  11.   if (a == 0)
  12.   {
  13.     /* 0が表示されるはず */
  14.     printf("%d", a);
  15.   }
  16.   LeaveCriticalSection(&cs); /* ロック解除 */
  17.   return TRUE;
  18. }
  19.  
  20. /* スレッド2 */
  21. DWORD WINAPI Thread2(LPVOID param)
  22. {
  23.   EnterCriticalSection(&cs); /* ロック */
  24.   a = 1;
  25.   LeaveCriticalSection(&cs); /* ロック解除 */
  26.   return TRUE;
  27. }
  28.  
  29. int main(void)
  30. {
  31.   a = 0;
  32.   InitializeCriticalSection(&cs); /* ロック機能の準備 */
  33.   CreateThread(NULL, 0, Thread1, Thread1, 0, NULL); /* スレッド1を開始 */
  34.   CreateThread(NULL, 0, Thread2, Thread2, 0, NULL); /* スレッド2を開始 */
  35.   return 0;
  36. }
図1-4: multithreading.c
7~27行目で「Thread1」と「Thread2」という関数を作成し、Windows.hの「CreateThread」という関数を使って33~34行目でこれらをマルチスレッドで実行しています。 CreateThread関数を実行した瞬間、Thread1やThread2はメインのスレッドとは別のスレッドとして開始されます。
ロックを行うには、5行目の「CRITICAL_SECTION」型の変数csを、33行目のように「InitializeCreateSection」関数で初期化しておき、10行目や23行目の「EnterCriticalSection」関数でロックし、その後「LeaveCriticalSection」関数でロック解除する流れです。
また、C#やJavaには標準でロックの仕組みが用意されています。 例えばC#で先ほどのプログラムを書く場合は、図1-5のようになります。
  1. class Multithreading
  2. {
  3.   static int a;
  4.   static object cs; /* ロックに使用する変数 */
  5.  
  6.   /* スレッド1 */
  7.   static void Thread1()
  8.   {
  9.     /* lock(...){} で囲んだ部分がロックされる */
  10.     lock (cs)
  11.     {
  12.       if (a == 0)
  13.       {
  14.         /* 0が表示されるはず */
  15.         System.Console.WriteLine(a);
  16.       }
  17.     }
  18.   }
  19.  
  20.   /* スレッド2 */
  21.   static void Thread2()
  22.   {
  23.     lock (cs)
  24.     {
  25.       a = 1;
  26.     }
  27.   }
  28.  
  29.   static void Main(string[] args)
  30.   {
  31.     a = 0;
  32.     cs = new object(); /* ロック機能の準備 */
  33.     new System.Threading.Thread(new System.Threading.ThreadStart(Thread1)).Start(); /* スレッド1を開始 */
  34.     new System.Threading.Thread(new System.Threading.ThreadStart(Thread2)).Start(); /* スレッド2を開始 */
  35.   }
  36. }
図1-5: multithreading.cs
このようにC#では「lock」という構文を使ってロックします。
以上が「マルチスレッド」の説明になります。

2例外

ここからは「例外」について解説します。
例外れいがい」とはその名の通り、本来行われるべきではない例外的な処理、つまりエラーを扱う仕組みです。 C言語にはありませんが、C++、C#、Javaなど多くの言語に搭載されています。
例外を使わずにエラーを扱う場合は、関数の戻り値でエラーコードを返すことがよく行われます(図2-1)。
  1. int G(void)
  2. {
  3.   /* エラーが発生したら1を返す */
  4.   return 1;
  5. }
図2-1: 関数の戻り値でエラー処理をする
この例では、処理に成功した場合は0を、失敗した場合は1を返すことにより、関数の呼び出し元でエラーが発生したかどうかを知ることができます。
しかし、エラー以外に返したい戻り値がある関数では、戻り値が既に使われているために工夫をする必要があります。 また図2-2のように、関数の中で関数が呼び出される場合にはエラー処理を何重にも書く必要があり煩雑になります。
  1. int G(void)
  2. {
  3.   /* エラーが発生したら1を返す */
  4.   return 1;
  5. }
  6.  
  7. int F(void)
  8. {
  9.   /* 関数G内でエラーが発生したら1を返す */
  10.   if (G() != 0)
  11.   {
  12.     return 1;
  13.   }
  14.   else
  15.   {
  16.     return 0;
  17.   }
  18. }
図2-2: 関数の中で関数を呼ぶ

2.1例外のthrowとcatch



そこで、関数を縦断してエラー情報を受け渡せる仕組みが作られました。 それが「例外」です。 エラー情報を含めたインスタンスのことを「例外」と呼び、例外を発生させることを「throwスロー」といいます。 そしてthrowされた例外を「catchキャッチ」してエラー処理を行います(図2-3)。
例外のthrowとcatch
図2-3: 例外のthrowとcatch
関数G内で例外がthrowされると、catchされるまで関数を次々と抜けていきます。 この図では例外はmain関数でのみcatchしているため、関数Fや関数Gで発生したエラーはすべてmain関数で処理することになります。
「どこで例外が発生したか」は無関係に「どのような例外が発生したか」で処理できるため、様々な関数で発生するエラーをまとめて扱うことができます。

2.2finally



このように、例外がthrowされると関数を抜けていきますが、ヒープから確保した領域を解放したいなど、関数を抜ける前に必ず行っておきたい後処理があります。 それを実現するのが「finallyファイナリー」です。
finallyを書くと、例外がthrowされたときにはfinally内の処理を行ってから関数を抜け、例外は外側の関数のcatch処理に任せます。 例外がthrowされなかった場合でもfinally内の処理を行ってから次の処理へと進みます。

2.3プログラム上で書くには



プログラム上で例外処理を書く場合、例外のthrowには「throw」を使い、例外のcatchやfinallyには「try-catch-finally」を使います。 どの範囲で発生する例外に対し処理するかを「try」で囲んで示し、必要に応じてcatchやfinallyを記述します。
例えばC++では図2-4のように書きます。
  1. void G()
  2. {
  3.   /* エラーが発生したら「1」という例外をthrowする */
  4.   throw 1;
  5. }
  6.  
  7. void F()
  8. {
  9.   G();
  10. }
  11.  
  12. int main()
  13. {
  14.   try
  15.   {
  16.     F();
  17.   }
  18.   catch (int e) /* catchした例外がeに入る */
  19.   {
  20.     /* ここにエラー処理を書く */
  21.   }
  22.   return 0;
  23. }
図2-4: exception_handling.cpp
この場合、関数Gの4行目でthrowした「1」という例外は、main関数の18行目でcatchされ、変数eには「1」が入ります。 仮に関数G内で例外をthrowしなかった場合は、main関数の「catch{}」内に書いた処理はスキップされて実行されません。
ちなみにC++にはfinallyは無く、finallyに相当する処理は別の方法で実現します。
また、C#では図2-5のように書きます。
  1. class ExceptionHandling
  2. {
  3.   static void G()
  4.   {
  5.     /* エラーが発生したらSystem.Exceptionのインスタンスをthrowする */
  6.     throw new System.Exception();
  7.   }
  8.  
  9.   static void F()
  10.   {
  11.     try
  12.     {
  13.       G();
  14.     }
  15.     finally
  16.     {
  17.       /* 解放処理などを書く */
  18.     }
  19.   }
  20.  
  21.   static void Main(string[] args)
  22.   {
  23.     try
  24.     {
  25.       F();
  26.     }
  27.     catch (System.Exception e) /* catchした例外がeに入る */
  28.     {
  29.       /* ここにエラー処理を書く */
  30.     }
  31.   }
  32. }
図2-5: exception_handling.cs
この場合、関数Gの6行目で例外がthrowされると、関数Fの15行目の「finally{}」の処理が行われてから、Main関数の27行目でcatchされます。
今回は「マルチスレッド」と「例外」について説明しました。 次回は、世の中の実際のプログラムがどのように作られているのかを紹介します!
1679825009jaf