トランザクション メモリ

トランザクション メモリは、データベース理論からのトランザクションの考え方に基づいています。トランザクション メモリにより、スレッドの処理が非常に簡単になります。それには2つの理由があります。データ競合とデッドロックがなくなります。トランザクションは構成可能です。

トランザクションは、プロパティ A を持つアクションです トミシティ、C 一貫性、 孤独、そして D 耐久性(ACID)。永続性を除いて、すべてのプロパティは C++ のトランザクション メモリに対して保持されます。したがって、残りの短い質問は 3 つだけです。

ACI(D)

いくつかのステートメントで構成されるアトミック ブロックのアトミック性、一貫性、および分離とは何ですか?

atomic{
 statement1;
 statement2;
 statement3;
}
  • 原子性: ブロックのすべてのステートメントが実行されるか、まったく実行されません。
  • 一貫性: システムは常に一貫した状態にあります。すべてのトランザクションが合計注文を構成します。
  • 隔離: 各トランザクションは、他のトランザクションから完全に分離して実行されます。

これらのプロパティはどのように保証されますか?トランザクションは、その初期状態を覚えています。その後、トランザクションは同期なしで実行されます。実行中に競合が発生した場合、トランザクションは中断され、初期状態に置かれます。このロールバックにより、トランザクションがもう一度実行されます。トランザクションの終了時にトランザクションの初期状態が保持されている場合でも、トランザクションはコミットされます。

トランザクションは、初期状態が保持されている場合にのみコミットされる一種の投機的アクティビティです。これは楽観的なアプローチであるミューテックスとは対照的です。トランザクションは同期なしで実行されます。初期状態との競合が発生しない場合にのみ公開されます。ミューテックスは悲観的なアプローチです。最初に、ミューテックスは、他のスレッドがクリティカル領域に入ることができないことを保証します。スレッドは、ミューテックスの排他的所有者である場合にのみクリティカル領域に入ります。したがって、他のすべてのスレッドはブロックされます。

C++ は、同期ブロックとアトミック ブロックの 2 つのフレーバーでトランザクション メモリをサポートします。

トランザクション メモリ

今までは、取引についてしか書いていませんでした。いいえ、同期ブロックとアトミック ブロックについて、より具体的に書きます。どちらも他方にカプセル化できます。具体的に言うと、同期ブロックはトランザクションセーフでないコードを実行できるため、アトミック ブロックではありません。これは、元に戻すことができないコンソールへの出力のようなコードである可能性があります。これが、同期ブロックがしばしばリラックスと呼ばれる理由です。

同期ブロック

同期ブロックは、グローバル ロックによって保護されているかのように動作します。これは、同期されたすべてのブロックが全体の順序に従うことを意味します。したがって、同期ブロックに対するすべての変更は、次の同期ブロックで使用できます。同期ブロック間に同期関係があります。同期ブロックはグローバル ロックによって保護されているように動作するため、デッドロックを引き起こすことはありません。従来のロックはメモリ領域を明示的なスレッドから保護しますが、同期ブロックのグローバル ロックはすべてのスレッドから保護します。これが、次のプログラムが明確に定義されている理由です:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// synchronized.cpp

#include <iostream>
#include <vector>
#include <thread>

int i= 0;

void increment(){
 synchronized{ 
 std::cout << ++i << " ,";
 }
}

int main(){
 
 std::cout << std::endl;
 
 std::vector<std::thread> vecSyn(10);
 for(auto& thr: vecSyn)
 thr = std::thread([]{ for(int n = 0; n < 10; ++n) increment(); });
 for(auto& thr: vecSyn) thr.join();
 
 std::cout << "\n\n";
 
}

7 行目の変数 i はグローバル変数であり、同期ブロック内の操作はトランザクションセーフではありませんが、プログラムは明確に定義されています。 i と std::cout へのアクセスは完全な順序で行われます。これは同期ブロックによるものです。

