[Linux] マルチスレッド --- スレッドの同期と相互排他 + 生産および消費モデル

人生はいつもこんなに苦しいものなのでしょうか?それとも子供の頃だけだったのでしょうか?- いつも

ここに画像の説明を挿入

記事ディレクトリ



1. スレッド相互排他

1. マルチスレッド共有リソースアクセスの安全性の低下

1.
現在、共有リソースのチケットがあると仮定し、このリソースに対して複数のスレッドで操作、つまりチケットの操作を実行したい場合、次の 2 つのコードの結果は異なります。また、上記のコードには、問題がありますが、次のコードには負の投票数があります。何が起こっているのでしょうか?
実際、この問題はマルチスレッドがスケジューラによってスケジュールされていることが原因で発生します。

ここに画像の説明を挿入

ここに画像の説明を挿入

2.
上記の問題を理解するには、実際のスレッドがスケジュールされる際にそのコンテキストが CPU のレジスタにロードされ、スレッドが切り替わる際にスレッドが切り替わるというスレッドのスケジューリングの特徴を知る必要があります。このとき、次回スレッドを切り替えるときにコンテキストデータを復元できるように、スレッドのコンテキストを保存する必要があります。
さらに、チケットなどの操作の場合、実際には少なくとも 3 つの対応するアセンブリ命令 (1. データの読み取り 2. データの変更 3. データの書き戻し) があり、スレッド関数は各スレッドのプライベート スタックにあることがわかっています。コピーは 1 つあります。上記の例では、複数のスレッドが同じスレッド関数を実行するため、このスレッド関数は確実にリエントラント状態になり、必ず複数のスレッドで実行されます。今日は、1 つの CPU (CPU がコアであり、プロセッサ チップは複数のコアを統合します) だけが現在のプロセスでスレッドをスケジュールしていると仮定します。その場合、スレッドは CPU スケジューリングの基本単位であるため、次のスレッドが存在します。切り替えが行われ、スレッドのコンテキストが保存され、CPU はプロセス内の別のスレッドをスケジュールします。

ここに画像の説明を挿入
3.
上記の原則を理解した後、usleep の機能も知る必要がありますが、if 分岐文の 1 行目に usleep を配置すると、投票数に問題があり、負の数が表示されます。主な理由は、usleep がスレッドを一時的にブロックする可能性があるためです。その後、CPU はスレッドをオフにして、代わりに他のスレッドを実行しますが、切り替えられたスレッドが再スケジュールされると、最後に実行したステートメントから下方向に実行され続けることに注意してください。 。
そのため、複数のスレッドが同時に分岐判定文に入ってブロックして待機する状況が発生し、チケットが 1 になったとして、この時点で残りのスレッドがスケジュールされ、すべてのスレッドが開始されます。チケットを実行する- -, - - その後、ループ条件を満たさないとスレッドが終了するので、4つのスレッドを作成すると、すでに投票数が0のときに3つのスレッドが減り続けるため、投票数はネガティブ。

ここに画像の説明を挿入

4.
問題を再現できるかどうかは、主に usleep と論理判定をチケットから分離することに依存します。その場合、if 論理判定の実行後、チケットの実行前にスレッドが切り替わる可能性があります。これが複数の場合に発生します。すべてのスレッドが再スケジュールされ、そのコンテキスト データが再ロードされると、スレッドは逆方向に実行され続けますが、この時点ではチケットは失われており、共有リソース チケットには複数のスレッドからアクセスされたときにデータが含まれることになります。安全でない質問です。

5.
論理的判断とチケットを分離しましたが、分離しなければ問題ないということでしょうか?
答えはそうではなく、問題は依然として存在しますが、そのような問題を再現するには確率に頼る必要があるため、再現するのはそれほど簡単ではありません。しかし、原則がわかっている限り、チケットのみの場合に問題があるかどうかを分析してみましょう - このステップ?
ループ内で投票番号 -1 を同時に実行する 2 つのスレッドの例を示しました。リソースを共有するマルチスレッド操作によって引き起こされるこれらの問題の本質的な理由が 1 つだけと言いたいのであれば、他のスケジューラーが切り替えられている間に、リソースが自分のコンテキスト構造とともに操作の途中で切り替えられる可能性があるということです。起動されたスレッドは引き続きこの共有リソースにアクセスできますが、オフにされたスレッドはそのことを認識しません。誰も教えてくれなかった!私と私のコンテキストは、CPU によって再スケジュールされるのを待っています。でも帰ってきたら空模様が一変!まだ何も分からず、愚にもつかない共有変数の操作を続けていると、共有リソースのデータが不整合になるという問題が発生します。

ここに画像の説明を挿入

2. 解決策の提案: ロック (ローカル ロックと静的ロックの 2 つの初期化/破棄スキーム)

2.1 ロックの事前理解と実装

1.
那该如何解决上面的问题呢?多个执行流操作共享资源时,发生了数据不一致问题。
解决上面的问题实际要通过加锁来实现,但在谈论加锁的话题之前,我们需要来重新看待几个概念。
多个执行流总是能够共享许多资源,但在加锁保护后的共享资源我们称为临界资源。
而多个执行流执行的函数体内部,对临界资源进行操作的代码称为临界区,需要注意的是临界区不是整个函数体内部的代码,而是指对共享资源进行操作的代码称为临界区。
如果我们想让多个执行流串行的访问临界资源,而不是并发或并行的访问临界资源,这样的线程调度方案就是互斥式的访问临界资源!(串行就是指只要一个线程开始执行这个任务,那么他就不能中断,必须得等这个线程执行完这个任务,你才能切换其他线程执行其他的任务,这个概念等会讲完锁之后大家就明白什么是互斥了)
当线程在执行一个对资源访问的操作时,要么做了这个操作,要么没有做这个操作,只要两种状态,不会出现做了一半这样的状态,我们称这样的操作是原子性的。(就比如你妈让你写作业,你要么给我把作业写完了再出去玩,要么就一个字也别写给我滚出家门,就这两种状态,不会出现你写了一半,然后你妈让你出去玩的这种情况,这样也是原子性)

ここに画像の説明を挿入

2.
上記の 4 つのグループの概念が準備できたら、共有リソースをロックおよびロック解除する方法について説明します。まず第一に、ロックは実際にはデータ型です。このロックは通常定義する変数やオブジェクトのようなものです。このロックのタイプはシステムによってカプセル化されたタイプであり、再定義後は pthread_mutex_t になります。変数またはオブジェクトは存続中に初期化することもできます。変数が初期化されると、それは宣言ではなく変数の定義になります。変数とオブジェクトにも独自の破棄スキームがあります。組み込み型の変数が破棄されると、オペレーティング システムは自動的にリソースを再利用します。カスタム オブジェクトが破棄されると、オペレーティング システムはそのデストラクタを呼び出してリソースを再利用します。
同じことがロックにも当てはまります。ロックには独自の初期化スキームと破棄スキームもあります。ローカル ロックを定義する場合、初期化と破棄には pthread_mutex_init() と pthread_mutex_destroy() を使用する必要があります。グローバル ロックを定義する場合、または静的ロックの場合ロックの場合、初期化と破棄に init を使用する必要はありません。PTHREAD_MUTEX_INITIALIZER を使用して直接初期化してください。独自の初期化および破棄スキームがあり、静的ロックまたはグローバル ロックの破棄方法を気にする必要はありません。
定义好锁之后,我们就可以对某一段代码进行加锁和解锁,加锁与解锁意味着,这段代码不是一般的代码,只有申请到锁,持有锁的线程才能访问这段代码,加锁和解锁之间的代码可以称为临界区,因为想要访问这段空间必须有锁才可以访问。pthread_mutex_lock实际就是申请锁的代码和临界区的入口,如果你申请锁成功了,那么你就可以进入临界区访问临界资源,如果你并没有申请成功,比如当前这把锁已经被别的线程申请到并持有了,其他线程正持有锁在临界区访问着呢,那么你就无法进入临界区,因为你并没有持有锁,必须得在pthread_mutex_lock这个接口外面等着,直到你申请到锁之后,你才能进入临界区访问临界资源,这样的线程访问实际就是互斥,指的是当一个线程正在持有锁访问临界区的时候,其他线程无法进入临界区,直到持有锁的线程释放锁之后才会有可能进入临界区,注意是有可能,因为当线程释放锁之后,这把锁还需要被竞争,哪个线程竞争到这把锁,哪个线程才能持有锁的访问临界资源!

ここに画像の説明を挿入

3.
ロックの初期化と破棄、およびロックの追加とロック解除の方法について説明した後、ロックを使用して、上に表示された共有リソースへの安全でないアクセスの問題を解決しましょう。マルチスレッドによって重要なリソースにアクセスする場合、スレッドの切り替えなど、重要なリソースへの非アトミック アクセスにつながる何かが原因ではないでしょうか? では、私はあなたにこれをさせません。私はこの重要なリソースをロックします。そのため、あなたが現在ロックを申請し、重要なリソースにアクセスしているスレッドは、重要なリソースにアクセスするためのアトミック アクセスを私に与える必要があります。つまり、あなたはそうしなければなりません重要なリソースにアクセスする作業が完了した後でのみ、重要なリソースにアクセスしたくないか、重要なリソースにアクセスした後、すべての重要なリソースにアクセスする必要があり、途中の半分にはアクセスできません。したがって、クリティカルなリソースがロックされている限り、クリティカルなリソースは安全になります。どのスレッドがクリティカルなリソースにアクセスしようとしても、アトミックな方法でアクセスする必要があるため、アクセスの途中でリソースが現れることはありません。スレッドはスイッチダウンされ、他のスレッドはクリティカルなリソースへのアクセスを続けるためにスイッチアップされますが、ロックを保持しているスレッドがスイッチダウンされると、このスレッドは適用されたロックを保持したままスイッチダウンされ、この時点で他のスレッドもスイッチダウンされます。電源が入っていて重要なリソースにアクセスしたいのですが、ロックがないので役に立ちません。ロックを保持しているスレッドが切り替わると、ロックを保持したスレッドも切り替わりますが、クリティカル セクションにアクセスできず、CPU はコードの実行を続行できないため、ロックを保持しているスレッドが再び切り替えられるときのみ続行できます。重要なリソースへのアクセス作業を除くと、この作業はロックを申請したスレッドによってのみ完了する必要があり、完了することができます。他のスレッドはこの作業を完了できません。逆に言えば、これは原子性ではないでしょうか?ロックを保持しているスレッドが重要なリソースにアクセスする作業を開始している限り、たとえ実行中にスイッチがオフになったとしても、心配する必要はありません。他のスレッドはこの作業を実行できないため、依然としてロックを保持しているスレッドを待つため この作業を続行できるのは、スイッチがオンになったときだけです。この作業を開始すると、必ず完了するという意味ですか? 作業の半分を実行し、何度も停止し、他のスレッドが重要なリソースにアクセスできるようにする状況は発生するでしょうか? もちろん違います!これがロックの役割です。

