Nagarei / DxLibEx

DXライブラリC++化プロジェクト
Boost Software License 1.0
31 stars 3 forks source link

マルチスレッド対応について #7

Open Nagarei opened 9 years ago

Nagarei commented 9 years ago

マルチスレッド対応について正確に決めておく必要があると思います。

と3パターンあると思いますが、さらに

というパターンがあります。 特に最後のはただ「スレッドセーフではない」では済まないのでどうしたものかという感じです。

論点(だと思う):DxLibを呼び出す関数のスレッドセーフ保障

//DxLibの他のオブジェクトからの動作と競合する例
//異なるオブジェクトへのアクセスのみだが、問題が起こる
void func()
{
    Screen screen(640, 480);
    screen.draw([]{
        LoadGraphScreen( 0 , 0 , "test1.bmp" , TRUE ) ;
    });
}
void Run(){
    std::thread thd(func);
    func();
    thd.join();
}
Nagarei commented 9 years ago

見直してみるとわかりづらいですね...。

ここではDxLibがスレッドセーフに動作するとしておきます。 例えば関数Texture2D::DrawGraphを呼び出すとします。 しかし、同時にScreen::draw関数が別のスレッドでよばれていた場合Texture2D::DrawGraphはどこに描画されるかがわからなくなってしまいます。 このように表から見れば異なるオブジェクトに対する操作なのにスレッドセーフでなくなってしまうというのが問題です。

yumetodo commented 9 years ago

4

うーん、クラス内にstatic変数作って、mutexすればそこまで面倒では無いと思うんですが・・・。 http://minus9d.hatenablog.com/entry/20130914/1379168684

Nagarei commented 9 years ago

下のケースではライブラリ内でmutexを使っても状況は変わりません。はじめからこっちを載せておくべきでした...。

