2016-07-02 4 views
3

ティム・ハリスは言った:ロックベースのプログラムが正しいスレッドセーフフラグメントを構成しないのはなぜですか?

https://en.wikipedia.org/wiki/Software_transactional_memory#Composable_operations

をおそらく最も基本的な反論[...]は、ロックベース プログラムが構成されていないということです:組み合わせたときに正しいフラグメントが失敗することがあります。 の例では、スレッドセーフの挿入と削除を伴うハッシュテーブルを考えて、 オペレーションを削除してください。テーブル t1から1つのアイテムAを削除し、それをテーブルt2に挿入するとします。中間状態( のどちらのテーブルにも項目が含まれていない)は、他のスレッドから見えてはならない。 ハッシュテーブルの実装者がこの必要性を予期しない限り、 は単にこの要件を満たす方法ではありません。 [...]簡潔に言えば、 の操作は個別に正しい(挿入、削除)ことはできません より大きな正しい操作に構成されています。 - Tim Harris他、 「合成可能メモリトランザクション」、セクション2:背景、ページ2 [6]

この意味は?私は2つのハッシュマップstd::unordered_mapと2つのミューテックスstd::mutex(各ハッシュマップに1つ)を持っている場合は

その後、私は単純にロックすることができ、その両方:http://ideone.com/6RSNyN

#include <iostream> 
#include <string> 
#include <mutex> 
#include <thread> 
#include <chrono> 
#include <unordered_map> 

std::unordered_map<std::string, std::string> map1 ({{"apple","red"},{"lemon","yellow"}}); 
std::mutex mtx1; 

std::unordered_map<std::string, std::string> map2 ({{"orange","orange"},{"strawberry","red"}}); 
std::mutex mtx2; 

void func() { 
    std::lock_guard<std::mutex> lock1(mtx1); 
    std::lock_guard<std::mutex> lock2(mtx2); 

    std::cout << "map1: "; 
    for (auto& x: map1) std::cout << " " << x.first << " => " << x.second << ", "; 
    std::cout << std::endl << "map2: "; 
    for (auto& x: map2) std::cout << " " << x.first << " => " << x.second << ", "; 
    std::cout << std::endl << std::endl; 

    auto it1 = map1.find("apple"); 
    if(it1 != map1.end()) { 
     auto val = *it1; 
     map1.erase(it1); 
     std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(1000)); 
     map2[val.first] = val.second; 
    } 
} 

int main() 
{ 
    std::thread t1(func); 
    std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(500)); 
    std::thread t2(func); 
    t1.join(); 
    t2.join(); 

    return 0; 
} 

私は自分自身、スレッドセーフなハッシュマップmy_unordered_mapを実装する場合その後、私はそのようなことを実施します:

template<typename key, template val> 
class my_unordered_map { 
    std::recursive_mutex mtx_ptr; 
    void lock() { mtx_ptr->lock(); } 
    void unlock() { mtx_ptr->unlock(); } 
    template<typename mutex_type> friend class std::lock_guard; 
public: 
// .. all required public methods which lock recursive mutex before do anything 
    void insert(key k, val v) { std::lock_guard<std::recursive_mutex> lock(mtx); /* do insert ... */ } 
    // ... 
}; 

そして、このような使用します。

my_unordered_map<std::string, std::string> map1 ({{"apple","red"},{"lemon","yellow"}}); 

my_unordered_map<std::string, std::string> map2 ({{"orange","orange"},{"strawberry","red"}}); 

void func() { 
    std::lock_guard<my_unordered_map> lock1(map1); 
    std::lock_guard<my_unordered_map> lock2(map2); 

    // work with map1 and map2 
    // recursive_mutex allow multiple locks in: lock1(map1) and map1->at(key) 
} 

同様にmap1とmap2の両方に対して、スレッドセーフなコードと完全にシーケンシャルな一貫性が得られます。

しかし、これはどのような場合に言われましたか?

おそらく最も基本的な反論[...]は、ロックベース プログラムが構成されていないということです:組み合わせたときに正しいフラグメントが失敗することがあります。