ここに画像の説明を挿入

4.
ロック後にコードを実行すると、実際にチケットを取得する速度がロック前よりも遅くなっていることがわかります。その理由は非常に簡単です。ロックが追加される前に、スレッドは同時実行または並列実行できることを説明しましょう。同時実行と並列処理とは何かについて簡単に説明します。この 2 つの違いと概念については後ほど詳しく紹介します。同時実行は単純です。スレッドの半分がオフになると、CPU はこの時点で他のスレッドが実行されるようにスケジュールすることもできることは理解されています。つまり、複数のスレッドが実行されている場合、各スレッドは一定期間 CPU によって実行されます。内部ではすべてのスレッドが実行可能となり、各スレッドの実行処理が進められます。並列処理とは、重要なリソースに同時にアクセスする 2 つのスレッドなど、複数のコアで異なるスレッドを同時に実行することです。ロックを解除すると、複数のコアが重要なリソースにアクセスしながら 2 つのスレッドのコードを同時に実行する可能性があります。しかし実際には、この状況は一般的ではありません。なぜなら、私たちが書くコードは優先度がそれほど高くないため、基本的に同時実行に従って実行されるからです。
その後、ロックする前に同時に実行されます。つまり、スレッドが切り替えられると、チケットの他のスレッドも再スケジュールされてチケットをカウントできます。
ロックがロックされた後は、ロックを保持しているスレッドが切り替わっても、CPU にスケジュールされている他のスレッドはロックを持っていないためカウントできないと上で述べたので、ロックがロックされた後は同時に実行されません。したがって、ロックを保持しているスレッドが切り替わっている間、スレッドは重要なリソースにシリアルにアクセスしているため、投票数は変わりません。これは、あるスレッドが別のスレッドにアクセスした後、別のスレッドの番になることを意味します。前に述べたように、スレッドが作業を完了してロックを解放した後、他のスレッドがロックを競合して重要なリソースにアクセスできます。これはシリアルです。
複数のスレッドがタスクを実行している場合、同時実行を行うため、現在のスレッドが切り替わっても、他のスレッドが実行されるので大丈夫であるため、逐次実行の効率は同時実行の効率よりも低くなります。このタスクは引き続き実行できます。ロックを追加するとシリアル実行になるため、現在ロックを保持しているスレッドがオフになると、スケジュールされた他のスレッドはタスクを実行し続けることができなくなり、当然効率が低下します。(効率が低いほど効率は低くなります。結局のところ、共有リソースは安全になりました。以下に実行結果も表示されます。ロックがない場合、投票数はマイナスになります。ユーザーはこの状況をどのように許容できるでしょうか。 )

ここに画像の説明を挿入

2.2 ローカル ロックとグローバル ロックの 2 つのロック スキームのコード実装

1.
ローカル ロックを定義する場合、共有リソースのセキュリティを確保するために、各スレッドがこのロックを使用して相互に排他的に共有リソースにアクセスできるように、必ずこのロックを各スレッドに渡します。また、出力された結果でどのスレッドがチケットを取得しているかを区別できるように、各スレッドに名前を付けたいと考えています。
それでは、ロックとスレッド名をカプセル化するために ThreadData 構造体が必要なのでしょうか? そこで、構造体を定義し、構造体ポインタをスレッドに渡し、スレッドがロックを使用して重要なリソースにアクセスできるようにします。

ここに画像の説明を挿入

2.
次に、ロック後の動作現象を見てみましょう。while ループ後の usleep(1000) がない場合、チケットを取得したユーザーは基本的に一定期間同じユーザーであることがわかります。チケットを取得した場合、ユーザー 1 はおそらくチケットを取得する必要があります。その後、長期間のチケットは他のユーザーに変更されます。これはなぜですか?
なぜなら、ロックは、スレッドが相互排他的な方法で重要なリソースにアクセスしなければならないことを規定するだけで、どのスレッドが最初に重要なリソースにアクセスする操作を実行するかを規定するものではないからです。言い換えれば、あなたのスレッドが重要なリソースにアクセスするためのロックを保持している限り、私はあなたのアクセスに同意します。ロックを保持している限り、つまり解放した後は、あなたがどのスレッドであるかは気にしません。場合によっては、このロックを再び奪い合うことができれば、いつでもこのロックを保持したままアクセスできます。ロックされるまで競争を続けることができれば、いつでも重要なリソースにアクセスできます。
以下の現象ですが、実際にロックを再競合する際には、ロックを解放したばかりのスレッドの方が競争力が高いため、次のような現象が現れます。(同時に、他のスレッドはチケットを取得できないため、競争の激しいスレッドがチケットを取得し続けるのをただ見守ることしかできません。この現象は飢餓状態と呼ばれ、解決策は実際にはスレッドの同期によって解決されます。ここで、最初にウォームアップし、私は詳しくは後ほどお話します。)

ここに画像の説明を挿入

3.
上記の現象は正しいですか? もちろんそれは正しいです!私のスレッドは非常に競争が激しいのに、チケットを取得し続けることができないのはなぜですか? ロックは、重要なリソースに相互排他的な方法でアクセスする必要があることのみを規定しており、どのスレッドがチケットを最初に取得するか最後に取得する必要があるかについては規定していません。チケットを取得し続けます。私に何ができますか?
しかし!上記の現象は正しいですが、彼はビーバーではありません。たとえば、電車の切符を手に入れると、この切符は 1 人のユーザーによって盗まれ、他のユーザーはそれを盗むことができませんでした。では、鉄道局はどのようにしてお金を稼ぐのでしょうか? ユーザーの消費が鉄道局をどのように支えられるのでしょうか?複数のユーザーが使用する必要があります。
したがって、スレッド同期を使用して解決することに加えて、usleep(1000) によって解決することもできます。OS によって異なります) しばらくして、ブロックが一時停止されると、他のスレッドがロックを奪い合うことはできなくなりますか? 他のスレッドもチケットを取得できますか? チケットを奪い合っている競争の激しいスレッドを無力に見る必要はありません。

ここに画像の説明を挿入
以下は、usleep を使用した場合と使用しない場合の 2 つの結果の比較です。usleep を使用しないと、1 つのスレッドが長時間チケットを占有する可能性があります。usleep を使用すると、複数のスレッドが調整してチケットを取得でき、1 つのスレッドが継続的に占有されることはありません。チケットの受け取り状況。
ここに画像の説明を挿入

4.
ローカル ロックを使用した上記のコードの実装に加えて、静的ロックまたはグローバル ロックも使用できます。ローカル静的ロックは、ロックのアドレスをスレッド関数に渡す必要があり、そうでない場合、スレッド関数はロックを使用できません。ロックはローカルだからね!グローバル ロックの場合、そのアドレスをスレッド関数に渡す必要はありません。スレッド関数はロックを直接参照できるため、直接使用できます。

ここに画像の説明を挿入

3. コード現象に基づいて質問する

3.1 ロックをどのように扱うか?

1.
共有リソースへの安全でないアクセスの問題に対する上記の解決策を完了したら、ロックについてさらに深く理解しましょう。
共有リソースは複数のスレッドからアクセスされると安全ではないことがわかっているため、共有リソースを保護するためにロックする必要があります。しかし、戻って考えてみましょう。ロック自体は共有リソースなのでしょうか? すべてのスレッドがロックを申請し、ロックを解放する必要がありますが、これはロック リソースへのアクセスと同じではないでしょうか。では、ロック自体は共有リソースではないのでしょうか? 複数のスレッドがロックの共有リソースにアクセスする場合、ロック自体を保護する必要がありますか? もちろん必要ですよ!他の共有リソースはロックによって保護できますが、ロックについてはどうすればよいでしょうか?
実際、ロックとロック解除のプロセスはアトミックです。つまり、ロックを申請し、競争力が十分であればロックを取得できます。そうでない場合はロックを取得できません。また、ロックを申請したときにスレッドが切り替わったとは言えません。ロックの適用は途中です。現在、他のスレッドがロックを適用しているため、そのような中間状態は存在しません。ロックとロック解除のプロセスはアトミックであるため、アクセス ロックは実際には安全です。(しかし、なぜロックとロック解除のプロセスはアトミックなのでしょうか?それをどのように理解すればよいでしょうか?これについては後で説明します。)

ロック自体の共有リソースを含め、アドレス空間内のほとんどのリソースが共有されます。
ここに画像の説明を挿入

2.
ロックの適用が成功すると、スレッドはコードを逆方向に実行し続け、クリティカル セクションに入り、クリティカル リソースにアクセスします。では、ロックの申請が失敗した場合はどうなるでしょうか? それとも一時的にロックを申請できないのでしょうか?実行フローについてはどうですか?
次のコードでは、スレッド関数が 2 つのミューテックス ロックに内部的に適用されますが、これにより実際に問題が発生します。コードの実行が続行されず、プロセス内のすべてのスレッドがスケジュールされず、どのスレッドも実行できないことがわかります。チケットを取得するには、 ps -aL を介してスレッドが存在することも確認できますが、スレッドはコードを実行しません。また、 ps -axj を使用すると、現在のプロセスが Sl+ 状態、つまりブロックされた状態になっていることがわかります。 、Rではありません 稼働状況!

