2014年4月28日月曜日

C/C++ 処理の流れ その4 並列処理

並列処理は同時に処理を行うことで、長い処理の間にユーザーが別の処理を行えるようにするなど、使い勝手の面で重要でした。

さらに最近では、CPUも単体での性能は頭打ちになってきており、CPUを複数使って、並列に処理することで高速化することも重要となってきました。

基本的にC/C++言語での処理の流れは、単体での処理となっており、並列処理はOS依存であったりライブラリであったりと、同じような処理を行うにも方法が様々です。

最近の新しいバージョンのC/C++では、標準で並列処理をライブラリで持っていますが、普及はまだまだのような気がします。

並列処理にはプロセスとスレッドとコルーチンがありますが、一番一般的なスレッドを扱う方法について説明します。

Win32でのスレッド(C言語)


Windowsの場合は<process.h>の_beginthredex()関数を使用してスレッドを作成し、並列処理を行います。CreateThread()というWin32APIもあるのですが、こちらよりbeginthreadex()関数の方が良いです(標準ライブラリの初期化をため、標準ライブラリを使用する場合は必須です)。

#include <stdio.h>
#include <process.h>
#include <windows.h>

unsigned int __stdcall print_hello(void *dmy)
{
        int i;

        for(i = 0; i < 20; i++) {
                printf("Hello\n");
                Sleep(10);
        }
        return 0;
}

unsigned int __stdcall print_world(void *dmy)
{
        int i;

        for(i = 0; i < 20; i++) {
                printf("World\n");
                Sleep(10);
        }
        return 0;
}

int main()
{
        HANDLE th;

        th = (HANDLE)_beginthreadex(NULL, 0, &print_hello, NULL, 0, NULL);
        print_world(NULL);
        WaitForSingleObject(th, INFINITE);        
        return 0;
}

上記では、分かりやすくするためにスレッドの作成に失敗した場合等のエラー処理は全く行っていないので注意してください。

_beginthraedexで、新しいスレッド(並列処理)を作成して、その並列処理でprint_hello()関数を実行します。元の処理はそのまま次に進みprint_world()関数を実行します。そのため、print_hello()関数とprint_world()関数は同時に行われます。

最後にWaitForSingleObject()関数で、並列処理のスレッドの終了を待って、プログラムを終了します。 これを行わないと、終わる前にプロセスが終了してしまい、スレッドの処理は中断されることになります。

その他、本来であれば、printf()の処理は同じ端末に対して行うので、本来であれば排他処理が必要なのかもしれません。排他処理は同じデータに並列処理が同時にアクセスし、データが異常になるのを防ぐために行います。

詳細は、Microsoftの開発者用のサイトを参照してください。


Linux(POSIX)でのスレッド(C言語)



LinuxやMac OS X等のUNIX系ではPOSIXという規格があり、その中でpthreadライブラリが定義されています。

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

void *print_hello(void *dmy)
{
        int i;

        for(i = 0; i < 20; i++) {
                printf("Hello\n");
                usleep(10);
        }
        return NULL;
}

void *print_world(void *dmy)
{
        int i;

        for(i = 0; i < 20; i++) {
                printf("World\n");
                usleep(10);
        }
        return NULL;
}

int main()
{
        pthread_t th;

        pthread_create(&th, NULL, &print_hello, NULL);
        print_world(NULL);
        pthread_join(th, NULL);        
        return 0;
}

コンパイルをするときには -lpthreadのオプションを追加する必要があります。関数名や引数がWindowsとは違いますが、やっていることは同じなのでほとんど同じ記述になります。

またMinGW環境でも利用可能です。


C++11でのスレッド


C++の2011年番のC++11では<thread>にstd::threadが追加されました。以下がprint_hello()関数とprint_world()関数を並列動作させる例です。残念ながらMinGW環境ではコンパイル時にエラーとなります。

#include <stdio.h>
#include <thread>

void print_hello()
{
        for(int i = 0; i < 20; i++) {
                printf("Hello/n");
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
}

void print_world()
{
        for(int i = 0; i < 20; i++) {
                printf("World/n");
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
}

int main()
{
        std::thread th(print_hello);
        print_world();
        th.join();
        return 0;
}


C11でのスレッド


C言語でも2011年版のC11において、<threads.h>にスレッドを扱うthrd_t型や、thrd_create()関数、thrd_jon()関数が追加されています。残念ながらgccでもサポートがまだのようです(Fedora20, Mingwでthraeds.hが無いことを確認)。


その他のライブラリ



並列プログラミングは、どちらかというと歴史が新しく色々なライブラリがあります。例えば以下です。C++11のstd::threadの元となったライブラリでC++98でも使用できます。
  • boost::thread

 std::threadなどの標準のライブラリは、低レベル(人間に分かりにくく、コンピュータ寄りになっているが、制御は細かくできる)になっていますが、より分かりやすくするためにも色々なライブラリがあります。これらではコンピュータにコアがいくつ付いているかを自動的に判定し、それぞれのコアに処理を分けて振るようになっています。
  • OpenMP
  • MPI(Message Passing Interface)
  • TBB(Intel Threading Building Blocks)
  • Intel Click Plus
  • PPL(Microsoft Parallel Pattern Library)



また、今は同じ種類のCPUを使うホモジニアスマルチコアでなく、違う種類のCPUを使用するヘテロジニアスマルチコアという種類のマルチコアがあります。違う種類のCPUとしてよく使われるのがグラフィックを描画するコアであるGPU(Graphics Processing Unit)です。

現在のGPUは3D画像を扱うのが当たり前になっており、元々大量の3次元処理が得意と言うこともあり、画像処理だけでなく一般の計算処理にも使用しようとしたものです。GPGPU(General-purpose computing on graphics processing units)と言われます。

そのようなヘテロジニアスを使うライブラリとしては以下があります。
  • OpenCL
  • CUDA
  • OpenACC
  • C++AMP(C++ Accelerated Massive Parallelism)

今の所、ヘテロジニアスではメモリ空間が分離されており、スレッドというよりプロセス的な使い方が必要となっていますが、AMDが推進しているHMA(Heterogeneous System Architecture)のhUMAではメモリも統合されており、使い勝手も向上するのでは無いかと思います。


他にも一応、複数の演算を並行して行うという点では、SIMD演算と呼ばれるCPUの以下の命令も並列処理と言えるかもしれません。一つの命令で複数のデータの演算を行うことができます。
  • MMX, SSE, AVX(x86/x64)
  • NEON(ARMv7)
  • AltiVec, VMX(Power)
  • VIS(SPARC)
  • MIPS-3D, MDMX(MIPS)

CPUごとに命令が違うのですが、これらをC++上から同様に扱うMicrosoftのDirectXMachやlibsimdppなどのライブラリもあるようです。

上記のように並列処理には色々な実装があります。いつかは標準でもっと簡単に使えるようになって欲しいですが、現状ではOS標準のAPIの使用が一番いいかもしれません。


0 件のコメント:

コメントを投稿