並行性
プログラムの異なる部分が様々なタイミングで実行されたり、アウトオブオーダに実行されると、並行性が発生します。 組込みでは、次のものが該当します。
- 割り込みが発生するたびに実行される割り込みハンドラ
- マイクロプロセッサがプログラムの一部を定期的にスワップする様々な形式のマルチスレッド
- システムによっては、各コアがプログラムの異なる部分を同時に独立して実行できるマルチコアマイクロプロセッサ
多くの組込みプログラムは割り込みを処理する必要があるため、早かれ遅かれ、並行性は発生します。 割り込みは、捉えにくく、難しいバグが数多く発生し得る場所でもあります。 幸運なことに、Rustは正しいコードを書く助けになる抽象化と安全性保証とを、いくつか提供しています。
並行性なし
組込みプログラムの最も簡単な並行性は、並行性がないことです。ソフトウェアは1つの動作し続けるメインループからなり、割り込みも発生しません。 時には、これが手元の問題の最適解かもしれません。 通常、ループは何か入力を受け付け、何らかの処理を行い、何かを出力します。
#[entry] fn main() { let peripherals = setup_peripherals(); loop { let inputs = read_inputs(&peripherals); let outputs = process(inputs); write_outputs(&peripherals, outputs); } }
並行性がないため、プログラム間でのデータ共有や、ペリフェラルへのアクセス同期に悩む必要はありません。 このような単純なアプローチに逃れることができるのであれば、素晴らしい解決策かもしれません。
グローバルでミュータブルなデータ
組込みでないRustと異なり、通常、ヒープ領域を作成し、そのデータへの参照を新しく作成したスレッドに渡す、というような贅沢はできません。 代わりに、割り込みハンドラはいつでも呼び出される可能性があり、使用する共有メモリにアクセスする方法を知っていなければなりません。 最も低いレベルでは、 静的に割り当てられた ミュータブルなメモリを持つ必要があることを意味します。 このメモリは、割り込みハンドラとメインコードの両方が参照できます。
Rustでは、このようなstatic mut
変数への読み書きは、常にアンセーフです。
特別な注意を払わないと、レースコンディションを引き起こす可能性があります。
つまり、その変数へのアクセスが、さらにその変数にアクセスする割り込みによって、中断されるということです。
この動作によって、コード内に分かりにくいエラーが発生する可能性があります。 例えば、1秒毎に入力信号の立ち上がりエッジをカウントする組込みプログラム(周波数カウンタ)を考えてみましょう。
static mut COUNTER: u32 = 0; #[entry] fn main() -> ! { set_timer_1hz(); let mut last_state = false; loop { let state = read_signal_level(); if state && !last_state { // DANGER - Not actually safe! Could cause data races. // 危険。実際に安全ではありません。データ競合を引き起こす可能性があります。 unsafe { COUNTER += 1 }; } last_state = state; } } #[interrupt] fn timer() { unsafe { COUNTER = 0; } }
毎秒、タイマ割り込みはカウンタを0に戻します。同時に、メインループは信号を継続的に測定し、信号がローからハイに変わった時にカウンタをインクリメントします。
static mut
なCOUNTER
にアクセスするためには、unsafe
を使う必要があります。
これは、未定義動作を引き起こさないことを、コンパイラに約束するということです。
レースコンディションがどこにあるかわかりますか?
COUNTER
のインクリメントは、アトミックであることが保証されて いません 。
実際、ほとんどの組込みプラットフォームにおいて、この操作は、ロードし、インクリメントし、ストアする、という動作に分割されます。
割り込みがロードの後からストアの前に発生した場合、0に戻すリセットは、割り込みから復帰した後に無視されます。
そして、その期間では、2倍の遷移をカウントすることになります。
クリティカルセクション
それでは、データ競合についてどうすれば良いのでしょうか。
単純な方法は、割り込みが無効なコンテキストである クリティカルセクション を使うことです。
main
中のCOUNTER
へのアクセスを、クリティカルセクションでラッピングします。
そうすることで、COUNTER
のインクリメントが完了するまで、タイマ割り込みが発生しないようにできます。
static mut COUNTER: u32 = 0; #[entry] fn main() -> ! { set_timer_1hz(); let mut last_state = false; loop { let state = read_signal_level(); if state && !last_state { // New critical section ensures synchronised access to COUNTER // 新しいクリティカルセクションは、COUNTERへの同期アクセスを保証します cortex_m::interrupt::free(|_| { unsafe { COUNTER += 1 }; }); } last_state = state; } } #[interrupt] fn timer() { unsafe { COUNTER = 0; } }
この例ではcortex_m::interrupt::free
を使いました。他のプラットフォームでもクリティカルセクションのコードを実行するための、類似の方法があります。
これは、割り込みを無効にして、コードを実行し、再び割り込みを有効にすることと同じです。
タイマ割り込み内クリティカルセクションを置く必要がないことに注意して下さい。これは次の2つの理由からです。
- 読み込みをしないため、
COUNTER
に0を書くことは、競合の影響を受けません - いずれにせよ、
main
スレッドによって割り込まれることはありえません
COUNTER
がお互いに プリエンプション する複数の割り込みハンドラから共有される場合、
それぞれにクリティカルセクションが必要になるでしょう。
クリティカルセクションは、当面の問題を解決しますが、慎重に検討しなければならないunsafe
なコードをまだたくさん書いています。
その結果、必要以上にクリティカルセクションを使用することになり、オーバーヘッドと割り込みレイテンシおよびジッタをもたらします。
注目すべき点は、クリティカルセクションでは、割り込みが発生しないことが保証されますが、 マルチコアシステムでは、排他性の保証は提供されないことです。 他のコアは、割り込みでなくても、とあるコアと同じメモリにアクセスできてしまいます。 マルチコアを使う場合、より強力な同期プリミティブが必要になります。
アトミックアクセス
プラットフォームによっては、アトミック命令が利用できます。アトミック命令は、リードモディファイライト操作の保証を提供します。
特にCortex-Mの場合、thumbv6
(Cortex-M0)はアトミック命令を提供しませんが、thumbv7
(Cortex-M3以上)は提供します。
これらの命令は、全ての割り込みを無効化する手荒な方法の代替手段を提供します。
インクリメントを試みる時、ほとんどの場合は成功しますが、割り込まれた場合はインクリメント操作全体を自動的にやり直します。
このようなアトミック操作は、複数のコアにまたがっても安全です。
use core::sync::atomic::{AtomicUsize, Ordering}; static COUNTER: AtomicUsize = AtomicUsize::new(0); #[entry] fn main() -> ! { set_timer_1hz(); let mut last_state = false; loop { let state = read_signal_level(); if state && !last_state { // Use `fetch_add` to atomically add 1 to COUNTER // 自動的にCOUNTERに1を加えるために`fetch_add`を使います COUNTER.fetch_add(1, Ordering::Relaxed); } last_state = state; } } #[interrupt] fn timer() { // Use `store` to write 0 directly to COUNTER // COUNTERに直接0を書くために`store`を使います COUNTER.store(0, Ordering::Relaxed) }
ここで、COUNTER
は安全なstatic
変数です。AtomicUsize
のおかげでCOUNTER
の型は、割り込みを無効化することなく、
割り込みハンドラとメインスレッドの両方から安全に修正できます。
可能であれば、これはより良い解決方法です。しかし、あなたのプラットフォームではサポートされていないかもしれません。
Ordering
の注釈:これは、コンパイラとハードウェアがどのように命令の順番を入れ替えるか、に影響を与えます。
また、キャッシュの可視性にも影響します。ターゲットがシングルコアプラットフォームだと仮定すると、Relaxed
で十分であり、このケースでは最も効率の良い選択です。
より厳密なオーダリングでは、コンパイラはアトミック操作の前後にメモリバリアを発行します。
使用するアトミック操作によって、必要かもしれませんし、必要でないかもしれません!
アトミックモデルの正確な詳細は複雑であり、他の文書内でしっかりと説明されています。
詳細は、ノミコンのアトミックとオーダリングを参照して下さい。
抽象化、SendとSync
上記の解決方法のいずれも、これと言って満足いくものではありません。
どの解決方法もunsafe
ブロックを必要とし、非常に注意深くチェックしなければならず、人間工学的ではありません。
Rustではもっとうまくやれるはずです!
カウンタを、コード内のどこからでも安全に使えるインタフェースに抽象化することができます。 次の例では、クリティカルセクションカウンタを使いますが、アトミックと非常に良く似たことが実現できます。
use core::cell::UnsafeCell; use cortex_m::interrupt; // Our counter is just a wrapper around UnsafeCell<u32>, which is the heart // of interior mutability in Rust. By using interior mutability, we can have // COUNTER be `static` instead of `static mut`, but still able to mutate // its counter value. // カウンタはUnsafeCell<u32>の単なるラッパです。UnsafeCellはRustの内部可変性の重要要素です。 // 内部可変性を使用することで、COUNTERを`static mut`の代わりに`static`として持つことができます。 // しかし、依然として、カウンタの値は変更することができます。 struct CSCounter(UnsafeCell<u32>); const CS_COUNTER_INIT: CSCounter = CSCounter(UnsafeCell::new(0)); impl CSCounter { pub fn reset(&self, _cs: &interrupt::CriticalSection) { // By requiring a CriticalSection be passed in, we know we must // be operating inside a CriticalSection, and so can confidently // use this unsafe block (required to call UnsafeCell::get). // クリティカルセクションを引数として要求することで、クリティカルセクション内で // 実行されなければならないことがわかります。そのため、このアンセーフブロックを // 自信を持って使用できます(UnsafeCell::getの呼び出しに必要です)。 unsafe { *self.0.get() = 0 }; } pub fn increment(&self, _cs: &interrupt::CriticalSection) { unsafe { *self.0.get() += 1 }; } } // Required to allow static CSCounter. See explanation below. // 静的なCSCounterを許可するために必要です。以下の説明を参照して下さい。 unsafe impl Sync for CSCounter {} // COUNTER is no longer `mut` as it uses interior mutability; // therefore it also no longer requires unsafe blocks to access. // 内部可変性を使用するため、COUNTERは、もはや`mut`ではありません。 // 従って、アクセスの際に、アンセーフなブロックも必要なくなりました。 static COUNTER: CSCounter = CS_COUNTER_INIT; #[entry] fn main() -> ! { set_timer_1hz(); let mut last_state = false; loop { let state = read_signal_level(); if state && !last_state { // No unsafe here! // アンセーフはここでは必要ありません! interrupt::free(|cs| COUNTER.increment(cs)); } last_state = state; } } #[interrupt] fn timer() { // We do need to enter a critical section here just to obtain a valid // cs token, even though we know no other interrupt could pre-empt // this one. // 有効なcsトークンを得るため、ここでクリティカルセクションに入る必要があります。 // 他の割り込みがプリエンプションを起こさないと分かっていても必要です。 interrupt::free(|cs| COUNTER.reset(cs)); // We could use unsafe code to generate a fake CriticalSection if we // really wanted to, avoiding the overhead: // let cs = unsafe { interrupt::CriticalSection::new() }; // オーバーヘッドを避けるために、本当に必要であれば、偽のクリティカルセクションを生成する // アンセーフなコードを使うことができます。 // let cs = unsafe { interrupt::CriticalSection::new() }; }
unsafe
コードを慎重に検討された抽象の内側に移動しました。
そして、アプリケーションコードは、unsafe
ブロックを含んでいません。
この設計は、アプリケーションがCriticalSection
トークンを渡すことを要求します。
トークンは、interrupt::free
によってのみ、安全に生成することができます。
そのため、このトークンが渡されることを要求することで、自分自身でロックを実際にかけることなしに、クリティカルセクション内で動作していることを保証します。
この保証は、静的にコンパイラによって提供されます。cs
による実行時のオーバーヘッドはありません。
カウンタが複数ある場合、複数の入れ子になったクリティカルセクションなしに、同じcs
を与えることができます。
これは、Rustの並行性についても重要なトピックを提起します。Send
とSync
トレイトです。
the Rust bookを要約すると、安全に別のスレッドに移動できるとき、型はSendです。
一方、複数のスレッド間で安全に共有できるとき、型はSyncです。
組込みでは、割り込みがアプリケーションコードとは異なるスレッドで動作すると考えます。
そのため、割り込みとメインコードとの両方からアクセスされる変数は、Syncでなければなりません。
Rustのほとんどの型では、コンパイラによってSendとSyncの両方のトレイトが自動的に継承されます。
しかし、CSCounter
はUnsafeCell
を含んでいるため、Syncではありません。
従って、static CSCounter
を作ることはできません。static
変数は、複数のスレッドからアクセスされるため、Syncでなければなりません。
CSCounter
が実はスレッド間で共有しても安全なように処理していることを、コンパイラに伝えるため、Syncトレイトを明示的に実装します。
これまでのクリティカルセクションの使用と同様に、シングルコアのプラットフォームでのみ安全です。
マルチコアのプラットフォームでは、安全性を確保するためにさらなる取り組みが必要です。
ミューテックス
カウンタの問題に特有の便利な抽象化を行いましたが、並行性のために利用されるいくつかの抽象化が存在します。
そのような 同期プリミティブ の1つはミューテックス(mutex)です。mutexはmutual exclusionの略です。
ミューテックスは、私達のカウンタのような変数への排他アクセスを保証します。
あるスレッドは、ミューテックスの ロック(または 獲得)を試みます。
すると、すぐに成功するか、ロックが獲得されるのを待ってブロックするか、ミューテックスをロックできなかったエラーを返します。
そのスレッドがロックを保持している間、保護されたデータへのアクセスが許可されます。
そのスレッドが実行を完了すると、ミューテックスを アンロック(または 解放)することで、他のスレッドがミューテックスをロックできるようにします。
Rustでは、通常、アンロックを実装するためにDrop
トレイトを使用します。
これは、ミューテックスがスコープの外に到達すると、常に解放されることを保証するためです。
割り込みハンドラでミューテックスを使用するのはトリッキーです。割り込みハンドラ内でブロックすることは、通常、好ましくありません。 割り込みハンドラ内で、メインスレッドがロックを解放するのを待ってブロックすると、特に悲惨です。 なぜならば。デッドロック になるからです。(割り込みハンドラ内に実行がとどまるため、メインスレッドがロックを解放することは決してありません) デッドロックはアンセーフとは考えられていません。安全なRustでも発生する可能性があります。
この動作を完全に避けるため、カウンタの例で示すように、ロックのためにクリティカルセクションを必要とするミューテックスを実装できます。 クリティカルセクションがロックしている間続く限り、ミューテックスのロック/アンロックの状態を追跡することなしに、 ラップされた変数に排他的にアクセスできます。
これは実際にcortex_m
クレートで行われています!
それを使ってカウンタを書くことができます。
use core::cell::Cell; use cortex_m::interrupt::Mutex; static COUNTER: Mutex<Cell<u32>> = Mutex::new(Cell::new(0)); #[entry] fn main() -> ! { set_timer_1hz(); let mut last_state = false; loop { let state = read_signal_level(); if state && !last_state { interrupt::free(|cs| COUNTER.borrow(cs).set(COUNTER.borrow(cs).get() + 1)); } last_state = state; } } #[interrupt] fn timer() { // We still need to enter a critical section here to satisfy the Mutex. // ミューテックスを満たすために、ここでもクリティカルセクションに入る必要があります。 interrupt::free(|cs| COUNTER.borrow(cs).set(0)); }
今回はCell
を使っています。これは、RefCell
の同類で安全な内部可変性を提供するために使用されます。
既に、UnsafeCell
が、Rustの内部可変性の最下層であることを見てきました。
UnsafeCellは、値への複数のミュータブル参照の取得を可能としますが、アンセーフなコードでのみ使用できます。
Cell
はUnsafeCell
と似ていますが、安全なインタフェースを提供します。
Cellは参照を取得せず、現在値のコピーを取得するか、置き換えることだけを許可します。
CellはSyncでないため、スレッド間で共有できません。
これらの制約は安全に使えることを意味しますが、static
変数として直接使用できません。static
はSyncである必要があるからです。
では、なぜ上記の例はうまく動くのでしょうか?Mutex<T>
は、Cell
のようなSendなT
に対してSyncを実装します。
このことが、Cellをstaticで使うことを安全にします。なぜなら、クリティカルセクションの間だけ、その中身へのアクセスを提供するからです。
従って、全くアンセーフなコードなしに、安全なカウンタを手に入れることができます。
この方法は、カウンタのu32
のような単純な型に適しています。しかし、もっと複雑なCopyでない型についてはどうでしょうか?
組込みにおいて非常に一般的な例は、ペリフェラル構造体です。これは、通常Copyではありません。
そのためには、RefCell
に頼ることができます。
ペリフェラルの共有
svd2rust
および同様の抽象化を使って生成されるデバイスクレートは、ペリフェラルへの安全なアクセスを提供します。
これは、同時に1つのペリフェラル構造体インスタンスしか存在できないように強制することによって、もたらされます。
このことは、安全性を保証しますが、メインスレッドと割り込みハンドとの両方からペリフェラルにアクセスすることを難しくします。
ペリフェラルアクセスを安全に共有するため、上で見たようにMutex
を使うことができます。
また、RefCell
も必要です。RefCellは、実行時チェックを使って、同時に1つのペリフェラルへの参照だけが渡されるようにします。
実行時チェックは、普通のCell
よりもオーバーヘッドが大きくなりますが、
コピーではなく参照を受け渡しするため、同時に存在するのが1つだけであることを確認する必要があります。
最後に、メインコード内でペリフェラルを初期化した後、なんとかしてペリフェラルを共有変数に移動する方法が必要です。
これを実現するために、Option
型を使います。None
で初期化し、後でペリフェラルのインスタンスを設定します。
use core::cell::RefCell; use cortex_m::interrupt::{self, Mutex}; use stm32f4::stm32f405; static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> = Mutex::new(RefCell::new(None)); #[entry] fn main() -> ! { // Obtain the peripheral singletons and configure it. // This example is from an svd2rust-generated crate, but // most embedded device crates will be similar. // ペリフェラルのシングルトンを取得し、設定します。 // この例は、svd2rustで生成されたクレートから持ってきたものですが、 // ほとんどの組込みデバイスクレートは同様になります。 let dp = stm32f405::Peripherals::take().unwrap(); let gpioa = &dp.GPIOA; // Some sort of configuration function. // Assume it sets PA0 to an input and PA1 to an output. // 一連の設定をする関数です。 // PA0を入力、PA1を出力に設定すると仮定して下さい。 configure_gpio(gpioa); // Store the GPIOA in the mutex, moving it. // GPIOAをミューテックスに格納し、ムーブします。 interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA))); // We can no longer use `gpioa` or `dp.GPIOA`, and instead have to // access it via the mutex. // もはや`gpioa`や`dp.GPIOA`は使いません。 // 代わりに、ミューテックス経由でアクセスする必要があります。 // Be careful to enable the interrupt only after setting MY_GPIO: // otherwise the interrupt might fire while it still contains None, // and as-written (with `unwrap()`), it would panic. // MY_GPIOを設定した後にのみ、割り込みを有効にするように注意して下さい。 // そうしなければ、まだNoneが含まれている間に、割り込みが発生する可能性があります。 // (`unwrap()`を使用して)書き込まれると、パニックになるでしょう。 set_timer_1hz(); let mut last_state = false; loop { // We'll now read state as a digital input, via the mutex // ミューテックス経由で、デジタル入力としての状態を読み込みます。 let state = interrupt::free(|cs| { let gpioa = MY_GPIO.borrow(cs).borrow(); gpioa.as_ref().unwrap().idr.read().idr0().bit_is_set() }); if state && !last_state { // Set PA1 high if we've seen a rising edge on PA0. // PA0の立ち上がりエッジを検出した場合、PA1をハイに設定します。 interrupt::free(|cs| { let gpioa = MY_GPIO.borrow(cs).borrow(); gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit()); }); } last_state = state; } } #[interrupt] fn timer() { // This time in the interrupt we'll just clear PA0. // 今回は、割り込み内では単純にPA0をクリアするだけです。 interrupt::free(|cs| { // We can use `unwrap()` because we know the interrupt wasn't enabled // until after MY_GPIO was set; otherwise we should handle the potential // for a None value. // `unwrap()`を使うことができます。割り込みはMY_GPIOが設定されるまで有効化されないことを // 知っているためです。そうでなければ、Noneを処理しなければならないでしょう。 let gpioa = MY_GPIO.borrow(cs).borrow(); gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().clear_bit()); }); }
非常に多くのことを取り入れています。重要な部分を詳細に見ていきましょう。
static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> =
Mutex::new(RefCell::new(None));
ここでは、共有変数はRefCell
を内部に含むMutex
です。さらに、RefCellはOption
を含んでいます。
Mutex
はクリティカルセクションの間だけ、アクセスできるようにします。
その結果、普通のRefCell
はSyncでないにも関わらず、変数はSyncになります。
RefCell
は、GPIOA
を使うのに必要となる参照によって内部可変性を提供します。
Option
を使用すると、この変数を空の値に初期化できます。後で実際に変数を移動します。
ペリフェラルのシングルトンには静的にアクセスすることはできません。実行時のみアクセスできるため、Optionが必要とされます。
interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
クリティカルセクションの内部で、ミューテックスのborrow()
を呼んでいます。borrow()はRefCell
の参照を提供します。
その後、replace()
を呼び出して、RefCell
に新しい値をムーブします。
interrupt::free(|cs| {
let gpioa = MY_GPIO.borrow(cs).borrow();
gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
});
最後に、MY_GPIO
を安全で並行なやり方で使います。
クリティカルセクションは、通常通り割り込みの発生を防ぎ、ミューテックスを借用できます。
RefCell
は&Option<GPIOA>
を提供し、その借用がいつまで続くかを追跡します。
その参照がスコープ外になると、RefCell
が借用されなくなったことを示すため、更新されます。
&Option
の外にGPIOA
をムーブすることはできないので、as_ref()
を使って&Option<&GPIOA>
に変換します。
そうすると、最終的に、unwrap()
で&GPIOA
を取得できます。
&GPIOAにより、ペリフェラルを修正することができます。
ヒューッ!これは安全ですが、少し大げさすぎて扱いにくいです。他に方法はないのでしょうか?
RTFM
代替手段の1つは、RTFMフレームワークです。RTFMは、Real Time For the Massesの略です。
RTFMは、共有リソースが常に安全にアクセスされることを保証するために、静的な優先度を適用し、
static mut
変数(「リソース」)へのアクセスを追跡します。
この方法は、(RefCell
のように)常にクリティカルセクションに入り、参照カウントを使うというオーバーヘッドを必要としません。
デッドロックがないことを保証したり、時間とメモリのオーバーヘッドを極めて小さくするといった、多くの利点があります。
このフレームワークは他の機能も含んでいます。例えば、メッセージパッシングは明示的な共有状態の必要性を減らします。 また、タスクを指定した時間に実行するスケジュールする機能もあります。これは、周期タスクの実装に使えます。 詳しくはドキュメントを参照して下さい。
リアルタイムオペレーティングシステム
組込み向け並行性の異なる一般的なモデルとして、リアルタイムオペレーティングシステム(RTOS)があります。 現在、Rustではあまり検証されていませんが、従来の組込み開発では広く使用されています。 オープンソースの例として、FreeRTOSとChibiOSがあります。 これらのRTOSは、複数のアプリケーションスレッドを動作させる機能を提供しています。 スレッドが制御を明け渡す時(コオペレーティブマルチタスク)か、 周期タイマまたは割り込みに基づく時(プリエンプティブマルチタスク)に、CPUで実行するスレッドを切り替えます。 RTOSは、通常ミューテックスや他の同期プリミティブを提供します。また、DMAエンジンようなハードウェア機能を同時に使えることも多いです。
この本を書いている時点では、Rustで書かれたRTOSの例はそれほど多くありません。 しかし、興味深い分野ですので、この分野にご注目下さい!
マルチコア
組込みプロセッサにおいても、2個以上のコアを持つことがより一般的になってきています。
このことは、並行性をさらに複雑にします。(cortex_m::interrupt::Mutex
を含む)クリティカルセクションで使っている全ての例は、
他の実行スレッドは、割り込みスレッドだけであることを前提にしています。
しかし、マルチコアシステムにおいては、これは当てはまりません。
代わりに、マルチコア(SMP; symmetric multi-proccesingとも呼ばれます)向けに設計した同期プリミティブが必要になります。
通常、これまでに見たアトミック命令を使用します。 アトミック命令は、処理システムが全てのコア間でのアトミック性を維持してくれるためです。
これらのトピックを詳細に説明することは、この本のスコープ範囲外ですが、 一般的なパターンはシングルコアの場合と同じです。