ここに画像の説明を挿入
3.
したがって、ロックを適用できない場合、実行フローはブロックされます。
スレッドがロックを申請すると、そのロックは別のスレッドによって奪われ、当然ロックを申請できなくなり、オペレーティング システムはそのようなスレッドを一時的に休止状態にします。ロックを保持しているスレッドがロックを解放する場合にのみ、オペレーティング システムは POSIX ライブラリのコードを実行し、休止状態のスレッドを再度目覚めさせ、スレッドにロックを競合させます。そうでない場合は、スリープを続けます。
なぜ問題があるのでしょうか? 実際の理由は、現在のスレッドはすでにロックを申請していますが、再度ロックを申請しており、実際にはロックを保持していますが、主観的にスレッドを 2 回実行させているため、ロックを保持していることを認識していないことです。ロックを申請するというステートメントは、私たちが彼に依頼したものです。彼は自分でロックを保持しており、これからロックを申請しようとしていますが、ロックはすでに保持されているため、現在のスレッドは必然的にロックの申請に失敗します。ロック、つまり冬眠中ですが、いつ目覚めますか? もちろんロック解除時も!ロックが解放されると、オペレーティング システムは現在のスレッドを起動しますが、ロックは解放されますか? もちろん違います!自分でロックを保持しているため、他のスレッドがロックを解放するのをまだ待っており、他のスレッドにはロックがありません。また、コード pthread_mutex_unlock を自分で実行することはできません。つまり、ロックを解放しません。あなた自身、そしてあなたはまだこのロックを持っていません ロックを解放するためのスレッド、これはあなた自身をブロックしているだけではありませんか?これは実際にはデッドロックであり、スレッドはロックが正常に解放されるまで待つことができず、その後、このスレッドは常にブロックされ、実行できなくなります。他のスレッドにも同じことが当てはまります。
したがって、上記の非常に多くのスレッドがすべてブロックされており、実行できるすべてのスレッドは、実際にはデッドロックの問題が原因であることがわかります。すべてのスレッドはロックを申請できません。そのほとんどは、ロックが存在しないためです。ロックが発生したときは、常にロックが発生します。スリープするためにロックが解放されるのを待ちたいのですが、愚かなスレッドが自分でロックを保持していますが、自分がロックを保持していることを忘れ、他の人にロックを返すように頼みますが、他の人がロックを解放するのを待っています。その結果、休止状態の問題が発生します。

4.
では、どうやって解決すればいいのでしょうか?2 つの方法があります。1 つ目は、pthread_mutex_trylock() を通じてロックを適用することです。このインターフェイスはロックの適用を試行し、ロックが適用されている場合は、コードの実行を続行して逆方向に実行します。ロックが適用されていない場合は、すぐにエラーが返されます。したがって、このインターフェイスは実際にはロックを適用する非ブロック的な方法です。問題の原因の観点から問題を解決します. アプリケーションのロックをブロックしたくないですか? じゃあブロックしないだけ?しかし、実際には、この解決策は非常に悪いです。スレッドに問題があるとプロセス全体が終了し、他のスレッドがロックを申請できない場合、ロックを申請することもできなくなります。ロックを申請したスレッドと相互排他 重要なリソースにアクセスする人はどうなるでしょうか。他のスレッドはロックを申請できないため、現在のスレッド リソースはリサイクルされます。そしてすべてのスレッドも終了しました!これは合理的ですか? もちろん無理ですよ!したがって、このソリューションは使いやすいものではありません。ロックの適用と解放には、依然として主流のロックとロック解除を使用する必要があります
したがって、ロックによって適用されるロックには、保留中のロックと呼ばれる別の種類のロックが存在します。
では、どうすれば解決できるでしょうか?私が知る限り、実際に良い解決策はありません。私たちプログラマができるのは、デッドロック コードを書かないように注意することだけです。一度書いたら、デッドロックによって引き起こされる問題を迅速に解決しなければなりません。デッドロックが発生している位置を確認してください。 、コードを変更してください。

実際、上記の内容を一言でまとめると、「ロックを保持している人は誰でもクリティカル セクションに入ることができます。ロックを持っていない場合は、ブロックしてクリティカル セクションの外でロックが解除されるのを待つことしかできません」となります。そして最後までロックを奪い合います ロックを保持してクリティカルエリアに入ってコードを実行するだけです 競争が足りない場合はブロックを続けてクリティカルエリアの外で素直に待ちます
ここに画像の説明を挿入
5.
上記で、クリティカル セクション、クリティカル リソース、シリアル実行、ロックを保持していないスレッドのブロック待機、および相互排他アクセスの概念を理解しました。しかし、ここには原子性という別の概念があります。スレッドがロックを保持するプロセスにおけるアトミック性の概念を実際に理解するにはどうすればよいでしょうか?
ロック プロセスを実際に理解する際の原子性の概念について説明する前に、いくつかの問題について説明します。これらの問題についてはここでは説明しません。以下に私が描いた絵をご覧ください。実際、これらの問題については上ですでに述べましたが、ロックを保持していないスレッドはロックが解放されるのを待ってブロックされ、ロックを保持しているスレッドが切り替わるスケジュールが設定されている場合、ロックを保持しているスレッドは独自のスレッドで切り替わるということに他なりません。ロックは 1 つしかなく、オフになったばかりのスレッドによって保持されているため、CPU に再スケジュールされた他のスレッドは依然としてロックを申請できません。したがって、CPU に対して再スケジュールされたスレッドは、コードを逆方向に実行し続けることができないため、役に立ちません。実際、これら 2 つのトピックについてはすでに上で説明したので、ここでレビューすることに相当します。

ここに画像の説明を挿入
6.
それでは!ロックを保持しない他のスレッドの場合、実際に意味のあるロックの状態は 2 つだけです。1 つはロックを申請する前、もう 1 つはロックを解除した後です。ロックが適用される前はロックは適用されていないため、ロックを保持していない他のスレッドにとっては当然意味があります。ロックが解放された後、この時点ではロックは適用されていない状態ですが、当然、ロックを保持していないスレッドがロックを競合する可能性があるため、これも意味のある状態です。
そして、ロックを保持していないスレッドの観点から見ると、スレッドは現在ロックを保持しているのはアトミックではないでしょうか? 彼らが見るロックには、アプリケーションの前とロック保持スレッドがロックを解放した後の 2 つの意味のある状態しかなく、これはアトミックであり、中間状態は存在しません。
したがって、今後ロックを使用する場合は、ロックが追加された後、スレッドがシリアルに実行されるため、クリティカル セクションの粒度が非常に小さいことを確認する必要があります。粒度が非常に大きい場合は、より多くの時間がかかります。このクリティカル セクションを実行すると、コード全体の効率が自然に低下します。これは、クリティカル セクションがシリアルである一方で、残りの非クリティカル セクションが同時または並列で実行されるため、全体の効率が大きく影響を受けるためです。クリティカルセクションの実行効率は非常に重要なので、通常のロックとロック解除では、プログラム全体の動作効率を高い状態に維持できるように、クリティカルセクションの粒度を小さくする必要があります。

7.
いくつかの追加トピックについて言えば、ロックを保持していないスレッドは、ロックの解放を待っている間にブロッキング状態になると言いますが、より具体的に言うと、ロックを保持していないスレッドは実際にはブロック状態になります。キュー内では、ミューテックス オブジェクトは、ロックによってブロックされたスレッドを格納するために使用される待機キューを内部に保持します。
ロックはプログラマの動作です。共有リソースにアクセスする場合は、その共有リソースにアクセスするすべてのスレッドをロックする必要があります。一部のスレッドがロックされ、一部のスレッドがロックされていないとは言えません。たとえば、現在スレッドのバッチがあり、2 つのスレッド関数を実行する必要があります。これら 2 つのスレッド関数は内部的に共有リソースにアクセスしますが、1 つのスレッド関数は共有リソースを内部的にロックし、もう 1 つは共有リソースをロックしません。スレッドの 1 つのバッチが、共有リソースへの相互排他的なシリアル アクセスが必要である一方で、別のスレッドのバッチが共有リソースに自由かつ同時にアクセスできるため、間違いなくセキュリティ上の問題が発生します。これは、プログラマによって書かれたバグと考えられます。共有リソース 十分に徹底されていない場合、それはあなた自身の問題です。

3.2 ロックとロック解除の本質を理解するにはどうすればよいですか? (ハードウェアレベルとソフトウェアレベルでのロック)

1.
この記事の前半で、単純な i++ および ++i ステートメントはアトミックではないことについて説明しました。そのようなステートメントは実際には少なくとも 3 つのアセンブリ ステートメントに対応し、メモリからデータを読み取り、それを保存する必要があるためです。レジスタ内でデータを変更し、最後に変更したデータをメモリに書き戻すため、++i や i++ のようなステートメントはアトミックであってはなりません。これは、実行中に中間状態が存在するためです。これは、何らかの理由により実行される可能性があります。実行途中でスイッチが切れるので停止します。この種の非アトミックな操作は、データの不整合の問題を引き起こします。これは、前によく話した、共有リソースへの安全でないアクセスの問題です。その後のソリューションはロックと呼ばれるもので、共有リソースへの相互排他的アクセスによってセキュリティが確保されます。
ロックとロック解除のプロセスは、実際には共有リソース ロックにアクセスするプロセスです。では、ロックとロック解除はどのようにしてアクセス ロックのアトミック性を確保するのでしょうか? 答えはアセンブリステートメントを通じて得られます。
ミューテックスのロック処理を実現するために、ほとんどのCPUアーキテクチャではレジスタとメモリユニットのデータを交換するスワップ命令とエクスチェンジ命令が用意されていますが、アセンブリ命令は1つだけなのでアトミック性が保証されています。また、マルチプロセッサプラットフォームであっても、メモリにアクセスするバスサイクルはシーケンシャルであるため、一方のプロセッサのスワップ命令が実行されると、バスサイクルが準備されてからでないと他方のプロセッサのスワップ命令にアクセスできません。

