Qtとアトミック命令の話

  • 4
    Like
  • 0
    Comment
More than 1 year has passed since last update.

この記事は、Qt Advent Calendar 2015 23日目の記事です。

kudanと申します。
今年はQtをベースに簡単なスクリプト言語っぽいものを作ろうかとしていたのですが時間的に無理があり力尽きてしまったので、Qtのアトミック命令のお話をしようと思います。

アトミック命令とは

複数のコアが共有しているメモリ上で安全にデータを操作できる命令のことです。
代表的なものにコンベア アンド スワップがあります。
この命令は、書き換える値と最後に読み込んだ値を使い、現在の値が最後に読み込んだ値と同一であれば書き換える、異なっていれば他のスレッドが書き換えている可能性があるので書き込みを中止し、成功の有無が結果として返されるものです。

何に使うのか

以上の説明だけでは、mutex等のロックに比べての一体何の利点があるのか分かりません
アトミック命令はmutexよりも低レベルな並行制御です。扱えるデータも整数型やポインタ型に制限されます。

アトミック命令をC++から使用する場合、
基本的には言語やライブラリで用意されたアトミック変数クラスを用います。
Qtでは QAtomicPointer QAtomicInteger等が用意されているようです。

デットロックの回避とスケーラビリティ

アトミック変数は、mutexと違いスレッド間競合が発生した場合単に操作が失敗するだけです。そのため、あるスレッドがロックしたまま開放し忘れた場合に発生するデットロックは回避する手段があります。
※手段があると言っているのはアトミック変数でも場合によってはデットロックが起きるため。

アトミック変数とは、mutexやスレッド間通信より低級でありデータの一貫性が保障される最も高速なスレッド間の情報伝達手段となります。

サンプルコード

test.cpp
#include <QAtomicInt>
#include <QDebug>

int main()
{
    QAtomicInt p=0;
    //int p=0;
    #pragma omp parallel for //openMP
    for(int index=0;index<10000000;index++){
        p++;

    }
    qDebug() << p;

    return 0;
}

※openMPの並行forを使用しているので、
proファイルに QMAKE_CXXFLAGS += -fopenmp LIBS += -fopenmp
を書き込んで下さい。

実行してみると
QAtomicIntの場合pの値は10000000となり。
intに差し替えると10000000以外の値になることわかると思います。

QAtomicPointer

整数型より大きなデータを扱う場合は、QAtomicPointerを使います。
このQAtomicPointer、Qtで用意されているQSharedPointerやQScopedPointerと異なり。
スマートポインタではありません、
そのため使用する場合は、手動でメモリの確保、開放を行う必要があります。

LockFreeStackの簡単な実装

lockfreestack.cpp
#include <QAtomicPointer>
#include <QDebug>

template<class T>
struct KLockFreeStackNode{
    T* value=nullptr;
    KLockFreeStackNode<T>* next=nullptr;
};

template<class T>
struct KLockFreeStack{
    QAtomicPointer<KLockFreeStackNode<T>> head=nullptr;
    void push(T *value){
        auto n=new KLockFreeStackNode<T>();
        n->value=value;
        for(;;){
            n->next=head;
            if(head.testAndSetOrdered(n->next,n)){
                break;
            }
        }
        return;
    }
    T* pop(){
        KLockFreeStackNode<T>* n;
        for(;;){
            n=head;
            if(head.testAndSetOrdered(n,n->next)){
                auto r=n->value;
                delete n;
                return r;
            }
        }
        return nullptr;
    }
};

int main(){
    //0から100まで 並列にスタックに追加し 取り出す
    KLockFreeStack<int>  stack;
    #pragma omp parallel for
    for(int index=0;index<100;index++){
        auto n=new int;
        *n=index;
        stack.push(n);
    }
    qDebug() << "start";
    #pragma omp parallel for
    for(int index=0;index<100;index++){
        auto n=stack.pop();
        qDebug() <<"取り出した値" << *n << "取り出し回数" << index;
        delete n;
    }
    qDebug() << "end";
    return 0;
}

上のコードを実行すると、複数のスレッドから同時に追加、取り出しが可能であることがわかると思います。

アトミック変数はQtだけではなく C++でもC++11以降 std::atomic でサポートされています。
Qtのようなフレームワークを使う場合、アトミック変数やアトミック命令を意識してプログラムを書くことは稀だとおもいます。ですがシグナル スロットを用いたスレッド間通信やmutexでは過剰であったり、速度的に問題が起こる場合はアトミック変数を使用することも解決策の一つではあります。