//DxLibの他のオブジェクトからの動作と競合する例
//異なるオブジェクトへのアクセスのみだが、問題が起こる
void func()
{
    Screen screen(640, 480);
    screen.draw([]{
        LoadGraphScreen( 0 , 0 , "test1.bmp" , TRUE ) ;
    });
}
void Run(){
    Texture2D graph1(Texture2D::LoadGraph("test1.bmp));
    std::thread thd(func);
    graph1.DrawGraph( 0 , 0 , true) ;
    thd.join();
}

ユーザーがmutexを使って排他制御をすれば大丈夫なのですが...。それを要請するためにどのようなスタンスを取るべきかといった感じです。

yumetodo commented 9 years ago

ちょっと待って下さい。私はクラス内のstatic変数としてmutexをつかう、と書いたと思います。

この時点でヘッダーだけで実装することはかなわず、.cppファイルが必要になりますが、それくらい作ってもいいと思います。

なるべく.hppに書くようにしてきた理由は関数callのオーバーヘッドを減らすためだったので、それにかかわらなければ.cppに書いていいと理解していますが、そうでは無いのでしょうか?

Nagarei commented 9 years ago

ごめんなさい。僕は「クラス内のstatic変数としてmutexを使う」ことにしても、この問題を解決する方法を思いつきません。 上のケースを解決するサンプルコードを書いていただけないでしょうか?

Nagarei commented 9 years ago

DrawnOn関数を仮実装ですが追加しておきました。

yumetodo commented 9 years ago

あとで実験してちゃんと書きますが、イメージです

a.hpp

#include <mutex>
class foo{
public:
    //前略
    template<typename Func_T>
    void DrawnOn(Func_T&& draw_func) {
        this->screen_authority.lock();
        auto old_draw_screen = DxLib::GetDrawScreen();
        this->SetDrawScreen();
        draw_func();
        DxLib::SetDrawScreen(old_draw_screen);
        this->screen_authority.unlock();
    }
    //中略
private:
    //中略
    static std::mutex screen_authority;
}

a.cpp

#include "a.hpp"
std::mutex foo::screen_authority;

http://minus9d.hatenablog.com/entry/20130914/1379168684

Nagarei commented 9 years ago

そのやり方だと、以下が反例になります。Run関数が呼んでいるLoadGraphScreenの書き込む先がDrawnOnのSetDrawScreenの呼び出しの前か後かで変わってしまいます。 このケースは異なるオブジェクトに対するアクセスなのでこのような曖昧さが生じるべきではないです。

void func()
{
    Screen screen(640, 480);
    screen.DrawnOn([]{
        DxLib::LoadGraphScreen( 0 , 0 , "test1.bmp" , TRUE ) ;
    });
}
void Run(){
    std::thread thd();
    DxLib::LoadGraphScreen( 0 , 0 , "test2.bmp" , TRUE ) ;
    thd.join();
}
yumetodo commented 9 years ago

いや、正直仕方ないかなと。そしてやっと

DxLibを使う関数は全てスレッドセーフ保障なしにする

がわかった。

100%確実なのは、DxLib側のmutexに相当する部分をAPIとして公開してもらうことですが、それも馬鹿げてるので(というかあれのAPI化とか多分相当な労力になる気がする)、dxle名前空間の関数を使用する限りにおいてはスレッドセーフを保証する方向で行きましょう。混ぜるな危険、ということで。

もっとも、スレッドセーフを保証できなくなるのは一部の関数だけのはずなので、(ですよね?)、あとでスレッドセーフを保証できなくなるDxLib側の関数をリストアップしましょう。

継承元のTexture2Dにmutex変数をstaticメンバー変数として持たせるほうがいいのかな

Nagarei commented 9 years ago

継承元のTexture2Dにmutex変数をstaticメンバー変数として持たせる

DrawBox系をdxleに入れた場合問題になる、と思ったけれどそれはなさそうですね...。 しかし、将来こういう事が起こる可能性を考慮するとmutexをグローバル変数にしたほうが良いのではと思うのですがどう思いますか?

yumetodo commented 9 years ago

いやそれよりもmutexをstaticメンバー変数にもつクラスを例えばscreen_mutex_cとかいう名前で作って、それを継承したほうがいい気がしてきました・・・

いずれにせよ、変更はたいしたbreaking changeではないのでどれでもいいっちゃいいんですが。

Nagarei commented 9 years ago

mutexをstaticメンバー変数にもつクラスを例えばscreen_mutex_cとかいう名前で作って、それを継承したほうがいい気がしてきました・・・

ではそんな感じで作ってみます。

Nagarei commented 9 years ago

mutex系で一つのヘッダにするのと関連する機能のところに書くのとどっちが良いですかね?

yumetodo commented 9 years ago

多分関連する機能のところに書く方向でいいと思います。

そういえばもう死んでますが http://wayback.archive.org/web/20031210174108/http://dkingyoutility.sourceforge.jp/studiokingyonet/DxEx.html こんなサイトがあったんですね。クリティカルセクションってなんだろう。

Nagarei commented 9 years ago

WinAPIではmutexよりもクリティカルセクションを使った同期の方が早い模様。 https://msdn.microsoft.com/ja-jp/library/cc429052.aspx std::mutexは内部でどっちを使ってるんだろう...。 ただ、どうせDxLib関連の部分でマルチスレッドなんて誰も使わないので環境依存の少ないstd::mutexで実装します。

Nagarei commented 9 years ago

各クラスの説明のところにスレッドセーフ保障についてどのレベルで提供するかを書く事を提案します。

  1. スレッドセーフ(DxLib関係なし)
  2. C++11スレッドセーフ(DxLib関係なし)
  3. スレッドセーフ(DxLibがスレッドセーフの場合)
  4. C++11スレッドセーフ(DxLibがスレッドセーフの場合)
  5. スレッドアンセーフ

こんな感じでしょうか? C++11スレッドセーフは#4の僕の書き込みの「全てが読み込み操作ならば排他制御は不要である」のことを指しています。

yumetodo commented 9 years ago

それがいいと思います。