2.
実際、言語のアセンブリ ステートメントとデータを交換することによって保証されるアトミック性に加えて、オペレーティング システムのハードウェア レベルでアトミック性を達成する別の簡単な方法があります。スレッド実行プロセス中にスレッド実行の半分が切り替えられる可能性があるため、スレッド完了タスクはアトミックではないため、スレッドが CPU 上にある限り、実行中にスレッドをまったく切り替えられないようにすることはできますか?船はダウンすることができないため、コードが完全に実行されるまで待ってからダウンする必要があります。
実行中にスレッドが切り替えられる場合には、タイム スライスが到着した、より優先度の高いスレッドが到着した、一部のペリフェラルやデバイスへのアクセスによりスレッドがブロックされて待機する必要があるなど、さまざまな理由が考えられます。いずれにせよ、スレッドが実行途中で切り替わる可能性はあります。
したがって、システムレベルでは、すべての割り込みを禁止し、スレッド割り込みに応答しないようにし、割り込まれたバスの応答を禁止し、外部割り込みを閉じて、スレッドが切り替わらないようにするだけで、アクセスのアトミック性を実現できます。共有リソース。
もちろん、このようなソリューションは比較的低レベルであり、比較的重量のあるソリューションと見なすことができますが、このようなソリューションをハードウェア レベルで実装したとしても、スレッドによって実行される作業を除いて、コストは依然としてかなり高くなります。これを行うと、半分の場合、そのようなスキームはアトミック性を達成するために使用されません。

3.
ロック プロセスのアセンブリ コードについて話す前に、いくつかのコンセンサス トピックについて話しましょう. CPU には、すべての実行フローで共有されるレジスタ セットが 1 つだけあり、CPU 内のレジスタの内容は次のとおりです。各実行フローにプライベートであり、ランタイム コンテキストと呼ばれます。ロックされたアセンブリ ステートメントは、al レジスタに 0 を入れてから、唯一のアセンブリ ステートメント xchgb を実行し、al レジスタの内容を物理メモリ ユニットと交換し、その後 al レジスタの内容を交換することであることがわかります。が 1 になり、物理メモリ上のミューテックスの値が 0 になり、物理メモリ上のミューテックスの 1 が al レジスタの 0 と交換されると、スレッド A がロックを奪ったことが視覚的にわかります。ロック、スレッド A を切り替えることは可能ですか? もちろんそれは可能ですが、スレッド A が切り替わると、スレッド A は独自のコンテキスト データとともに切り替わります。
このとき、スレッド B は再スケジュールされた後、最初に独自のコンテキストで al レジスタに 0 をロードし、次に xchgb アセンブリ ステートメントを実行しますが、この時点では物理メモリのミューテックスが 0 であるため、ロックが解除されます。したがって、交換後も al レジスタ内の値は 0 のままで、判定を続けた後、else 分岐文に入り、スレッドは、交換待ちのためサスペンド待ち状態となります。ロックを保持しているスレッドによってロックが解放されます。
したがって、スレッド A がロックの申請に成功している限り、たとえスレッド A の操作が中断されたとしても、心配する必要はありません。レジスタとメモリを交換するためのアセンブリ ステートメントは 1 つだけであり、これによりアトミック性が確保されます。ロックプロセス、つまりロック適用プロセス。そして、スレッド A が切断されると、スレッド A はロックを保持して切断されるため、この時点で他のスレッドがスケジュールされていても、ロックを申請することはできないため、ブロックして待機する必要があります。スレッド A を再スケジュールし、スレッド A のコンテキストをレジスタにロードするだけです。このとき、al の内容は 1 になり、戻り値 0 はロック適用が成功し、スレッド A がロック型アクセス クリティカル セクションを保持できることを意味します。

ここに画像の説明を挿入

4.
上記のロック処理はアトミックであり、レジスタとミューテックスの内容の交換は 1 つのアセンブリ文のみで完了し、ミューテックスはいわゆる共有リソースであるため、1 つのアセンブリ文でミューテックス操作のアトミック性が保証されます。
ロック解除プロセスも非常に簡単です。1mov をミューテックスに入れるだけでロックの解放プロセスが完了します。その後、ロックを待ってブロックされているスレッドをウェイクアップして、ロックを獲得するために競合させます。解放されているので、同じように、ロックを解放します。アセンブリ ステートメントは 1 つだけであり、ロック解放プロセスのアトミック性も保証できます。

3.3 RAII スタイルのパッケージ デザイン ロック? (コンストラクターはロック、デストラクターはロックを解除)

1.
単にロックをカプセル化して使用したい場合、どのように設計すればよいでしょうか? また、以前のパッケージ設計スレッドと同様に、スレッドの作成とスレッドの破棄を行う C++ スタイルのオブジェクト指向バージョンを作成したいと考えています。
実際の実装も非常にシンプルで、元のアプリケーションのロック、ロック、ロック解除インターフェイスをカプセル化するだけです。まずミューテックスクラスを定義し、クラス内にコンストラクタを実装してロックのアドレスを初期化した後、ロックとロック解除用の2つのインターフェースを定義することで、内部的にロックとロック解除が可能なクラスを定義できます。
次に、別のカプセル化層を追加して、RAII (リソース取得は初期化) スタイルのロックを実装します。つまり、コンストラクターでロックし、デストラクターでロックを解除します。
ロックの初期化と破棄のスキームはクラスの外にあるため、使用する場合は、最初にロックを初期化し、初期化と破棄のスキームを決定してから、小さなコンポーネント Mutex.hpp を使用してロックを実行する必要があります。そしてロック解除プロセス!

ここに画像の説明を挿入
2.
ここにナレッジ ポイントを追加します。オブジェクトのライフ サイクルはコード ブロックに従います。つまり、オブジェクトがコード ブロックを離れると、デストラクターが自動的に呼び出されます。たとえば、以下のチケット取得コードでは、次のようにします。 usleep(1000) もクリティカル セクションに配置したくありません。ロック後のコードはクリティカル セクションに属し、ロック解除はオブジェクトが破棄された場合にのみ発生するため、コード ブロックを使用してスコープを実現できます。クリティカルセクションの制御。

ここに画像の説明を挿入

コードブロックがない場合は表示されます ロックを解放したばかりのスレッドは競争力が強くチケットを占有し続けるため、他のスレッドで飢餓問題が発生します 前にも述べたコードブロックがあります ロックが解除された後解放され、スレッドが保持されたままにします。 ロックのスレッドはしばらく停止します。そのため、他のスレッドもロックをめぐって競合し、チケットを取得できます。
ここに画像の説明を挿入

この知識ポイントを今まで知らなかった、またはあまり明確に知らなかった 上記のようなコードブロックの使用法を見たことがなかったので、vs に行って検証してみました。 . 事実 確かに上記の通り。

ここに画像の説明を挿入

4. リエントラントかつスレッドセーフ

1.
複数のスレッドが同時にコードを実行し、同時に共有リソースにアクセスする場合、マルチスレッド アクセスにより共有リソースにデータの不整合があり、共有リソースは安全ではなく、他のスレッドの実行に問題が発生する場合、この状況が発生します。スレッド安全ではありません。特に、ロック保護なしで共有リソースにマルチスレッドでアクセスするコードの場合、スレッドのセキュリティが確保されない可能性が高くなります。
そしてリエントラントとは何でしょうか?このトピックは馴染みのない話ではありませんが、以前プロセスシグナルについて説明したとき、プロセスはシグナルを受信し、カーネルに入るときにシグナルを検出し、ハンドラーメソッドにジャンプしてシグナル処理関数を実行し、シグナル処理関数が表示されることがあります。とメイン実行 ストリーム内で同じ関数本体を実行します 例えば、先ほどのリンクリストのpush_backがメインとハンドラで同時に実行されてしまい、原因不明のエラーが発生する可能性があります。の場合、この関数を非リエントラント関数と呼びます。問題がなければ、この関数はリエントラント関数です。非リエントラント関数は、この関数のプロパティを参照するものであり、この関数が非リエントラント関数と呼ばれることではなく、実行フローによってリエントラントであってはならず、実行によってリエントラントである場合のみであることに注意してください。フローでは、何か問題が発生する可能性が高くなります。

2.
以下は、スレッドの安全性と非安全性、関数のリエントラント性と非リエントラント性に関するいくつかのトピックです。実際には、これらは単なる概念をごちゃ混ぜにしているだけであり、コードを書くときにまったく使用されないため、ここでは単に説明します。彼らについて今ここで。

ここに画像の説明を挿入

3.
一言で言えば、リエントラント関数はスレッド セーフにとって十分かつ不必要な条件であり、スレッド関数がリエントラントである場合はスレッド セーフである必要があり、その逆も同様です。

ここに画像の説明を挿入

5. デッドロック

5.1 デッドロックの概念

1.
デッドロックとは、プロセス内の各スレッドがロックを保持しているが、同時に他のスレッドのロックにも適用され、各スレッドが保持しているロックが占有されて解放されないため、全員が待機して相手を待つことを意味します。最初にロックを解放しますが、全員がロックを解放せず、全員がロックを占有するため、全員が永続的な待機状態、つまり永続的なブロック状態になり、すべての実行フローが実行されなくなります。このような問題はデッドロックです。
チケットを取得するための以前のコードでは、複数のスレッドが同じロックを使用していました。将来の一部のシナリオでは、複数のロックを使用する必要があります。複数のロックの場合、一部のスレッドがロックを保持したまま解放しない場合、それらのスレッドはロックを解除する必要があります。他のスレッドによって保持されているロックを申請しており、各スレッドがこの状態にある場合、これがデッドロックの問題です。

2.
ロックによってデッドロックの問題が発生する可能性はありますか? もちろん、それは可能です。この問題については前に説明しました。スレッドはすでにロックを保持していますが、ロックが解放されるのを待ちますが、今はロックを解放できないため、ロックを保持して待ちます。 。実際、ロバに乗ってロバを探している男性ですが、最後にはロバを見つけることができるでしょうか?もちろん見つからない!

3.
デッドロックを引き起こす論理連鎖について話しましょう。見てください。私たちの議論の焦点はデッドロックの 4 つの必要条件です。ここではデッドロックについて説明します。

ここに画像の説明を挿入

5.2 デッドロックの 4 つの必要条件

