Go インタビューの質問: ロックの実装原則 sync-mutex

Go では、sync.Mutex (ミューテックス ロック) と sync.RWMutex (読み取り/書き込みロック) という 2 つの主要なタイプのロックが実装されています。

この記事では主に sync.Mutex の使用法と実装原理を紹介します。

なぜロックが必要なのか

同時実行性が高い場合、または複数のゴルーチンが同時に実行される場合、次のシナリオのように、同じメモリが同時に読み書きされる可能性があります。

var count int
var mu sync.Mutex

func func1() {
    
    
	for i := 0; i < 1000; i++ {
    
    
		go func() {
    
    
			count = count + 1
		}()
	}
	time.Sleep(time.Second)
	fmt.Println(count)
}

出力値は 1000 であると予想されますが、実際の値は 948、965 などです。複数の実行の結果には一貫性がありません。
この現象が発生する理由は、count=count+1 の場合、各ゴルーチンの実行ステップが次のとおりであるためです。

  • 現在のカウント値を読み取る
  • カウント+1
  • カウント値を変更する

複数のゴルーチンを実行して値を同時に変更すると、後から実行されたゴルーチンが前のゴルーチンによる count の変更を上書きします。

Go では、同時実行プログラムによるパブリック リソースへのアクセスを制限するために最も一般的に使用される方法は、ミューテックス ロック (sync.mutex) です。

sync.mutex には 2 つの一般的なメソッドがあります。

  • Mutex.lock() はロックを取得するために使用されます。
  • Mutex.Unlock() は、ロックを解放するために使用されます
    。Lock メソッドと Unlock メソッドの間のコード セクションは、リソースのクリティカル セクションと呼ばれます。このセクションのコードはロックによって厳密に保護されており、スレッドセーフです。いつでも、せいぜい goroutine が実行されているだけです。

これに基づいて、上記の例は sync.mutex を使用して改善できます。

var count int
var mutex sync.Mutex

func func2() {
    
    
	for i := 0; i < 1000; i++ {
    
    
		go func() {
    
    
			mutex.Lock()
			count = count + 1
			mutex.Unlock()
		}()
	}
	time.Sleep(time.Second)
	fmt.Println(count)
}

出力結果は1000です。

ゴルーチンが mutex.lock() メソッドを実行するとき、他のゴルーチンがロック操作を実行するとブロックされ、現在のゴルーチンが mutex.unlock() メソッドを実行してロックを解放するまで、他のゴルーチンはロックを取得し続けません。ロックの実行。

実施原則

sync.Mutex のデータ構造
Go における sync.Mutex の構造は次のとおりです。

type Mutex struct {
    
    
	state int32
	sema  uint32
}

Sync.Mutex は 2 つのフィールドで構成されます。state はミューテックス ロックの現在の状態を示すために使用され、sema はロック ステータスのセマフォを制御するために使用されます。これら 2 つの分野の説明を読んだ後、すべての道士はそれらを理解しているように見えるかもしれませんが、理解していないかもしれないと私は信じています。これら 2 つのフィールドが何を行うのか詳しく理解しましょう。
ミューテックスロック状態には主に以下の 4 つの状態が記録されます。

waiter_num : 現在このロックの取得を待機しているゴルーチンの数を記録
starving : 現在のロックが飢餓状態かどうか (ロック解除の飢餓状態については後で詳しく説明します) 0: 通常の状態 1: 飢餓状態 wakeen
:ゴルーチンが停止しているかどうか現在のロックが目覚めました。0: 起動されているゴルーチンがない; 1: ロック処理中のゴルーチンがある
Locked : 現在のロックがゴルーチンによって保持されているかどうか。0:未保持 1:既に保持
セマフォの役割:
ロックを保持しているゴルーインがロックを解除するとセマフォが解放され、ロックを掴むことでブロックされていたゴルーインを目覚めさせ、ロックを取得します。ロック。

2つのロックモード

ミューテックス ロックは、通常モードと飢餓モードという 2 つの主なモードで設計されています。

スターベーションモードが導入される理由は、ゴルーチンによるミューテックスロックの取得の公平性を確保するためです。いわゆる公平性とは、実際には、複数のゴルーチンがロックを取得するときに、ゴルーチンがロックを取得する順序がロックを要求する順序と一致していれば公平であることを意味します。
通常モードでは、待機キュー内でブロックされているすべてのゴルーチンが順番にロックを取得しますが、待機キュー内のゴルーチンが起動されると、このゴルーチンは直接ロックを取得せず、ロックを要求した新しいゴルーチンと競合します。通常、新規にロックを要求したゴルーチンの方がロックを取得しやすいのは、新規にロックを要求したゴルーチンが実行のためにCPUスライスを占有しており、ロックを取得するロジックが実行できる可能性が高いためです。直接実行されます。

スターベーション モードでは、新たにロックを要求したゴルーチンはロックを取得しませんが、キューの最後に参加してロックの取得を待機します。
空腹モードのトリガー条件:

  • goroutine が 1ms を超えてロックを待機すると、ミューテックス ロックはスターベーション モードに切り替わります。

空腹モードの解除条件:

  • ロックを取得したゴルーチンが、ロックを待っているキュー内の最後のゴルーチンである場合、ミューテックス ロックは通常モードに切り替わります。
  • ロックを取得するゴルーチンの待ち時間が1ms以内の場合、ミューテックスロックは通常モードに切り替わります。

予防

  1. goroutine で Lock() が正常に実行された後は、再度ロックしないでください。そうしないと、パニックが発生します。
  2. ロックを解除するために Lock() の前に Unlock() を実行するとパニックが発生します。
  3. 同じロックに対して、1 つのゴルーチンで Lock を実行し、ロックに成功した後、別のゴルーチンで Unlock を実行してロックを解除することができます。

おすすめ

転載: blog.csdn.net/m0_73728511/article/details/133011077