この記事では主にC#スレッド同期の関連知識を紹介します。非常に優れた参考値です。以下のエディターで見てみましょう
マルチスレッドの内容は大きく 2 つの部分に分かれており、専用、スレッド プール、タスク、並列、PLINQ、を介して実行できます。など、ここでは作業スレッドと IO スレッドも関係しています。2 つ目は、スレッド同期の問題です。
「C# 経由の CLR」の内容を学習することで、スレッド同期のより明確なアーキテクチャを形成しました。マルチスレッドでのスレッド同期を実現するのは、スレッド同期構造です。この構造は 2 つのカテゴリに分類され、1 つは基本的な構造です。 、 1 つはハイブリッド構造です。いわゆるプリミティブは、コードで使用される最も単純な構成要素です。基本的な構造は 2 つのカテゴリに分かれており、1 つはユーザー モード、もう 1 つはカーネル モードです。ハイブリッド構造は、内部でプリミティブ構造を使用するユーザー モードとカーネル モードを使用します。ユーザー モードとカーネル モードにはそれぞれ長所と短所があり、ハイブリッド構造は長所と短所のバランスをとるため、そのモードを使用するための特定の戦略があります。 2. デメリットを回避するための設計。以下は、スレッド同期アーキテクチャ全体のリストです
プリミティブ
1.1 ユーザーモード
1.1.1 volatile
1.1.2 インターロック
1.2 カーネルモード
1.2.1
1.2.2 ManualResetイベントおよび AutoResetEvent 1.2.3 セマフォ 1.2.4 ミューテックスミキシング
2.1 さまざまなスリム 2.2 モニター 2.3 MethodImplAttribute および SynchronizationAttributeReaderWriterLock 2.5 Barier(使用頻度は低い) 2.6 CoutdownEvent(使用量を減らしてください) スレッド同期の問題の原因から始めましょう。メモリ内に整数 上記のリソース共有の問題に対処するために、さまざまな方法がよく使用されます。それらを 1 つずつ紹介していきます。まず、プリミティブ構造のユーザー モードについて説明します。ユーザー モードの利点は、一連の CPU 命令によって調整されるため、実行が比較的高速であることと、それによって引き起こされるブロッキングです。オペレーティング システムに関する限り、このスレッドは常に実行されており、ブロックされたことはありません。欠点は、そのようなスレッドの実行を停止できるのはシステム カーネルだけであることです。一方、スレッドはブロックされずに回転しているため、CPU 時間も占有し、CPU 時間の無駄が発生します。 1 つ目は、原始的なユーザー モード構造の volatile 構造です。この構造に関するインターネット上の多くの理論は、CPU が指定されたフィールド (フィールド、つまり変数) をメモリから読み取り、毎回メモリに書き込むというものです。時間。ただし、これはコンパイラーのコードの最適化と関係があります。まず次のコードを見てみましょう
public class StrageClass { vo int mFlag = 0; int mValue = 0; public void Thread1() { mValue = 5; mFlag = 1; } public void Thread2() { if (mFlag == 1) Console.WriteLine(mValue); } }
2. 出力5。ただし、CSC コンパイラが IL 言語にコンパイルするとき、または JIT がマシン語にコンパイルするとき、メソッド Thread1 ではコードの最適化が実行され、コンパイラは 2 つのフィールドに値を割り当てることは重要ではないと考え、実行されるだけです。単一スレッドの観点からは、マルチスレッドの問題がまったく考慮されていないため、2 行のコードの実行順序が混乱し、最初に mFlag に 1 の値が割り当てられる可能性があります。 、次に mValue に値 5 が割り当てられ、3 番目につながります。その結果、0 が出力されます。残念ながら、この結果をテストすることはできませんでした。この現象の解決策は volatile コンストラクトです。このコンストラクトを使用すると、このコンストラクトを使用してフィールドで読み取り操作が実行されるたびに、その操作が元のコード シーケンスで最初に実行されることが保証されます。この構造を使用してフィールドに書き込むと、その操作は元のコード シーケンスの最後に実行されることが保証されます。
現在 volatile を実装する構造体は 3 つあります。1 つは Thread の 2 つの
staticメソッド VolatileRead と VolatileWrite です。MSND の分析は次のとおりです
Thread.VolatileRead はフィールド値を読み取ります。 この値は、プロセッサの数やプロセッサ キャッシュの状態に関係なく、コンピュータのプロセッサによって書き込まれた最新の値です。
Thread.VolatileWrite フィールドに値を即座に書き込み、その値がコンピューター内のすべてのプロセッサに表示されるようにします。 在多处理器系统上, VolatileRead 获得由任何处理器写入的内存位置的最新值。 这可能需要刷新处理器缓存;VolatileWrite 确保写入内存位置的值立即可见的所有处理器。 这可能需要刷新处理器缓存。 即使在单处理器系统上, VolatileRead 和 VolatileWrite 确保值为读取或写入内存,并不缓存 (例如,在处理器寄存器中)。 因此,您可以使用它们可以由另一个线程,或通过硬件更新的字段对访问进行同步。 从上面的文字看不出他和代码优化有任何关联,那接着往下看。 volatile关键字则是volatile构造的另外一种实现方式,它是VolatileRead和VolatileWrite的简化版,使用 volatile 修饰符对字段可以保证对该字段的所有访问都使用 VolatileRead 或 VolatileWrite。MSDN中对volatile关键字的说明是 volatile 关键字指示一个字段可以由多个同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。 从这里可以看出跟代码优化有关系了。而纵观上面的介绍得出两个结论: 1.使用了volatile构造的字段读写都是直接对内存操作,不涉及CPU寄存器,使得所有线程对它的读写都是同步,不存在脏读了。读操作是原子的,写操作也是原子的。 2.使用了volatile构造修饰(或访问)字段,它会严格按照代码编写的顺序执行,读操作将会在最早执行,写操作将会最迟执行。 最后一个volatile构造是在.NET Framework中新增的,里面包含的方法都是Read和Write,它实际上就相当于Thread的VolatileRead 和VolatileWrite 。这需要拿源码来说明了,随便拿一个Volatile的Read方法来看 而再看看Thraed的VolatileRead方法 另一个用户模式构造是Interlocked,这个构造是保证读和写都是在原子操作里面,这是与上面volatile最大的区别,volatile只能确保单纯的读或者单纯的写。 为何Interlocked是这样,看一下Interlocaked的方法就知道了 就随便拿其中一个方法Add(ref int,int)来说(Increment和Decrement这两个方法实际上内部调用了Add方法),它会先读到第一个参数的值,在与第二个参数求和后,把结果写到给第一参数中。首先这整个过程是一个原子操作,在这个操作里面既包含了读,也包含了写。至于如何保证这个操作的原子性,估计需要查看Rotor源码才行。在代码优化方面来说,它确保了所有写操作都在Interlocked之前去执行,这保证了Interlocked里面用到的值是最新的;而任何变量的读取都在Interlocked之后读取,这保证了后面用到的值都是最新更改过的。 CompareExchange方法相当重要,虽然Interlocked提供的方法甚少,但基于这个可以扩展出其他更多方法,下面就是个例子,求出两个值的最大值,直接抄了Jeffrey的源码 查看上面代码,在进入循环之前先声明每次循环开始时target的值,在求出最值之后,核对一下target的值是否有变化,如果有变化则需要再记录新值,按照新值来再求一次最值,直到target不变为止,这就满足了Interlocked中所说的,写都在Interlocked之前发生,Interlocked往后就能读到最新的值。 基元内核模式 カーネル モードは、オペレーティング システムのカーネル オブジェクト に依存してスレッド同期の問題を処理します。まず欠点について説明します。速度は比較的遅いです。理由は 2 つあり、1 つはオペレーティング システムのカーネル オブジェクトによって実装されており、オペレーティング システム内での調整が必要であるためです。もう 1 つは、AppDomain を理解するとわかるようになります。アクセスされたオブジェクトが現在の AppDomain にない場合、値によってマーシャリングされるか、reference によってマーシャリングされます。アンマネージド リソースのこの部分は参照によってマーシャリングされ、パフォーマンスに影響を与えることが確認されています。上記の 2 つの点を組み合わせると、カーネル モードの欠点がわかります。ただし、次のような利点もあります。 1. リソースを待機するときにスレッドは「スピン」せずにブロックされます。これにより CPU 時間が節約され、このブロックに対してタイムアウト値を設定できます。 2. Windows スレッドと CLR スレッドの同期が実現でき、異なるプロセスのスレッドも同期できます (前者は未体験ですが、後者についてはセマフォに境界値リソースがあることが知られています)。 3. セキュリティ設定を適用して、承認されたアカウントのアクセスを禁止できます (何が起こっているのかわかりません)。 カーネルモードのすべてのオブジェクトの基本クラスは WaitHandle です。カーネルモードのすべてのクラスは次のように階層化されています WaitHandle EventWaitHandle AutoResetEvent イベント セマフォ ミューテックス WaitHandle は、アンマネージ オブジェクトを参照によってマーシャリングする MarshalByRefObject を継承します。 WaitHandle には主にさまざまな Wait メソッドが含まれています。Wait メソッドが呼び出されると、シグナルを受信する前にブロックされます。 WaitOne はシグナルを待機し、WaitAny(WaitHandle[] waitHandles) は任意の waitHandle のシグナルを受信し、WaitAll(WaitHandle[] waitHandles) はすべての waitHandle のシグナルを待機します。これらのメソッドには、タイムアウトを設定できるバージョンがあります。他のカーネル モード構造にも同様の Wait メソッドがあります。 EventWaitHandle は内部的にブール値を保持しており、ブール値が false の場合、Wait メソッドはスレッドをブロックし、ブール値が true になるまでスレッドは解放されません。このブール値を操作するメソッドには Set() と Reset() があります。前者はブール値を true に設定し、後者はブール値を false に設定します。これはスイッチに相当します。Reset を呼び出した後、スレッドは Wait を実行して一時停止され、Set が実行されるまで再開されません。これには 2 つのサブクラスがあり、どちらも同様の方法で使用されます。違いは、AutoResetEvent は Set を呼び出した後に自動的に Reset を呼び出すため、スイッチはすぐに閉じた状態に戻りますが、ManualResetEvent は手動で Set を呼び出してスイッチを閉じる必要があることです。これにより、通常、AutoResetEvent は解放されるたびに 1 つのスレッドの通過が許可されますが、ManualResetEvent では手動で Reset を呼び出す前に複数のスレッドの通過が許可されます。 セマフォは内部的に整数を保持します。セマフォオブジェクトを構築するとき、WaitOne が呼び出されるたびにセマフォは 1 ずつ増加します。最大値に加算されると、 Release が呼び出されると、1 つ以上のセマフォが解放されます。このとき、ブロックされていたスレッドが解放されます。これは、プロデューサーとコンシューマーの問題と一致しています。プロデューサーが製品キュー に製品を追加し続けると、キューがいっぱいになると、セマフォがいっぱいになるのと同じになり、プロデューサーはブロックされます。消費者が製品を消費すると、Release は製品キュー内のスペースを解放します。この時点で、製品を保管するスペースがない生産者は、製品キューに製品を保管する作業を再開できます。 再帰という 2 つの概念があります。これは、追加のカプセル化を除いて、前の構成に依存するだけでは実現できません。 ハイブリッド建設 上記のプリミティブ構造は最も単純な実装方法を使用しています。ユーザー モードはユーザー モードよりも高速ですが、カーネル モードではこの問題は解決されますが、パフォーマンスの低下が発生します。それぞれに長所と短所があります。ハイブリッド構造は、両方の利点を組み合わせたもので、内部的には特定の戦略を通じて適切なタイミングでユーザー モードを使用し、別の状況ではカーネル モードを使用します。しかし、これらの判断の層はメモリのオーバーヘッドをもたらします。マルチスレッド同期には完璧な構造はありません。それぞれの構造には長所と短所があり、特定のアプリケーション シナリオと組み合わせることで最適な構造が得られます。それは、特定のシナリオに従ってメリットとデメリットを比較検討できるかどうかにかかっています。 さまざまな Slim サフィックス付きクラス System.Threading 名前空間 には、Slim サフィックスで終わるいくつかのクラスがあります: ManualResetEventSlim、SemaphoreSlim、ReaderWriterLockSlim。最後のクラスを除いて、他の 2 つはプリミティブ カーネル モードで同じ構造を持っていますが、これら 3 つのクラスは元の構造の簡略化されたバージョンであり、特に最初の 2 つは元のクラスと同じように使用されますが、試してみてください。オペレーティング システムのカーネル オブジェクトの使用を回避し、軽量化の効果を実現します。たとえば、カーネル コンストラクト ManualResetEvent は SemaphoreSlim で使用されますが、このコンストラクトは遅延によって初期化され、必要な場合以外は使用されません。 ReaderWriterLockSlimについては後ほど紹介します。 監視とロック、lock キーワードはマルチスレッド同期を実現する最もよく知られた手段なので、コードから始めましょう この方法は非常に単純で、実用的な意味はありません。コンパイラはこのコードを何にコンパイルしますか? 次のように IL を見ると、try...finally ステートメント ブロック、Monitor.Enter メソッドと Monotor.Exit メソッドが IL コード内にあることがわかります。次に、コードを変更して再度コンパイルし、IL コードは似ていますが、実際には、lock ステートメント ブロックと同等のコードは次のとおりです。以下の通り では、lockは基本的にMonitorを呼び出すので、Monitorはどのようにしてオブジェクトをロックし、スレッド同期を達成するのでしょうか?マネージド ヒープ内のすべてのオブジェクトには 2 つの固定メンバーがあり、1 つはオブジェクト型のポインターを指し、もう 1 つはスレッド同期ブロック を指していることがわかります。このインデックスは、同期ブロック配列 Monitor には、Wait メソッドと Pulse メソッドのペアもあります。前者の場合、ロックを取得したスレッドが一時的にロックを解放し、現在のスレッドがブロックされて待機キューに置かれる可能性があります。他のスレッドが Pulse メソッドを呼び出すまで、スレッドは待機キューから待機キューに入れられます。次回ロックが解放されると、状況に応じて再度ロックを取得できる可能性があります。待機列にいます。 ReaderWriterLock は、従来のロック キーワード (Monitor の Enter および Exit に相当) です。共有リソースに対するロックは、一度ロックされると、他のリソースはまったくアクセスできなくなります。 而ReaderWriterLock对互斥资源的加的锁分读锁与写锁,类似于数据库中提到的共享锁和排他锁。大致情况是加了读锁的资源允许多个线程对其访问,而加了写锁的资源只有一个线程可以对其访问。两种加了不同缩的线程都不能同时访问资源,而严格来说,加了读锁的线程只要在同一个队列中的都能访问资源,而不同队列的则不能访问;加了写锁的资源只能在一个队列中,而写锁队列中只有一个线程能访问资源。区分读锁的线程是否在于统一个队列中的判断标准是,本次加读锁的线程与上次加读锁的线程这个时间段中,有否别的线程加了写锁,没没别的线程加写锁,则这两个线程都在同一个读锁队列中。 ReaderWriterLockSlim和ReaderWriterLock类似,是后者的升级版,出现在.NET Framework3.5,据说是优化了递归和简化了操作。在此递归策略我尚未深究过。目前大概列举一下它们通常用的方法 ReaderWriterLock常用的方法 Acqurie或Release ReaderLock或WriteLock 的排列组合 UpGradeToWriteLock/DownGradeFromWriteLock 用于在读锁中升级到写锁。当然在这个升级的过程中也涉及到线程从读锁队列切换到写锁队列中,因此需要等待。 ReleaseLock/RestoreLock 释放所有锁和恢复锁状态 ReaderWriterLock实现IDispose接口,其方法则是以下模式 TryEnter/Enter/Exit ReadLock/WriteLock/UpGradeableReadLock CoutdownEvent比较少用的混合构造,这个跟Semaphore相反,体现在Semaphore是在内部计数(也就是信号量)达到最大值的时候让线程阻塞,而CountdownEvent是在内部计数达到0的时候才让线程阻塞。其方法有Add(ref int,int)// 调用ExternAdd 外部方法
CompareExchange(ref Int32,Int32,Int32)//1与3是否相等,相等则替换2,返回1的原始值
Decrement(ref Int32)//递减并返回 调用add
Exchange(ref Int32,Int32)//将2设置到1并返回
Increment(ref Int32)//自增 调用add
AddCount //计数递增;
Signal //计数递减;
Reset //计数重设为指定或初始;
Wait //当且仅当计数为0才不阻塞,否则就阻塞。
Barrier也是一个比较少用的混合构造,用于处理多线程在分步骤的操作中协作问题。它内部维护着一个计数,该计数代表这次协作的参与者数量,当不同的线程调用SignalAndWait的时候会给这个计数加1并且把调用的线程阻塞,直到计数达到最大值的时候,才会释放所有被阻塞的线程。假设还是不明白的话就看一下MSND上面的示例代码
这里给Barrier初始化的参与者数量是3,同时每完成一个步骤的时候会调用委托,该方法是输出count的值步骤索引。参与者数量后来增加了两个又减少了一个。每个参与者的操作都是相同,给count进行原子自增,自增完则调用SgnalAndWait告知Barrier当前步骤已完成并等待下一个步骤的开始。但是第三次由于回调方法里抛出了一个异常,每个参与者在调用SignalAndWait的时候都会抛出一个异常。通过Parallel开始了一个并行操作。假设并行开的作业数跟Barrier参与者数量不一样就会导致在SignalAndWait会有非预期的情况出现。
接下来说两个Attribute,这个估计不算是同步构造,但是也能在线程同步中发挥作用
MethodImplAttribute这个Attribute适用于方法的,当给定的参数是MethodImplOptions.Synchronized,它会对整个方法的方法体进行加锁,凡是调用这个方法的线程在没有获得锁的时候就会被阻塞,直到拥有锁的线程释放了才将其唤醒。对静态方法而言它就相当于把该类的类型对象给锁了,即lock(typeof(ClassType));对于实例方法他就相当于把该对象的实例给锁了,即lock(this)。最开始对它内部调用了lock这个结论存在猜疑,于是用IL编译了一下,发现方法体的代码没啥异样,查看了一些源码也好无头绪,后来发现它的IL方法头跟普通的方法有区别,多了一个synchronized
于是网上找各种资料,最后发现"junchu25"的博客[1][2]里提到用WinDbg来查看JIT生成的代码。
调用Attribute的
调用lock的
对于用这个Attribute实现的线程同步连Jeffrey都不推荐使用。
System.Runtime.Remoting.Contexts.SynchronizationAttribute この属性をクラス定義に追加し、ContextBoundOject のクラスを継承します。これを MethodImplAttribute と比較します。スレッドがこのクラスのメソッドを呼び出すとき、ロックが取得できない場合、スレッドはブロックされます。本質的にロックを呼び出すと言われていますが、このステートメントを検証するのはさらに困難です。AppDomain とスレッド コンテキストも関係する国内リソースはほとんどありません。最後のコアは SynchronizedServerContextSink クラスによって実装されます。 AppDomain については別の記事で紹介します。しかし、ここで少し話したいと思います。メモリにはスレッド スタックとヒープ メモリがあると考えていました。これは単なる非常に基本的な分割であり、ヒープ メモリも複数の AppDomain に分割されています。各 AppDomain に少なくとも 1 つのコンテキスト。各オブジェクトは AppDomain 内のコンテキストに属します。 AppDomain 間のオブジェクトには直接アクセスできません。値によってマーシャリングするか (呼び出し元の AppDomain にオブジェクトをディープ コピーするのと同じ)、参照によってマーシャリングする必要があります。参照によるマーシャリングの場合、クラスは MarshalByRefObject を継承する必要があります。このクラスを継承するオブジェクトを呼び出す場合、クラス自体は呼び出されず、プロキシ経由で呼び出されます。次に、値操作によるクロスコンテキスト マーシャリングも必要です。通常、構築されるオブジェクトはプロセスのデフォルト AppDomain の下のデフォルト コンテキストにあり、SynchronizationAttribute 属性を使用するクラスのインスタンスは別のコンテキストに属します。また、ContextBoundObject 基本クラスを継承するクラスは、プロキシを使用してコンテキストにアクセスすることによって、コンテキストを越えてオブジェクトにアクセスします。参照によるオブジェクトのマーシャリングは、オブジェクト自体にはアクセスしません。コンテキストを越えてオブジェクトにアクセスするかどうかについては、RemotingServices.IsObjectOutOfContext(obj) メソッドを通じて判断できます。 SynchronizedServerContextSink は、mscorlib の内部クラスです。スレッドがクロスコンテキスト オブジェクトを呼び出すと、その呼び出しは SynchronizedServerContextSink によって WorkItem オブジェクトにカプセル化されます。これは、SynchronizedServerContextSink リクエストの SynchronizationAttribute 属性によって、複数の WorkItem 実行リクエストがあるかどうかに基づいて決定されます。処理された WorkItem はすぐに実行されますか? それとも、順番に実行するために先入れ先出し WorkItem キューに配置されますか? このキューは、キュー メンバーがキューに出入りするとき、または属性によって決定されるときに実行されます。 WorkItem をすぐに実行するには、ロックを取得する必要があります。ロックされたオブジェクトは、この WorkItem のキューでもあります。これにはいくつかのクラスの相互作用が含まれますが、まだ完全に理解できていません。明確に分析した後、さらに追加する予定です。ただし、この属性を通じて達成されるスレッド同期は、主にパフォーマンスの損失とロック範囲が比較的大きいため、強い直感に基づいて推奨されません。
以上がC#マルチスレッドにおけるスレッド同期の詳しい解説(画像と文章)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。