1.
相互排他条件: リソースは一度に 1 つの実行フローによってのみ使用できます。相互排他とは、実際にはロック後のスレッドのシリアル実行です。
要求と保持の条件: リソースの要求により実行フローがブロックされた場合、実行フローはすでに取得したリソースを保持します。率直に言うと、私は自分のものを手放さない、あなたのものが欲しい、そしてあなたが私にくれなければ、あなたがくれるまで待ちます。
非剥奪条件: スレッドは、取得したリソースを使い切る前に、他のスレッドからリソースを強制的に剥奪することはできません。率直に言って、あなたにはまだリソースがあります。他人の自由が欲しいなら、待たなければなりませんし、それを力ずくで奪うことはできません。自分のリソースの使用が終了したら、他の人のリソースを申請するのを待つことができます。つまり、他のスレッドからリソースを強制的に剥奪することはできず、その場合は、ブロックして他のスレッドがリソースを解放するのを待つ必要があります。
循環待機条件: 複数の実行フロー間で、エンドツーエンドで互いのリソースを待機する関係が形成されます。この現象をループ待機とも呼びます。

ここに画像の説明を挿入

2.
デッドロックを打破することは、実はデッドロックを打開するための 4 つの条件のうちの 1 つであり、1 つの条件が破られない限り、デッドロックは発生しません。
最初のミューテックスは、変更できないロックのプロパティです。
2 番目のロックを申請するときに、アプリケーションが一時的に失敗した場合、ブロックしてロックが解放されるのを待つ代わりに、エラーを直接返します。これにより、保持状態が破壊されます。つまり、リクエストが失敗、どちらでもない 自分のリソースが解放されないようにしますが、リソースを直接解放し、エラーが発生したときに戻ることで、デッドロックを回避することもできます。たとえば、ロックを適用するには、pthread_mutex_trylock を使用します。
たとえば、優先度の高いスレッドが優先度の低いスレッドからリソースを奪ったり、リソースを取得したり、優先度の低いスレッドのロックを直接取得したりすることができます。したがって、リソースを剥奪できるかどうかを判断する場合は、優先度によって判断できます。
ロックが適用される順序により、スレッドにはループ待機の問題が発生するため、ループ待機の問題が発生しないように、ロックを適用する順序を一貫して保つようにしました。例: 重要なリソースにアクセスするには 2 つのロック A と B が必要であると仮定すると、すべてのスレッドがロックを適用する順序は、最初に A ロックを適用し、次に B ロックを適用することです。この場合、正常にロックを適用したスレッドがロックは B ロックを適用できる必要があります。その後、スレッドはクリティカル セクションにアクセスするためにこれら 2 つのロックを保持できます。また、他のスレッドは B ロックを適用することはおろか、A ロックを適用することさえできないため、待機することしかできません。ロックを保持しているスレッドが A ロックを解放するため、デッドロックの問題が発生しないという利点があります。これを行わないと、確実にデッドロックの問題が発生します。たとえば、あるスレッドが最初に A ロックを適用してから B ロックを適用し、別のスレッドが最初に B ロックを適用してから、 A ロック。後者のスレッドは B ロックを解放し、後者のスレッドは最初のスレッドが A ロックを解放するのを待機し、各スレッドが要求して保持しているため、最終的な結果として、両方のスレッドが次の状態になります。永続的なブロックと待機により、デッドロックの問題が発生します。(この解決策は依然として非常に優れており、すべてのスレッドがロックを適用する順序が一貫しています。)

3.
では、デッドロックを回避するにはどうすればよいでしょうか? 次の方法でデッドロックを回避できます。これらは、プログラマーがコードを作成するときに注意する必要がある詳細です。
たとえば、リソースの 1 回限りの割り当てなどの詳細については、インターフェイスで大量のスペース リソースが適用される場合、コードを作成する途中でリソースを適用するのではなく、これらのリソースを事前に適用する必要があります。マルチスレッド環境では、複数の実行フローとロックの場合、コード内でリソースを適用するときに問題が発生する可能性があります。コードの量が膨大な場合、発生する問題は本当に頭痛の種になる可能性があります。同じロック条件は非常に複雑になります。
したがって、マルチスレッド環境では、一度にリソースを割り当てることを強くお勧めします。これを行わなくても、コードがエラーを起こした後、コードがどのように動作するかを教えてくれるので問題ありません。 。

ここに画像の説明を挿入

4.
上記で注意する必要があるデッドロックを回避するためのコード作成に加えて、デッドロックを回避するための 2 つのアルゴリズムについても説明する必要があります。
まず質問します。あるスレッドによって適用されたロックを別のスレッドが解放できますか? もちろん可能です!ロックの解放は、ロック解除インターフェイスを呼び出すだけではありませんか? この作業を実行できないスレッドはどれですか? 対応するロックのアドレスがいずれかのスレッドに渡されている限り、スレッドはロック解除インターフェイスを呼び出してロックを解放できます。したがって、デッドロック検出アルゴリズムのアイデアは、各スレッドが実行されているかどうかを測定するカウンターを定義するクラスを定義することです。スレッドが実行されている限り、カウンターは常に ++ になり、その後、別の監視スレッドを使用できます。このカウンタでは、長期間カウンタが変化しないとデッドロックが発生する場合がありますが、このとき監視スレッドがロックのロック解除を解除し、直接ロックを解除することでデッドロックを回避します。

バンカーのアルゴリズム (理解)

5.
教材ではデッドロックの解決方法が詳しく書かれていますが、実際にはプロジェクトではロックをできるだけ使用しないでください。問題を解決するためにどうしてもロックが必要な場合は、ロックの数はできるだけ少なくしてください。問題を解決するために使用する必要があります。このロックは C++ テンプレートと同じであるため、水深は非常に深いです。これを学んでいるからといって、これは重要であるに違いない、あるいは実際の使用率が高いなどということは絶対的なものではありません。

2. スレッド同期 + 生産および消費モデル

1. 条件変数を通じてスレッド同期トピックをスローする

1.
前述したように、チケットを取得するロジックでは、ロックを解放したばかりのスレッドの競争力が強いため、他のスレッドはロックを申請できず、その後、他のスレッドは長時間ロックを申請できず、ロックを申請できなくなります。ブロックして待つだけです。そのようなスレッドは飢えています。
条件変数がスレッド同期を実装する方法を理解するための例を示します。
今、学校が Xueba のために VIP 自習室を開設したとします。学校では、この自習室には一度に 1 人しか入室できないと規定されています。自習室のドアには鍵が掛けられています。早く来た人が鍵を受け取ります。自習室は勉強するために入る扉であり、自習室に入った後は扉が施錠されると他の人は入ることができなくなります。それで次の日勉強するつもりだったけどレポートができなくて、朝の3時に駆け寄って鍵を受け取り自習室に入って自習し、3時過ぎに何時間もの書類を書き、トイレに行きたいと思ってドアを開けると、外に立っている大勢の人々が誰が先に来たか、そしてなぜそんなに早く来たかについておしゃべりしていました。そんなに巻き毛?そして、後で鍵を壁に置いた後、トイレから戻った後に誰かが鍵を持って勉強部屋に入ってきて、再び転がることができなくなるのではないかと心配して、鍵をポケットに入れました。鍵を持ってトイレに行く もちろん、鍵を持ってトイレに行ったので、他の人が自習室に入ることはできません。戻ってきて、またドアを開けて中に入り、3時間自習をしましたが、お腹が空いてこのまま食べないと餓死してしまいそうだったので、ドアを開けて外に出ようとしたとき、食べると突然罪悪感でいっぱいになります午前3時になんとか自習室にたどり着きました今すぐ帰るのも悪くないので自習室を開けて勉強に戻りますもちろん他の人も大丈夫ですあなたとは競争しないでください!鍵はいつもポケットの中にあるから、外に出たら鍵を壁に掛ける、少し罪悪感を感じて鍵を拾って自習に戻る、なぜなら鍵に一番近いのは自分だからあなたの競争力は最強です。その結果、1分間自習室から出てきてはまた出てきて、また罪悪感を感じてまた戻ってくるということを繰り返しましたが、自習室長の姿さえ誰も見ていませんでした。
このように長時間ロックを取得できないスレッドは、クリティカル セクションに入ることができず、クリティカル リソースにアクセスできません。このようなスレッドを飢餓状態と呼びます。

2.
そこで、学校では新たな方針を導入し、自習室から出てきた人は列の最後尾に戻り、再度自習室に入る列に並んでください。自習室に入る鍵を手に入れる。
したがって、データのセキュリティを確保することを前提として、スレッドが特定の順序で重要なリソースにアクセスできるようにすることで、他のスレッドの枯渇の問題を効果的に回避します。これをスレッド同期と呼びます。

2. 生産モデルと消費モデルの概念的な理解 (321 原則)

1.
上記で、条件変数の役割、つまり、相互排他的アクセスを持つスレッドが同期を達成できるようにして、他のスレッドの枯渇問題を効果的に回避できるようにすることを事前に理解しましたが、条件変数の使用方法を実際に学ぶ前に、次のことを行う必要があります。もう 1 つ話します。このモデルは生産および消費モデルと呼ばれます。生産および消費モデルについて説明した後、条件変数を使用して、条件変数 + 生産とに基づいてブロッキング キューベースの生産および消費モデル コードを実装しましょう。消費モデル。

2.
実生活では、消費者として私たちは通常、生産者から製品を購入するのではなく、スーパーマーケットなどの場所に製品を買いに行きます。これは、サプライヤーは通常、製品を小売りせず、大量の製品をスーパーマーケットに均一に供給するためです。そして私たち消費者はスーパーマーケットなどの取引所から商品を購入します。
私たちが製品を購入するとき、生産者は何をしているのでしょうか? 生産者は商品を生産していたり​​、休暇をとったり、他のことをしていたり​​するため、生産と消費のプロセスはあまり影響を与えず、生産者と消費者の間のデカップリングが実現します。
そしてスーパーマーケットはどのような役割を果たしているのでしょうか?例えば、休日や消費が集中する時期には、スーパーに買いに来る人が多くなり、供給が需要を上回る可能性が高くなりますが、スーパーマーケットの倉庫には一括保管されるため、スーパーマーケットは通常、対策を講じています。そのため、消費が旺盛な時期でも、スーパーマーケットでは品薄になる心配がありません。労働期間中は、労働による報酬と引き換えに皆が忙しいため、消費に来る人が減り、物の流れが少なくなる可能性がありますが、このとき、供給者がまだ大量の物を供給していたらどうなるでしょうか。スーパーマーケットへ?スーパーは最近何も売れなくなったとしても、消費が旺盛な時期の大量消費に対応できるよう、サプライヤーの製品を先に倉庫に保管しておくことはできます。つまり、スーパーマーケットは実際にはバッファーとして機能し、コンピューター内のデータバッファーとして機能します。
そして、コンピューター内のどのシナリオが強く結合しているのでしょうか? 実際、関数呼び出しは強い結合のシナリオです。たとえば、main が func を呼び出すとき、func がコードを実行しているときに main は何をしているのでしょうか? main は何もできません。呼び出し後に func が戻るのを待つことしかできません。その後、main はコードを逆方向に実行し続けることができます。そのため、main と func の間の関係を強い結合関係と呼び、プロデューサーとコンシューマーが言及されています。上記は強結合関係ではありません。