+0

おそらく、操作がトランザクションで失敗するということです。map2がまだキーkを含んでいない場合のみ、map1からキーkを抽出します。トランザクションは両方のmutexを一緒にロックする必要があるため、マップごとに1つのプライベートミューテックスではできません。 –

答えて

4

あなた自身のプログラムは完全に上質です。

別のプログラムは、別のスレッドで別のタスクのために、

void func_other() { 
    std::lock_guard<my_unordered_map> lock2(map2); 
    std::lock_guard<my_unordered_map> lock1(map1); 

    // work with map1 and map2 
} 

のようなものはやはり、これは自分自身で、結構です代わりに使用する場合があります。

しかし、両方のプログラムを同時に実行すると、デッドロックが発生する可能性があります。スレッド1はマップ1をロックし、スレッド2はマップ2をロックし、両方のスレッドの次のロックは永遠に待機します。

私たちは2つのプログラムを簡単に構成することはできません。

ロックの代わりにSTMを使用すると、そのような種類の構成が(あるパフォーマンス価格で)常に許可されます。

+0

ありがとうございます。はい、**ロックベース**のコンテナは、ロックの順序を変更するとデッドロックが発生する可能性があります。つまり、「組み合わせて失敗する可能性があります」とは、この結合コードを書き換えて失敗を回避できます。しかし、2つの**ロックフリーのコンテナは、一般に2つのテーブルの整合性をとることができません。私。 STMだけが、2つのテーブルの一貫性に対する失敗がないことを保証することができます。しかし、代わりにSTMをデッドロックすると、トランザクション・システムはトランザクション・ロールバック(内部デッドロックや更新競合の場合)として、ソフトウェアで処理できるトランザクション・コミットの失敗を生成する可能性がありますか? – Alex

+1

@Alexはい。ほとんどのSTMアプローチでは、競合があっても1つのトランザクションがコミットされ、別のトランザクションはロールバックされる可能性があります。 STMエンジンはロールバックされたトランザクションを再試行できます。 – chi

+0

I.私は2つの方法があります:** 1。**私はSTMを完全な実行まで制御されたトランザクションループで使用するべきです。 ** 2 ** **ロックベースのプログラムでデッドロックを解決する必要があります。例えば、mutex.try_lock()をループの中で必要なmutexごとに使用したり、 'std :: this_thread ::特定の時間(1 usecまたは1 msec、...)の後に 'mutex.try_lock()== false'が指定された場合、' throw my_roll_back_transaction(); '例外をスローすると、それをキャッチし、すべての変更をロールバックし、ループを再試行していくつかのテーブルを一緒に変更します。 – Alex

4

挿入と消去のスレッドセーフアトミック操作は、通常、それらの内部にミューテックスを隠します。これにより、ロックしないでアクセスできなくなります。

あなたのケースでは、代わりにミューテックスを公開します。これには、すべてのユーザが適切にmutexを争奪する必要があります。

mutexesに完全にアクセスすると、安全に中間の状態を見ることができます。std::lockを使用しないためコードが失敗するため、mutexのロック順序がグローバルに一貫しています。異なるロック順序。

この種の問題は、あなたのトランザクションに必要なミューテックスを常に把握しておかなければならない場合、小さくて簡単に正しい部分を特定することができます。正しさは非ローカルになり、複雑さが爆発し、バグがたくさんあります。

+0

はい、 'std :: recursive_mutex mtx_ptr;'をプライベートメンバーとして非表示にしています。これは 'std :: lock_guard'だけが直接友人として使うことができます。そして、私は 'insert()'、 'erase()'、 'find()'のようなメソッドでこのmutexをRAII 'std :: lock_guard'でロックします。また、私は、find-erase-insert、2テーブル一貫性、などの多くの操作で 'std :: lock_guard'を使って手動でコンテナをロックすることを提案します。そして、ロックベースのプログラムの潜在的な問題は、違う順序のロックで – Alex

関連する問題