openFrameworksでofThreadを利用しマルチスレッドを実装する

先日openFrameworks(以下oF)を使った仕事でどうしてもシングルスレッドだと画像の処理速度が遅いのでマルチスレッドにしたことがあった。Macを使っていたので、最初はiOSアプリを作っていた時代に使い慣れていたGCDを利用していたのだけど、どうもGCDを利用すると落ちたりはしないのだが、肝心の処理する画像が極稀にグリッチし、頻繁にメモリの位置が間違っていた。そこで、最終的にはC++のスレッドを扱う機能を抽象化したofThreadを利用してマルチスレッドの処理を実現したので、ofThreadの使い方のメモを残しておく。

oFにおけるマルチスレッドプログラミングの注意点

最初に注意点を書くと、oFの元となるOpenGLの限界によりOpenGL的な機能はマルチスレッド出来ない。というのもOpenGLはメインスレッド(GLスレッドと呼ばれる)で動く必要があるからだ。そのため、画像の処理においてはofImageなどは利用できないので、ofPixelsを利用する必要がある。

リソース

主に『ofBook』の"Threads"という記事で背景と利用方法を学んだ。またofxThreadedImageLoaderは最新版(ofv0.9.3)のoFに標準で添付されているAddonで、ofThreadを利用した分かりやすいサンプルになっており大変参考になった。

ofThreadの使い方

ofThreadを継承して使う。threadedFunction()というメソッドをオーバライドするのが必須である。これはofThreadのインスタンスをstartThreadした時に呼ばれるメソッドだ。そのスレッドのmain()みたいなものである。サンプルとして一番わかり易い『ofBook』の"Threads"チャプターの最初に載っているコードが分かりやすいので下記に転載する。

class ImageLoader: public ofThread{
    void setup(string imagePath){
        this->path = imagePath;
    }

    void threadedFunction(){
        ofLoadImage(image, path);
    }

    ofPixels image;
    string path;
}

//ofApp.h
bool loading;
ImageLoader imgLoader;
ofImage img;

// ofApp.cpp
void ofApp::setup(){
    loading = false;
}

void ofApp::update(){
    if(loading==true && !imgLoader.isThreadRunning()){
        img.getPixelsRef() = imgLoader.image;
        img.update();
        loading = false;
    }
}

void ofApp::draw(){
    if (img.isAllocated()) {
        img.draw(0, 0);
    }
}

void ofApp::keyPressed(int key){
    if(!loading){
        imgLoader.setup("someimage.png");
        loading = true;
        imgLoader.startThread();
    }
}

このコードはImageLoaderのスレッドでofPixelsで画像を読み込み、メインスレッドでその状態を監視しofPixelsに読み込み終わったらofImageにofPixelsのポインタを渡してやることでofImage側での描画を可能にする、という仕組みである。

ofMutex

さて上記コードは恐らくそのまま動く*1排他制御をしていない。複数のスレッドが同時にメモリの同じ場所にアクセスし読み書きするとクラッシュするため、マルチスレッドプログラミングにおいてはメモリの同じ場所には1つのスレッドからしかアクセスできないようにしてやる必要がある。これはプログラマ側の責任だ。
排他制御のためにmutexが存在する。あるスレッドからmutexをlockをすると、他のスレッドはそのmutexがunlockされるまでlockすることができなくなるという仕組みである。後からlockしようとした他のスレッドはそのmutexがunlockされるまで動作が止まった状態になるため、lockしたならばなるべくはやくunlockする必要がある。
ofMutexの具体的な使い方だが、『ofBook』の"Threads"チャプターに適切な例があるので再び引用する。

class NumberGenerator{
public:
    void threadedFunction(){
        while (isThreadRunning()){
            lock();
            numbers.push_back(ofRandom(0,1000));
            unlock();
            ofSleepMillis(1000);
        }
    }

    vector<int> numbers;
}

// ofApp.h

NumberGenerator numberGenerator;

// ofApp.cpp

void ofApp::setup(){
    numberGenerator.startThread();
}

void ofApp::update(){
    numberGenerator.lock();
    while(!numberGenerator.numbers.empty()){
        cout << numberGenerator.numbers.front() << endl;
        numberGenerator.numbers.pop_front();
    }
    numberGenerator.unlock();
}

まとめと余談

ほぼ『ofBook』の抄訳みたいになってしまったので、詳しくはそちらを参照してほしい。
今から考えるとCPUに計算させるから遅いのであって、GPU、つまりShaderで処理してしまえばよかったのでは?という感じもあるが、実際の画像処理部分は自分が実装するのではなく、他者からの提供であり実装を変えてしまうと結果が変わる可能性があったので、最もマシな解決策だったと思われる。なおShaderはこの記事の元となる仕事とは別案件で利用したので、そのうち書くかもしれない。

*1:確認はしていない