ここに画像の説明を挿入
3.
生産と消費のモデルをさらに深く掘り下げると、スーパーマーケットは実際には典型的な共有リソースです. 生産者と消費者の両方がスーパーマーケットを訪問する必要があるため、スーパーマーケットの共有リソースはアクセス時に保護される必要があります.実際には、セキュリティを確保するために、ロックを通じて共有リソースへの相互排他的アクセスを実現します。
スーパーマーケットの共有リソースが 1 つだけの場合、生産と生産、消費と消費、生産と消費のすべてが共有リソースにシリアルにアクセスする必要があります。ただし、消費者が鍵を占拠してそこで買い物をしているのに、実際のスーパーには品物がないという可能性も考えられるため、効率を高めるために同期などの関係を構築しました。当然の疑問ですが、消費者が消費しすぎた後は、生産者が生産する番であるため、生産者と消費者の間には、排他関係だけでは不十分であり、同期関係も必要です。

ここに画像の説明を挿入
4.
321 原則は、生産と消費のモデルから抽出できます。つまり、3 つの関係、2 つの役割、1 つの取引場所です。消費スレッドと消費スレッドの関係、消費スレッドと生産スレッドの関係、生産スレッドと生産スレッドの関係に対応して、取引場所はブロッキング キュー blockqueue です。スレッドの同期を実現するには、条件変数が必要です。たとえば、生産者が生産を終えた後、スーパーマーケットは消費者に電話して、消費者に来て消費するように頼みます。消費後、スーパーマーケットは生産者に電話して生産者に生産を依頼します。このようにして、 、特定のスレッドが常に生成または消費できないほど競争が激しいという事実によって、他のスレッドが枯渇するという問題は発生しません。

ここに画像の説明を挿入
5.
生産および消費モデルの利点を要約します。
a. 彼は生産と消費の分離を達成し、相互に影響を及ぼさないようにしました。
b. 生産と消費における不均一な忙しさの問題を一定期間サポートする。バッファはデータの一部をデータバッファリング用に予約できるためです。
c.生産と消費の間の相互排他的かつ同期的な関係により、生産と消費モデルの効率が向上します

しかし、まだ問題があります。生産と消費は相互に排他的です。共有リソースはロックで保護する必要があるため、生産者が生産するとき、消費者は消費できません。ロックは 1 つだけなので、毎回この共有リソースにアクセスするスレッドのみが存在します。では、なぜ生産と消費のモデルが効率的だと言えるのでしょうか? この問題は非常に重要です。ブロッキング キューのコード実装については後で説明した後、この問題に焦点を当てなければなりません。

3. スレッド同期を実現するための条件変数の原理 (条件変数は内部的にスレッド待機キューを維持し、スレッドを待機またはスレッドをウェイクアップできます)

1.
複数のスレッドを連携させるためには、複数のスレッドの同期関係を実現する必要があり、同期関係を維持するために条件変数を導入する必要があります。条件変数とは何ですか? 実際、ミューテックスと同様、データ型によって定義されるオブジェクトです。初期化と破棄のスキームは、ミューテックスのスキームとまったく同じです。唯一の違いは、条件変数には使用時に頻繁に使用される 2 つのインターフェイスがあることです。1 つは pthread_cond_wait です。この関数の機能は、特定のロックを待機しているスレッドを条件変数の待機キューに入れて待機することです。もう 1 つは pthread_cond_wait です。 pthread_cond_signal、この関数の機能は、条件変数の待機キュー内の最初の待機スレッドをウェイクアップすることです。あまり頻繁には使用されませんが、時々使用されるもう 1 つのインターフェイスは pthread_cond_broadcast です。この関数は、条件変数の待機中のすべてのスレッドをウェイクアップします。ウェイクアップし、すべてのスレッドが競合するロックの状態に戻ります。シグナルのような cond キュー内のロックを待ってブロックされているスレッドをウェイクアップする代わりに。

ここに画像の説明を挿入

2.
先ほどの自習室の例に加えて、面接官が求職者と面接する例をご紹介しますが、条件変数の役割を誰でも実感できる例がたくさんあります。相互に排他的にアクセスできるスレッドは、特定の順序でクリティカル セクションに入り、クリティカルなリソースにアクセスできます。これが環境変数によってもたらされる最大の役割です。これにより、共有リソース アクセスのセキュリティが確保されるだけでなく、すべてのスレッドが共有リソースにアクセスするためのロックを取得できるようになり、スレッドの枯渇の問題が回避されます。したがって、次の例を見てください。条件変数によってもたらされる利点と機能、および条件変数によって実現されるスレッド同期について深く理解している場合は、このテキストを無視して、次の条件変数にジャンプして同期を実現できます。原則的な部分。

ここに画像の説明を挿入

3.
条件変数は構造体として理解でき、その中には現在のスレッドが待機しているロックの使用法を具体的に示すフィールドがあり、ステータスが有効であれば、ロックも解放されていることを意味します。現時点では、特定のロックを待機するスレッドのキューという特別に維持される別のフィールドがあります。ステータスが有効になると、pthread_cond_signal を呼び出して cond 内の待機キュー内のスレッドを起動し、このスレッドのコンテキストを CPU のレジスタにロードします。このスレッドは前のスレッドによって解放されたロックを適用します。このスレッドは、クリティカル セクションにアクセスするためのロックとミューテックスを保持できます。
したがって、条件変数の同期の根本的な原因は、待機とシグナルによるものです。たとえば、特定のスレッドがロックを解放した場合、待機キュー内の cond をウェイクアップしたいため、スレッドはロックの適用を続行すべきではありません。スレッドは消えました。彼らはまだこのロックを必要としています。あなたとしては、cond の待機キューに行って待ってください。次回目覚めたときに、ロックを再申請する資格があります。したがって、条件変数の待機とウェイクアップの方法により、一部のスレッドがロックを適用できないことによって引き起こされる飢餓の問題を発生させることなく、複数のスレッドが相互排他的な方法でクリティカル セクションに正常にアクセスできます。

ここに画像の説明を挿入

4. シリアル、コンカレント、パラレルの概念

1.
次に、シリアル、コンカレント、パラレルに関する概念をいくつか紹介します。これらの概念だけについて話すことは実際には難しくありませんが、それらが現代のコンピューターにどのように分散されているか、そのような知識はより貴重です。もう 1 つ言っておきたいのは、インターネット上ではマルチコアをマルチ CPU と呼ぶのが好きな人が多いですが、プロセッサ チップには複数のコアが統合されており、各コアには独自の独立したストレージがあるため、このように呼ぶことに実際には何も問題はありません。ユニット、制御ユニット、算術論理ユニットなので、各コアが異なるタスクを実行できます機能的には確かにマルチCPUと言えますが、私のような初心者は誤解を招きやすいでもあります。マルチ CPU プロセッサについては、ほとんどの人が別の呼び方をしていることがわかりました。

ここに画像の説明を挿入

2.
実際、コンピュータが動作しているときは、同時実行が必要です。同時実行は、複数のプログラムを同時に実行したいユーザーのニーズを十分に解決できます (これをマルチタスクと呼びます)。ただし、並列処理も必要です。たとえば、上記の例では、各大きなコアは異なるプログラムを実行しますが、同時に、特定の大きなコアがプログラムを実行しているときに、タイム スライス ローテーションで別のプログラムを実行することもできるため、コンピューティングでは並列性と同時実行性が同時に実現されます。存在します。
同時実行は並列処理よりも効率的である必要があるという前提は、マルチタスクです。マルチタスクの観点から直列化と同時実行を見てみると、スレッドが切り替わると直列化が切り替わるため、同時実行がより効率的である理由が確実に理解できます。リリースすると、この時間の間、CPU は何もできなくなり、この時間が無駄になり、マルチタスクの場合、効率は確実に低下します。同時実行性に関しては、スレッドが切り替えられるか、ロックが解放されるのを待っているかは関係ありません。CPU が他のスレッドの実行をスケジュールするため、切り替えられたスレッドが待機しているときは、時間が完全に切れてしまいます。これは無駄にはなりませんが、CPU が他のスレッドを実行するために使用されます。
なぜ逐次実行より同時実行の方が効率が良いのか理解できなかったのは、私がマルチタスクの観点ではなくシングルタスクの処理の観点から見ていたからですが、このような場面は非常に稀なはず、あるいはほぼ無いと言えるでしょう。存在しません。コンピューターの電源を入れた後、個別に処理されるタスクは 1 つだけですか? 絶対に違います、どうやって確認しますか?とてもシンプルです!タスク マネージャーを開いて、実行中のバックグラウンド プロセスの数を確認します。これはシングルタスクのシナリオですか?
当時私はそれが絶対であると誤解していました、単一のタスクを逐次実行しても同時実行しても実行効率は同じであり、この理解自体は間違っていませんが、そのようなシナリオは存在しません。これらのスレッドの実行効率はほぼ同じであり、デフォルトでマルチタスクであることを前提に議論しています。

5. 条件変数の基本的なコードの書き方