プログラムの出力はそれほどスリル満点ではありません。 i の値は、コンマで区切られて昇順で書き込まれます。完全を期すためにのみ。

データ競合はどうですか?同期ブロックでそれらを持つことができます。わずかな変更のみが必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// nonsynchronized.cpp

#include <chrono>
#include <iostream>
#include <vector>
#include <thread>

using namespace std::chrono_literals;

int i= 0;

void increment(){
 synchronized{ 
 std::cout << ++i << " ,";
 std::this_thread::sleep_for(1ns);
 }
}

int main(){
 
 std::cout << std::endl;
 
 std::vector<std::thread> vecSyn(10);
 std::vector<std::thread> vecUnsyn(10);
 
 for(auto& thr: vecSyn)
 thr = std::thread([]{ for(int n = 0; n < 10; ++n) increment(); });
 for(auto& thr: vecUnsyn)
 thr = std::thread([]{ for(int n = 0; n < 10; ++n) std::cout << ++i << " ,"; });
 
 for(auto& thr: vecSyn) thr.join();
 for(auto& thr: vecUnsyn) thr.join();
 
 std::cout << "\n\n";
 
}

データ競合を観察するために、同期ブロックを 1 ナノ秒間スリープさせます (15 行目)。同時に、同期ブロックを使用せずに std::cout にアクセスします (29 行目)。したがって、グローバル変数 i をインクリメントする 10 個のスレッドを起動します。出力は問題を示しています。

出力の問題を赤い丸で囲みました。これらは、std::cout が少なくとも 2 つのスレッドによって同時に使用されるスポットです。 C++11 標準は、文字がアトミックな方法で書き込まれることを保証しますが、これは光学的な問題のみです。さらに悪いことに、変数 i は少なくとも 2 つのスレッドによって書き込まれます。これはデータ競合です。したがって、プログラムは未定義の動作をします。プログラムの出力を注意深く見ると、103 が 2 回書き込まれていることがわかります。

同期ブロックの合計順序は、アトミック ブロックにも適用されます。

アトミック ブロック

同期ブロックではトランザクションの安全でないコードを実行できますが、アトミック ブロックでは実行できません。アトミック ブロックは、atomic_noexcept、atomic_commit、atomic_cancel の形式で使用できます。 3 つのサフィックス _noexcept、_commit、および _cancel は、アトミック ブロックが例外を管理する方法を定義します。

  • atomic_noexcept: 例外がスローされると、std::abort が呼び出され、プログラムが中止されます.
  • atomic_cancel: デフォルトの場合、std::abort が呼び出されます。トランザクションの終了の原因となるトランザクションセーフな例外がスローされた場合、それは成立しません。この場合、トランザクションはキャンセルされ、初期状態に置かれ、例外がスローされます。
  • atomic_commit: 例外がスローされた場合、トランザクションは正常にコミットされます。

トランザクション セーフの例外:

transaction_safe コードと transaction_unsafe コード

関数を transaction_safe として宣言するか、transaction_unsafe 属性をアタッチすることができます。

int transactionSafeFunction() transaction_safe;

[[transaction_unsafe]] int transactionUnsafeFunction();

transaction_safe は関数の型の一部です。しかし、transaction_safe とはどういう意味ですか? transaction_safe 関数は、提案 N4265 によると、transaction_safe 定義を持つ関数です。これは、次のプロパティがその定義に適用されない場合に当てはまります。

  • 揮発性のパラメータまたは揮発性の変数があります。
  • 安全でないトランザクション ステートメントが含まれています。
  • 関数が、揮発性の非静的メンバーを持つクラスのコンストラクタまたはデストラクタを本体で使用する場合。

もちろん、transaction_unsafe という用語を使用しているため、この transaction_safe の定義は十分ではありません。プロポーザル N4265 を読んで、transaction_unsafe の意味に対する答えを得ることができます。

次は?

次の投稿は fork-join パラダイムについてです。具体的には、タスクブロックについてです。