C++ロックについて考える


C++のマルチスレッドプログラミングは比較的複雑で、ピットが多く、問題が発生しにくいプログラミング分野であり、c++が大規模なプロジェクトを書く上で避けられない部分でもある.
ロックのかけ方はいろいろありますが、適用シーンもそれぞれ少し違いますが、原子化操作や通知など、異なる次元(または側面)によって、技術が楽観的ロックに属するか悲観的ロックに属するか、カーネルレベルかユーザーレベルか、linuxプラットフォームかwinプラットフォームか)によって多くのカテゴリに分けることができますが、その役割はマルチスレッドプログラムの安定した実行にサービスするため、一般的には「ロック」と総称されます.
例えば原子型(スレッド同時環境のタスクIDジェネレータclass idGeneratorのようなシーンを使用するには、内部のカウンタはstd::atomic_intのようなタイプを使用してスレッドの安全を保証する必要があります)、使用中にロックの操作がほとんど見られず、ロックの範疇にも組み込まれています.また、楽観ロック、スピンロックなど、多くの研究に値するロック分類の典型的な代表でもあります.ユーザ状態ロックなど.
最近同僚がstdについて話しています::shared_ptrはスレッドが安全かどうか.スレッドが安全だと信じていたので、コードで安心して使いました.しかし、真相を探るために、コードの面からこの問題を整理するには、よく研究しなければならない.幸いstlのコードが見つかりましたが、大体見た後、スレッドが安全だと確信しています.もちろん、私の「確信」は、相互間のコピーと移動における参照カウントメンテナンスの面にあります.実際のデータが格納されている場合は、セキュリティポリシーはありません.では、スレッドが安全ではないのではないでしょうか.
この質問はstd::shared_からすべきだと思います.ptrデータ構造は本質的に、C++の一連のスマートポインタが、様々な使用シーンでの元のポインタの代わりに使用されることを目的としています.std::shared_ptrに格納されているデータがスレッドセキュリティであるかどうかは、int*タイプの臨界リソースがスレッドセキュリティであるかどうかを考えてみましょう.答えは明らかで、そうではない.そのスレッドのセキュリティには、自分でロックしてメンテナンスする必要があります.
私がはっきり言ったかどうか分かりませんが、私はいつも下手で、実はstdについてです::shared_ptrのスレッドセキュリティの問題は、2つの面から考慮する必要があります.1つ目はstlが提供するこのツールクラスであり、そのスレッドセキュリティの考慮は主に引用カウントが正確かどうかです.もちろん、スレッドセキュリティです.2つ目は、ポインタとして指すコンテンツがスレッドが安全かどうかですが、ツールクラスはあなたのために何もしていません.
前のブログで実装されたttl cacheコードについてもう一度検討したが、やはりマルチスレッドの下の脆弱性が発見された.CheckInConstructor()と_CheckInDestructor()の場合、use_count()は必ずしも条件を満たす値が現れるとは限らない.この問題の修正は、スマートポインタの構造(コピー)によって参照カウントが変化するコードが構造関数の初期化リストにあり、参照カウントを取得するコードが関数体内にあるため、簡単なロック解除で解決されるものではありません.問題は少し厄介になります.
簡単に見えるように、demoを説明する簡単な抽象的な問題を書きました.
atomic_int g_count = 0;

template
class N { public: N(int p = 0) { (n % 2) ? (++g_count) : (--g_count); } };

class T
{
public:
	T() :n1(0), n2(0), n3(0), n4(0), n5(0), n6(0), n7(0), n8(0) { assert(g_count == 0); }
private:
	N<1> n1; N<2> n2; N<3> n3; N<4> n4; N<5> n5; N<6> n6; N<7> n7; N<8> n8;
};
単一スレッドでは、Tタイプコンストラクション関数のassertは永遠にトリガーされないが、マルチスレッドの同時環境では、あるスレッドが初期化リストn 1~n 8の構造中に別のスレッドがcpuタイムスライスを奪った場合、g_が適切に非対称に変化するcount、assertがトリガーされます.実験結果は断言を引き起こしやすく、テストコードは以下の通りである.
void test()
{
	int count = 10;
	std::vector<:thread> tv;
	tv.resize(count);
	for (int i = 0; i < count; ++i)
	{
		tv[i] = new std::thread([]() 
		{
			for (int j = 0; j < 1000; ++j)
			{
				T t;
				std::this_thread::sleep_for(std::chrono::milliseconds(10));
			}
		});
	}
	for (int k = 0; k < count; ++k)
	{
		if (tv[k])
		{
			tv[k]->join();
			delete tv[k];
		}
	}
}

この問題に対して,T構造関数のassertの前後にロックをかけることは何の役にも立たず,初期化リストと関数体内コードを同時に原子化する目的を達成することはできない(実際には初期化リストを単独で原子化することもできない).
しかし、私は运がずっと良いかもしれませんが、しばらく考えていて、放弃しようとして、やり直そうとした时、霊光が現れて、ありました!コードは次のように変更されました.
std::atomic_int g_count = 0;
std::mutex g_mutex;


template
class N { public: N(int p = 0) { (n % 2) ? (++g_count) : (--g_count); } };

template class OnlyLock { public: OnlyLock(LOCK& l) { l.lock(); } };
template class OnlyUnLock { public: OnlyUnLock(LOCK& l) { l.unlock(); } };

class T
{
public:
	T() :l(g_mutex), n1(0), n2(0), n3(0), n4(0), n5(0), n6(0), n7(0), n8(0) { assert(g_count == 0); OnlyUnLock<:mutex> l(g_mutex); }
private:
	OnlyLock<:mutex> l;
	N<1> n1; N<2> n2; N<3> n3; N<4> n4; N<5> n5; N<6> n6; N<7> n7; N<8> n8;
};

c++のクラスメンバーの初期化順序は宣言順序と一致するので、OnlyLock変数は他のすべてのメンバー変数の前に置けばよく、最後にコンストラクション関数体の最後尾でOnlyUnLockになります.
しかし、Tクラスにベースクラスがあり、ベースクラス構築関数で臨界リソース値を変更するとともに、Tクラス構築関数で値を取り、オブジェクト構築プロセス全体の原子性を保証する場合、上記のテクニックは、ベースクラスを変更しない限り、OnlyLockをベースクラスの最初のメンバー位置に移動することはできません.
さらにttl cacheを検討したところ,上記の変更は効果的であるはずであるが,性能問題を代行したり,より良い解決策があるかどうかを考慮すると,もう少しよく考えなければならないので,しばらくコード修正はない.
上記のロック処理について、また説明すべき点は、このような非RAIIのロック方式は一般的に使用を避けるべきであり、RAIIはプログラムの頑丈性をコード階層からより効果的に向上させることができ、例えばunlockを忘れたためにデッドロックをもたらすことはなく、同時に異常が発生し、外層に投げ出されたときに追加されたロックが自動的に解除されることを保証している.このRAIIのメカニズムは、scope_のような多くの同じタイプのシーンに適用されます.ptr.