😊

LinuxでOpenCVでのX出力をマルチスレッドで使ってハマった話

2021/02/28に公開

はじめに

OpenCVのVideoCaptureにてX Window Systemを使って
複数の動画を出力する機会があったのですが、
その際に仕様を知らずにハマってしまった話です。

X出力をマルチスレッドで実行

例えばこんな感じのコードを書いていたとします。

void streaming_test(int index){
    int ret = 0;
    int key = 0;

    cv::Mat frame;
    cv::VideoCapture video;
    video.open(video_array.at(index));
    if(!video.isOpened()){
        std::cout << "ERROR: Can't open " << video_array.at(index) << std::endl;
        return;
    }

    std::string window_name = "STREAM" + std::to_string(index);

    while(1){
        ret = video.read(frame);
        if(!ret){
            video.set(CV_CAP_PROP_POS_FRAMES, 0);
            continue;
        }
        cv::imshow(window_name, frame);
        key = cv::waitKey(1);
        if(key == 'q')  break;
    }

    cv::destroyAllWindows();
    return;
}

int main(){
    std::thread test_th0([&]{ streaming_test(0); });
    std::thread test_th1([&]{ streaming_test(1); });

    test_th0.join();
    test_th1.join();
}

このコードを実行したときに何が発生するかといいますと…

(STREAM0:19898): Gdk-ERROR **: 22:48:41.678: The program 'STREAM0' received an X Window System error.

X Window Systemにてエラーが発生しているようです。
初見で見たときは「何事!?」と驚きました。

この現象について原因を探ってみたところ、次のようなIssueを見かける。
https://github.com/opencv/opencv/issues/8407

You should interact with UI from the "main" thread only.

There are a lot of UI backends: GTK/QT/OpenGL/Win32/etc
Some UI backends may support "more or less" multi-thread interaction. But in general, multi-threading is not supported well by UI.

「あ、X Window Systemがマルチスレッドで使えないんか!」とここで気がつく。
よく考えたらX Windowをマルチスレッドで更新かけようとしてしまえば
VRAMでおかしなことになるはずで、システム的にブロックされる、というのは
おかしい挙動ではないと考えた。
(X Window SystemやOpenCV内でブロックするとロックのコストもかかるので
致し方ないといえる)

排他してみよう

ミューテックスで排他制御を入れておく。

std::mutex x_mtx;

void streaming_test(int index){
    int ret = 0;
    int key = 0;

    cv::Mat frame;
    cv::VideoCapture video;
    video.open(video_array.at(index));
    if(!video.isOpened()){
        std::cout << "ERROR: Can't open " << video_array.at(index) << std::endl;
        return;
    }

    std::string window_name = "STREAM" + std::to_string(index);

    while(1){
        ret = video.read(frame);
        if(!ret){
            video.set(CV_CAP_PROP_POS_FRAMES, 0);
            continue;
        }

        {
            std::lock_guard<std::mutex>   lock(x_mtx);
            cv::imshow(window_name, frame);
            key = cv::waitKey(1);
        }
        if(key == 'q')  break;
    }
    
    {
        std::lock_guard<std::mutex>   lock(x_mtx);
        cv::destroyAllWindows();
    }
    return;
}

int main(){
    std::thread test_th0([&]{ streaming_test(0); });
    std::thread test_th1([&]{ streaming_test(1); });

    test_th0.join();
    test_th1.join();
}

この状態で動かしてみると問題なく実行できた。
コピーして別のプロセスで動かしても問題はなかったことも確認できている。
(おそらくプロセス間はちゃんとブロック出来るのだろう)

移植性が気になる

確かにmutexを使えば実行できることはわかった。
しかし、このやり方が正当なやり方なのかは少々疑問でした。
正当なやり方ではない場合、移植性が低い可能性もあります。
X Window System->X11の中でマルチスレッドに何かしらの対応が入ってないか確認してみました。
https://xjman.dsl.gr.jp/X11R6/X11/CH02.html

Using Xlib with Threads

どうやらありそうです。
書いてある情報を参考にして実装してみました。

#include <X11/Xlib.h>

void streaming_test(Display* display, int index){
    int ret = 0;
    int key = 0;

    cv::Mat frame;
    cv::VideoCapture video;
    video.open(video_array.at(index % 3));
    if(!video.isOpened()){
        std::cout << "ERROR: Can't open " << video_array.at(index) << std::endl;
        return;
    }

    std::string window_name = "STREAM" + std::to_string(index);

    while(1){
        ret = video.read(frame);
        if(!ret){
            video.set(CV_CAP_PROP_POS_FRAMES, 0);
            continue;
        }

        {
            XLockDisplay(display);
            cv::imshow(window_name, frame);
            key = cv::waitKey(1);
            XUnlockDisplay(display);
        }
        if(key == 'q')  break;
    }
    
    {
        XLockDisplay(display);
        cv::destroyAllWindows();
        XUnlockDisplay(display);
    }
    return;
}

int main(){
    Status status = XInitThreads();
    if(!status){
        std::cout << "Not Support MultiThread." << std::endl;
        return 0;
    }

    Display* display = XOpenDisplay(NULL);
    if(display == NULL){
        std::cout << "Can't Open Display." << std::endl;
        return 0;
    }

    std::thread test_th0([&]{ streaming_test(display, 0); });
    std::thread test_th1([&]{ streaming_test(display, 1); });

    test_th0.join();
    test_th1.join();
}

このコードでもエラーが発生することなく動作するのを確認できた。
この方法ならX Windowの制御を司っている側の仕組みで排他できるので、
おそらくこちらの方が実装としては良いだろう。

最後に

マルチスレッドは排他トラップが多いけど、こんなトラップに引っかかるとは思わなかった…

Discussion