1.
ここでは、最初にグローバル ミューテックスと条件変数を使用して、条件変数の効果をコード レベルで理解するのに役立つ簡単なコード テストを実施します。実際に条件変数と生成および消費モデルを使用してコードを記述する環境が配置されます。最初の 3 つの部分で説明します。
まず、スレッドのバッチを作成し、スレッド関数内で共有リソース チケットをロックおよび保護し、条件変数を使用してスレッド間の同期関係を実現します。start_routine では、クリティカル セクションに入った後、すべてのスレッドが最初に待機を実行できるようにし、すべてのスレッドが条件変数で待機できるようにします (実際に pthread_cond_wait を実行すると、現在のスレッドが保持しているロックがアトミックな方法で自動的に解放されます)。メイン スレッドは、cond で待機中のスレッドをウェイクアップする責任があります。この場合、すべてのスレッドがクリティカル セクションにアクセスするためにロックを適用できるため、空腹のスレッドは存在しません。

ここに画像の説明を挿入
2.
メインスレッドが pthread_cond_signal を呼び出して、cond キューで待機中のスレッドをウェイクアップすると、チケットを取得したスレッドの実行結果が表示されます。実行チケットの数は非常に規則的であり、実行順序は 12453 です。各スレッドは適切に配置されており、スレッドの枯渇、投票を実行できないという状況は発生しません。
主な理由は、スレッドがウェイクアップされるときに、重要なリソースにアクセスしてロックを解放した後、コードをループで実行し、再度 pthread_cond_wait を実行するためです。このとき、再度ロックを解放して、待機中のキューに戻り、シグナルはウェイクアップを続け、この時点でキュー内の他のスレッドを再度待機します。このようにして、すべてのスレッドがロックを申請できます。

ここに画像の説明を挿入
ここでは、pthread_cond_timedwait インターフェイスの補足説明を示します。このインターフェイスと pthread_cond_wait の違いは、wait インターフェイスは、ロックを待ってブロックされているスレッドを、ロックが解放されるまで cond 待機キューに入れ、pthread_cond_signal インターフェイスがウェイクアップすることです。 cond 待機キューのスレッドを上げます。timedwait は、一定期間ロックを待機します。ロックが解放されない場合、インターフェイスは自動的にタイムアウトして戻り、スレッドがブロックされて長時間ロックを待機するのを防ぎます。ただし、このインターフェイスは一般的には使用されないため、依然として pthread_cond_wait インターフェイスの使用に重点を置いています。

ここに画像の説明を挿入

3.
pthread_cond_broadcast が呼び出されると、cond ブロッキング キュー内のすべての待機スレッドが起動され、これらのスレッドが特定の順序でロックをめぐって順番に競合します。スレッドがロックの使用とクリティカル セクションへのアクセスを終了すると、スレッドはロックを取得します。はロックを解放し、条件変数に戻って待機します。この時点で、残りの目覚めたスレッドがロックを獲得するために競合し、前のスレッドと同じ作業を実行します。したがって、印刷結果は次の図のようになり、スレッドを一括起動した後、5 つのスレッドすべてがチケットを取得し、そのたびに 5 スレッド単位で起動します。
これは条件変数によってもたらされるスレッドの同期です。最初にすべてのスレッドを条件変数で待機させてから、各スレッドを起動します。起動したスレッドが重要なリソースにアクセスした後、待機キューに再度入ります。このようにしてすべてのスレッドは、クリティカル セクション内のクリティカルなリソースにアクセスするためにロックを適用できます

ここに画像の説明を挿入

3. ブロックキューに基づく生産および消費モデル

1. ダブルブロッキングキューによる多生産・多消費モデルの実現

1.
上記では、生産消費モデルの概念と条件変数のコード実装について説明しました。次に、これら 2 つのツールを使用して、ブロッキング キューに基づく生産消費モデルを実現します。
当初の計画では、最初にブロッキング キューを生成して消費して、世代消費モデルを実現することでしたが、これは少し単純です。難しい点に進みましょう。難しいほど、スレッドの同期と相互排他についての全員の理解が深まります。 、ブロッキングキュー、条件変数 そこで、以下のようなマルチ生産・マルチ消費モデルのコードを直接実装し、2つのブロッキングキューを実現することで、スレッド間の同期と排他を実現します。共有リソースへのアクセスは引き続き維持できます。

ここに画像の説明を挿入
2.
異なるタスクを格納する 2 つのブロッキング キューを実装したいため、あらゆるタイプのオブジェクトを格納できるように、ブロッキング キューのクラス テンプレートを直接作成します。そこで、まず BlockQueue.hpp ファイルのコードを改善しましょう。 、ブロッキングキューのクラステンプレートコード。
ブロッキング キューの共有リソース アクセスの安全性を確保するにはロックが必要です。ブロッキング キューがいっぱいであるなど、実稼働スレッドが実稼働条件を満たしていない場合、現時点では実稼働スレッドは実稼働を継続すべきではありませんが、 go to cond コンシューマー スレッドが本番スレッドを起動するまでキューで待機するため、本番スレッドには pcond と呼ばれる独自のプロデュース cond が必要です。逆に、コンシューマにも同じことが当てはまり、コンシューマが消費条件を満たさない場合、コンシューマは独自の cond キューで待機する必要があるため、コンシューマは ccond と呼ばれる独自の concond を消費する必要もあります。したがって、BlockQueue クラスのプライベート メンバーには、_mutex mutex、_ccond、および _pcond の 2 つの条件変数が含まれている必要があります。また、ブロッキング キューの容量を記述する変数 (_maxcap) も必要であり、STL コンテナー queue<T> を追加します。 _q ;次に、定義されたすべてのブロッキング キューの最大容量が同じであることを望みます。そのため、_maxcap は変更不可能な静的メンバー変数として定義され、静的変数はクラス内でのみ宣言され、クラスの外で初期化されます。 static キーワードを追加せずに名前を初期化する場合のクラス。
ブロッキング キューが実装する必要があるインターフェイスは主に 4 つの部分で構成されています。ブロッキング キューで使用されるロック変数と条件変数はローカルであるため (オブジェクト自体は関数スタック内にあるため)、ミューテックスと 2 つの条件変数はコンストラクターで初期化する必要があります。 Frame) 条件 変数とロックはコンストラクターで初期化し、デストラクターで破棄する必要があります。
さらに、プッシュとポップの 2 つのインターフェイスを実装する必要があり、キュー内のプッシュ要素のセキュリティを確保するために、インターフェイスをロックおよびロック解除し、プッシュ条件が満たされているかどうかを判断する必要があります。キューがいっぱいの場合は、プッシュを続行せず、つまり生成を続行せず、pcond キューで待機します。条件が満たされた場合、待機実行フローがブロックされ、ウェイクアップされるのを待ちます。 STLqueue 要素のプッシュ インターフェイス プッシュを直接使用するのは非常に簡単です。要素をプッシュした後、コンシューマ スレッドをウェイクアップする必要があります。これは、キュー内にコンシューマが使用できる要素が少なくとも 1 つあるため、pthread_cond_signal を直接呼び出して ccond キュー内のスレッドをウェイクアップできるためです。最後のステップはロックを解除することです。
ポップの場合、STLqueue のポップ インターフェイスはポップされた要素を返さないため、出力パラメータを通じてポップされた要素の値を取得する必要があります。Push の実装ロジックと同様に、pop の条件はキュー内の要素が空であってはならず、空の場合は本番スレッドによってウェイクアップされるまで ccond キュー内で待機する必要があります。データがポップされた後は、キュー内に少なくとも 1 つの空の位置が存在する必要があるため、この時点で実稼働スレッドを起動し、実稼働スレッドに要素をプッシュさせ、最後にロックを解放することを忘れないでください。
インターフェイスの実装については、一般的なロジックはほぼ同じです。ただし、コードには特別な説明が必要な詳細がまだいくつかあります。pthread_cond_wait インターフェイスがクリティカル セクション内に配置されていることがわかっているため、スレッドは待機コードを実行する前にロックを保持します。スレッドが待機している間に他のスレッドがロックを適用してクリティカル セクションに入ることができるようにするため、pthread_cond_wait が実行されるときが呼び出されると、自動的にアトミックな方法でロックが解放され、自身をブロックして pcond キューに一時停止します。その後、キュー内のスレッドがウェイクアップされるとき、スレッドは pthread_cond_wait から逆方向に実行する必要があるため、この時点ではまだクリティカル セクション内にあるため、pthread_cond_wait が返されると、自動的にロックが再適用され、続行されます。クリティカル セクションでコードをリージョン内で逆方向に実行します。さらに、複数の生産と複数の消費のシナリオでは、誤ったウェイクアップが発生する可能性があるため、判定ロジック ステートメントは if ではなく while である必要があります。たとえば、ブロードキャストはすべての生産スレッドをウェイクアップしますが、実際の空の位置は 1 つだけです。 wakeup 後のこの時点では、特定のスレッドがロックを競合します。要素を入力した後、キューがいっぱいになり、ロックを解放します。別のスレッドがロックを競合した後、if ロジックの場合は、満たされているかどうかの再判定はしませんが、要素を直接プッシュするとセグメントフォルトが発生して境界外アクセスが発生するため、覚醒したスレッドが確実にプッシュであるかどうかをwhileループで判定します。条件が満たされた場合の要素。相手を起こしてロックを解除する順序は、相手を起こしても相手がロックを持っていない場合はブロックして待つ必要があるため、自由に設定できます。ロックを解放します。先にロックを解放しても、相手が目覚めていないため、まだロックを取得できません。したがって、これら 2 つのインターフェイスの呼び出し順序はインターフェイスの機能に影響を与えないため、誰でも書くことができます初め。

ここに画像の説明を挿入

3.
main 関数の上位層呼び出しのロジックは、複数の生成スレッドと複数の消費スレッドを作成し、2 つのブロッキング キューを使用して計算タスクとストレージ タスクの生成と消費を完了することです。そのため、BlockQueues クラスをカプセル化しました。クラス内で 2 つの Blockqueue をカプセル化します。1 つは計算タスクの保存用、もう 1 つはタスクの保存と保存用です。タスクは実際にはクラス オブジェクトなので、BlockQueue のクラス テンプレート パラメーターはそれぞれ C 計算と S 保存です。次に、ブロッキング キュー、複数の本番スレッドとコンシューマ スレッドを作成し、スレッドを保存します。実行に対応するスレッド関数は、生成、消費、保存であり、その後、BlockQueues 型のポインターを 3 つのスレッド関数に渡します。これにより、スレッド関数内で、BlockQueues クラスの 2 つのポインター メンバーを使用して、push および Pop を呼び出すことができます。ブロッキング キュー インターフェイスで、プッシュおよびポップしてタスクを完了します。
プロデュースでは、CalTask​​ クラスのオブジェクトを定義し、このタスク オブジェクトを c_bq (計算ブロックキュー) ブロッキング キューにプッシュする必要があります。オブジェクトを構築するには、2 つのオペランドと演算子が必要で、実行するための mymath 関数が必要です計算も必要です。これらのタスク オブジェクトがすべて呼び出し可能なオブジェクトであることを期待しているため、ポインターが入ります。コンシューマーが使用するとき、キューからタスクを取得した後、() 演算子のオーバーロードを呼び出して計算タスクを完了できます。より明確に見ると、CalTask​​ クラスは、1+1=? などの計算タスクの名前を実際に出力する toTaskString 関数も実装しています。これが実行されている端末の生成スレッド関数であることを確認してください。演算演算子には多くの種類があるため、文字列オブジェクト oper に 5 種類の演算子が含まれるように定義し、rand で乱数を生成して 2 つのオペランドの生成をシミュレートします。
消費では、タスクは比較的困難です。計算タスク CalTask​​ を消費する必要があり、ブロッキング キューを保存するためにタスク SaveTask (ブロックキューの保存) を生成して s_bq に保存する必要もあります。消費タスクは、出力パラメータを送信する必要があります。つまり、空の CalTask​​ オブジェクト t をポップ インターフェイスに渡し、ポップが終了した後、その t オブジェクトが c_bq から取り出されたタスク オブジェクトになります。キュー内の CalTask​​ オブジェクトを取り出した後は、実際には非常に簡単ですこのオブジェクトは実際にはファンクター オブジェクトであり、 () を通じて直接呼び出すことができるため、消費します。次に、タスクを生成してブロッキング キューに保存します。計算タスクと同様に、保存タスク オブジェクトも呼び出し可能オブジェクトとして実装する必要があるため、保存スレッドがタスク オブジェクトを取り出すときに、タスク オブジェクトを直接呼び出すこともできます。 () を介して SaveTask クラスの操作を実行します。関数はタスク オブジェクトの消費を実現するためにオーバーロードされます。したがって、SaveTask タスク オブジェクトを構築するときは、文字列タイプのオブジェクトである計算タスクの名前を渡す必要があります。これにより、タスクがファイルに保存されるときに、対応する保存された計算の名前が確認できるようになります。タスクはファイル内にあり、リターン 関数ポインタ Save を渡す必要があります。この関数の実際の機能は、ファイル操作を実行し、計算タスクの名前をディスク ファイルに保存することです。
saveの場合も理由は同じで、s_bqのsaveタスクオブジェクトを取り出したい場合は出力パラメータで取り出す必要があるため、s_bqのpopインターフェースにSaveTaskクラスの空のオブジェクトtを渡しています。 Pop 呼び出し後、t s_bq から取り出した保存タスクの呼び出し可能オブジェクトであるため、使用するときは、() を介して SaveTask クラスの () オーバーロード関数を直接呼び出して保存タスクを完了し、対応する名前を呼び出します。計算タスクはディスク ファイルに保存されます。
3 つのスレッド関数の具体的な実装が完了しました。MainCp.cc ファイルには注意すべき詳細もいくつかあります。デッドロックの問題を回避する方法について話したときに、コードを記述するときに注意する必要がある点の 1 つは、マルチスレッド プログラミング、特にコードのロックでは、最初と最後で適用されるリソースを統合しようとすることであると述べたことを思い出してください。一度適用してください。まったく予期しないエラーが発生する可能性があるため、コード内で必要な場合は適用しないでください。害、害、人は人に教えることはできませんが、正確に教えることはできます、そうです、最初にリソースを申請しなかったのは私です、それで解決できないバグにも遭遇しました、本当に長い間頭が痛かったです。2 番目のブロッキング キューを初期化するコード行が生成スレッドと消費スレッドの作成後に配置されている場合 (つまり、コメントアウトした場所)、そのまま実行してください。 a while 正常に動作し、しばらくするとセグメントエラーが報告されるなど、マルチスレッドを初めて使用する初心者にとっては、そのまま親切さが満載です。このような現象が発生する理由は、メイン スレッドが十分に高速に実行されている場合、消費スレッドが保存タスクを s_bq に配置する前に、メイン スレッドの s_bq が初期化されたばかりであるように見えるため、プログラムは正常に実行されます。しかし、メインスレッドが少しフルで実行されている場合、s_bq が初期化されていないように見え、消費スレッドが保存タスクを s_bq に配置しましたが、s_bq はメモリを割り当てていないワイルド ポインタであるため、レポートが表示されます。この時点ではワイルド ポインタにアクセスしたためセグメンテーション違反が発生しました。したがって、ベテランの皆さんは、使用する必要のあるスペース リソースを最初に割り当てるようにしてください。割り当てに慣れるまで待たずに、マルチスレッドでは間違いを見つけるのが簡単ではありません。

ここに画像の説明を挿入

4.
最後のファイルは Task.hpp で、このファイルは実装したい計算タスク クラスと格納タスク クラス、および計算メソッドと格納メソッドです。コンピューティング タスク クラスには 2 つのコンストラクターが実装されます。1 つは空のコンストラクターで、出力パラメーターとしてメインで空のオブジェクトを構築し、それをブロッキング キューのポップ インターフェイスに渡すために使用されます。もう 1 つは実際のタスク オブジェクトを構築します。クラス メンバーに必要なのは、2 つのオペランドと 1 つの演算子、およびラッパーだけです。ラッパーは多くの呼び出し可能なオブジェクトをラップできるため、ファンクター オブジェクト、ラムダ式、関数ポインターを作成して渡す場合、コンストラクターの場合、ラッパーの型は次のようになります。受け入れられ、これらすべてのプライベート メンバーはコンストラクター内で初期化できます。さらに、() 演算子オーバーロードと文字列のタスク名を返す toTaskString 関数を実装する必要があります。呼び出し可能なオブジェクトの計算結果を返すために、() 演算子オーバーロードは内部で mymath のメソッドをコールバックし、計算結果は snprintf 関数によって実行され、文字列はバッファにフォーマットされてから、そのバッファを使用して関数が返す文字列オブジェクトが構築されます。toTaskString は、コンピューティング タスクの名前をバッファーにフォーマットし、バッファーから構築された文字列オブジェクトも返します。
mymath 関数の実装については説明しません。2 つのオペランドの計算は switch case ステートメントを使用して実現できます。これはまさに入門レベルのコード実装と見なすことができます。
SaveTask クラスのメンバー変数には、保存されたコンピューティング タスク名 _message が含まれます。このタスク名は、実際には、CalTask​​ の () 演算子オーバーロード関数によって返される文字列オブジェクトであり、SaveTask のコンストラクターに渡されます。他のメンバー変数は、 「wrapper」。タスク名をファイルに書き込むファイル操作メソッド Save 関数ポインターをラップするために使用されます。同様に、メインで Pop が呼び出されたときに、空の出力パラメーターを使用してタスクを SaveTask オブジェクトに書き込むには、空のコンストラクターを実装する必要があります。タスクの消費を実現するために、通常どおり、() 演算子のオーバーロードも実装します。ラッパーでラップされた呼び出し可能オブジェクトをコールバックするだけです。
Save の実装に関しては、通常の C 言語のファイル操作と同じで、fopen でファイルを開き、fclose でファイルを閉じ、fputs でファイルを書き込むだけで、それほど難しくありません。

ここに画像の説明を挿入
5.
これまで、ダブルブロッキングキュー全体によって実装された複数生産および複数消費モデルについて説明してきましたが、以下はプログラムの実行結果です。コンピューティングタスクの生産と消費、および生産がうまく実現されました。複数のプロデューサーと複数のコンシューマーによるマルチスレッド シナリオで実装された生産および消費モデルです。これが実現できる理由は、共有リソースへのマルチスレッドの相互排他アクセスを保証するロックと、共有リソースへのマルチスレッドの相互排他アクセスの同期を保証する条件変数があるためです。

ここに画像の説明を挿入

2. 生産および消費モデルはどこで効率的ですか? (他のマルチスレッド同時または並列取得タスクおよび実行タスクには影響しません)

1.
上記のコードを作成した後、非常に重要な質問に答える必要があります。それは、なぜ生産および消費モデルが効率的なのかということです。彼がこれほど有能なところを見たことがありません。ブロッキングキューの共有リソースにアクセスするときも、相互排他的な方法でアクセスする必要はありませんか? なぜ生産と消費のモデルが効率的だと言えるのでしょうか?
本当!あなたの言ったことは何も間違っていません、とても正しいです!しかし、実際の生産および消費モデルは、ブロック キューに要素を入れたり、ブロック キューから要素を取得したりする際に、まったく効率的ではありません。これは、特定のスレッドがタスクをブロッキング キューに入れる場合に効率的であり、他のスレッドがタスクを取得する際に影響を与えず、特定のスレッドがブロッキング キューからタスクを取得する場合、他のスレッドがタスクを実行する際に影響を与えません。
今日作成したブロッキング キューは、実行や取得が簡単な、重要ではないコンピューティング タスクや保存タスクを保存しているだけですが、将来的には、スレッドは実際にデータベース、ネットワーク、周辺機器などから大規模なタスクを取得するようになるでしょう。ユーザーデータを処理する必要があるか? タスクを取得して実行するには時間がかかります。
生産および消費モデルの効率は効率的です。スレッドの 1 つが相互排他的な方法でブロッキング キューからタスクを取得したり、タスクを取得したりしても、他のスレッドがタスクを取得したりタスクを実行したりするときに、他のスレッドにはまったく影響しません。マルチスレッド タスクを同時または並行して実行しており、効率が非常に高いです。
一言でまとめると、生産消費モデルは、ブロックキューにタスクを入れたりブロックキューからタスクを取り出したりする際には効率的ではありませんが、特定のスレッドがタスクをブロックキューに入れたり入れたりするときに、他のスレッドに影響を与えないという点では非常に効率的です。スレッド タスクを取得し、タスクを同時または並行して実行します

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/erridjsis/article/details/130341148