並行性
並行性は、プログラムの異なる部分が異なる時点で実行されたり、 順不同で実行されたりし得るときに発生します。組み込みの文脈では、 これには次のものが含まれます:
- 関連する割り込みが発生するたびに実行される、割り込みハンドラ
- マイクロプロセッサがプログラムの各部分を定期的に切り替えて実行する、 さまざまな形式のマルチスレッド
- また、一部のシステムでは、各コアが同時にプログラムの別々の部分を 独立して実行できる、マルチコアのマイクロプロセッサ
多くの組み込みプログラムは割り込みを扱う必要があるため、並行性の 問題にはたいてい遅かれ早かれ直面します。また、そこでは見つけにくく 厄介なバグも数多く発生し得ます。幸いなことに、Rust は正しいコードを 書く助けとなるさまざまな抽象化と安全性保証を提供しています。
並行性なし
組み込みプログラムで最も単純なのは、並行性がまったくない場合です。 ソフトウェアは実行し続ける単一のメインループから成り、割り込みは まったくありません。問題によっては、これが完全に適していることも あります! 通常、ループは何らかの入力を読み取り、処理を行い、 何らかの出力を書き出します。
#[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 変数の読み書きは常に unsafe
です。特別な注意を払わないと、同じ変数にアクセスする割り込みに
よってその変数へのアクセスが途中で中断されるような、
レースコンディションを引き起こす可能性があるためです。
この挙動がどのようにコードに微妙なエラーを引き起こし得るかの例と して、各 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 {
// 危険 - 実際には安全ではありません!データ競合を引き起こす可能性があります。
unsafe { COUNTER += 1 };
}
last_state = state;
}
}
#[interrupt]
fn timer() {
unsafe { COUNTER = 0; }
}
毎秒、タイマー割り込みはカウンタを 0 に戻します。その一方で、
メインループは絶えず信号を測定し、low から high への変化を検出
するとカウンタをインクリメントします。COUNTER は static mut
なのでアクセスには 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 {
// 新しいクリティカルセクションにより、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, Cortex-M0+) が提供する
のはアトミックなロード命令とストア命令だけである一方、thumbv7
(Cortex-M3 and above) は完全な Compare and Swap (CAS) 命令を提供します。
これらの CAS 命令は、すべての割り込みを大がかりに無効化する方法に
代わる選択肢となります。インクリメントを試みると、ほとんどの場合
は成功しますが、途中で割り込まれた場合はインクリメント操作全体を
自動的に再試行します。これらのアトミック操作は、複数コア間でも
安全です。
```rust,ignore
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 {
// `fetch_add` を使って COUNTER に 1 をアトミックに加算する
COUNTER.fetch_add(1, Ordering::Relaxed);
}
last_state = state;
}
}
#[interrupt]
fn timer() {
// `store` を使って 0 を直接 COUNTER に書き込む
COUNTER.store(0, Ordering::Relaxed)
}
今回は COUNTER は安全な static 変数です。AtomicUsize
型のおかげで、COUNTER は割り込みハンドラと
メインスレッドの両方から、割り込みを無効化することなく安全に変更できます。
可能であれば、こちらのほうがより良い解決策です — ただし、プラットフォームで
サポートされていない場合があります。
Ordering について補足すると、これはコンパイラやハードウェアが命令を
どのように並べ替えうるかに影響し、キャッシュの可視性にも影響します。
ターゲットが単一コアプラットフォームであると仮定すると、この特定のケースでは
Relaxed で十分であり、最も効率的な選択です。より厳格な順序付けを指定すると、
コンパイラはアトミック操作の前後にメモリバリアを挿入します。アトミックを
何のために使っているかによって、これが必要な場合もあれば不要な場合もあります!
アトミックモデルの正確な詳細は複雑であり、ここ以外の場所で説明するのが適切です。
アトミックと順序付けの詳細については、nomicon を参照してください。
抽象化、Send、そして Sync
上記の解決策はどれも、特に満足のいくものではありません。これらは
非常に注意深く確認しなければならない unsafe
ブロックを必要とし、扱いやすくもありません。Rust なら、きっともっと良い方法があります!
このカウンタを、安全なインターフェースとして抽象化し、コード内の他のどこからでも安全に使えるようにできます。この例では、クリティカルセクションを使った カウンタを用いますが、アトミックでも非常によく似たことができます。
use core::cell::UnsafeCell;
use cortex_m::interrupt;
// このカウンタは単なる UnsafeCell<u32> のラッパーであり、これは 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) {
// CriticalSection を受け取ることを要求することで、必ず
// CriticalSection 内で動作していると分かるため、
// この unsafe ブロック(UnsafeCell::get の呼び出しに必要)を安心して使えます。
unsafe { *self.0.get() = 0 };
}
pub fn increment(&self, _cs: &interrupt::CriticalSection) {
unsafe { *self.0.get() += 1 };
}
}
// static CSCounter を許可するために必要です。説明は以下を参照してください。
unsafe impl Sync for CSCounter {}
// COUNTER は内部可変性を使っているため、もはや `mut` ではありません。
// したがって、アクセス時にもはや unsafe ブロックは不要です。
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 {
// ここに unsafe はありません!
interrupt::free(|cs| COUNTER.increment(cs));
}
last_state = state;
}
}
#[interrupt]
fn timer() {
// ここでは、有効な cs トークンを得るためだけに、クリティカルセクションへ
// 入る必要があります。他のどの割り込みも
// これをプリエンプトできないと分かっているにもかかわらずです。
interrupt::free(|cs| COUNTER.reset(cs));
// 本当にそうしたければ、オーバーヘッドを避けるために unsafe コードで
// 偽の CriticalSection を生成することもできます:
// let cs = unsafe { interrupt::CriticalSection::new() };
}
unsafe コードを慎重に設計した抽象化の内部へ移し、
これでアプリケーションコードには unsafe ブロックが一切含まれなくなりました。
この設計では、アプリケーションが CriticalSection トークンを渡す必要があります。
これらのトークンは interrupt::free によってのみ安全に生成されるため、それを
引数として要求することで、実際に自分でロック処理を行わなくても、
クリティカルセクション内で動作していることを保証できます。この保証は
コンパイラによって静的に提供されるため、cs に伴う実行時オーバーヘッドはありません。
複数のカウンタがある場合でも、複数のネストしたクリティカルセクションを必要とせず、
すべてに同じ cs を渡せます。
これは、Rust の並行性における重要なトピック、すなわち
Send and Sync トレイトにもつながります。Rust book を要約すると、型が Send
であるとは別のスレッドへ安全に移動できることを意味し、一方で Sync であるとは
複数のスレッド間で安全に共有できることを意味します。組み込みの文脈では、
割り込みはアプリケーションコードとは別のスレッドで実行されていると考えるため、
割り込みとメインコードの両方からアクセスされる変数は
Sync でなければなりません。
Rust のほとんどの型では、これらのトレイトは両方ともコンパイラによって自動的に導出されます。
しかし、CSCounter は UnsafeCell を含んでいるため
Sync ではなく、その結果 static CSCounter にはできません。static
変数は複数のスレッドからアクセスされうるため、必ず Sync でなければなりません。
実際には CSCounter がスレッド間で安全に共有できるよう配慮していることを
コンパイラに伝えるため、Sync トレイトを明示的に実装します。先ほどの
クリティカルセクションの利用と同様に、これは単一コアプラットフォームでのみ安全です。
複数コアでは、安全性を確保するためにさらに踏み込んだ対策が必要になります。
ミューテックス
ここではカウンタの問題に特化した便利な抽象化を作成しましたが、 並行性で使われる一般的な抽象化は他にも数多くあります。
そのような 同期プリミティブ の 1 つがミューテックスで、mutual exclusion の略です。
この仕組みは、今回のカウンタのような変数への排他的アクセスを保証します。スレッドは
ミューテックスの ロック(または 取得)を試みることができ、その結果は
ただちに成功するか、ロックを取得できるまで待機してブロックするか、あるいは
ミューテックスをロックできなかったというエラーを返すかのいずれかです。その
スレッドがロックを保持している間は、保護されたデータへのアクセスが与えられます。
スレッドでの処理が終わると、ミューテックスを アンロック(または 解放)し、
別のスレッドがそれをロックできるようにします。Rust では、通常
Drop トレイトを使ってアンロックを実装し、ミューテックスがスコープを
外れるときに常に解放されるようにします。
ミューテックスを割り込みハンドラで使うのは難しい場合があります。通常、 割り込みハンドラがブロックすることは許容されませんし、メインスレッドが ロックを解放するのを待ってブロックするのは特に致命的です。そうなると デッドロック してしまうからです(割り込みハンドラ内に実行が留まり続けるため、 メインスレッドは決してロックを解放しません)。デッドロックは unsafe とは見なされません。safe Rust でも起こりえます。
この挙動を完全に避けるには、カウンターの例と同じように、ロックするためにクリティカルセクションを必要とする mutex を実装できます。クリティカルセクションがロックと同じ長さだけ継続しなければならない限り、mutex の lock/unlock 状態を追跡する必要すらなく、ラップされた変数への排他的アクセスがあることを確信できます。
実際、これは `cortex_m` crate が私たちのためにやってくれています! カウンターはこれを使って次のように書けました。
```rust,ignore
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() {
// ここでも Mutex の要件を満たすためにクリティカルセクションに入る必要があります。
interrupt::free(|cs| COUNTER.borrow(cs).set(0));
}
現在は Cell を使っています。これは兄弟である RefCell とともに、安全な内部可変性を提供するために使われます。Rust における内部可変性の最下層である UnsafeCell はすでに見ました。これは、その値への複数の可変参照を取得できるようにしますが、それは unsafe code を使う場合に限られます。Cell は UnsafeCell に似ていますが、安全なインターフェースを提供します。現在の値のコピーを取得するか置き換えることだけを許可し、参照の取得は許可しません。また、Sync ではないため、スレッド間で共有することはできません。これらの制約により安全に使えますが、static は Sync でなければならないため、static 変数の中で直接使うことはできません。
では、なぜ上の例は動くのでしょうか? Mutex<T> は、Cell のように Send である任意の T に対して Sync を実装しています。これは、その内容へのアクセスをクリティカルセクション中にしか許可しないため、安全に実現できます。したがって、unsafe code をまったく使わずに安全なカウンターを得られるのです!
これは、カウンターの u32 のような単純な型には素晴らしい方法ですが、Copy ではないもっと複雑な型ではどうでしょうか? 組み込みの文脈で非常によくある例はペリフェラル構造体で、一般に Copy ではありません。
そのためには、RefCell を使います。
ペリフェラルの共有
svd2rust や同様の抽象化を使って生成されたデバイス crate は、ペリフェラル構造体のインスタンスが一度に 1 つしか存在できないことを強制することで、ペリフェラルへの安全なアクセスを提供します。これにより安全性は確保されますが、メインスレッドと割り込みハンドラの両方からペリフェラルへアクセスするのが難しくなります。
ペリフェラルへのアクセスを安全に共有するには、先ほど見た Mutex を使えます。さらに、RefCell も使う必要があります。これは、一度に 1 つの参照しかペリフェラルに対して渡されないことを実行時チェックで保証します。これは単純な Cell よりオーバーヘッドが大きいですが、コピーではなく参照を渡すため、一度に 1 つしか存在しないことを保証しなければなりません。
最後に、メインコードで初期化したあと、そのペリフェラルを共有変数に移動する方法も考慮しなければなりません。そのために、None で初期化し、あとでペリフェラルのインスタンスに設定する Option 型を使えます。
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() -> ! {
// ペリフェラルのシングルトンを取得し、それを設定します。
// この例は svd2rust で生成された crate のものですが、
// ほとんどの組み込みデバイス crate もこれと似ています。
let dp = stm32f405::Peripherals::take().unwrap();
let gpioa = &dp.GPIOA;
// 何らかの設定関数です。
// これが PA0 を入力に、PA1 を出力に設定すると仮定します。
configure_gpio(gpioa);
// GPIOA を mutex に保存し、ムーブします。
interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
// これ以降は `gpioa` や `dp.GPIOA` は使えなくなり、
// 代わりに mutex 経由でアクセスしなければなりません。
// 割り込みは MY_GPIO を設定したあとでのみ有効化するよう注意してください。
// そうしないと、まだ None を含んでいる間に割り込みが発生する可能性があり、
// このままのコードでは(`unwrap()` を使っているため)panic します。
set_timer_1hz();
let mut last_state = false;
loop {
// これ以降は mutex 経由で state をデジタル入力として読み取ります
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 {
// PA0 で立ち上がりエッジを検出したら PA1 を High にします。
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() {
// 今回は割り込みの中で PA0 をクリアするだけにします。
interrupt::free(|cs| {
// 割り込みは MY_GPIO が設定されたあとにしか有効化していないので
// `unwrap()` を使えます。そうでなければ 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));
共有変数は、Option を含む RefCell を囲んだ Mutex になっています。Mutex は、クリティカルセクション中にしかアクセスできないことを保証するため、通常の RefCell は Sync ではないにもかかわらず、この変数を Sync にします。RefCell は参照を使った内部可変性を提供し、GPIOA を使うためにこれが必要になります。Option によって、この変数を空の値で初期化し、あとで実際の変数をムーブできます。ペリフェラルのシングルトンには静的にはアクセスできず、実行時にしかアクセスできないため、これが必要です。
interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
クリティカルセクションの中では、mutex に対して 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 を使います。クリティカルセクションは通常どおり割り込みの発生を防ぎ、mutex を借用できるようにします。次に RefCell は &Option<GPIOA> を与え、それがどれだけの間借用されたままであるかを追跡します。その参照がスコープを抜けると、RefCell はもはや借用されていないことを示すよう更新されます。
GPIOA を &Option からムーブすることはできないため、それを as_ref() によって &Option<&GPIOA> に変換する必要があります。最後にこれを unwrap() すると、ペリフェラルを変更できる &GPIOA が得られます。
共有リソースへの可変参照が必要な場合は、代わりに borrow_mut と deref_mut
を使用する必要があります。以下のコードは、TIM2 タイマーを使用した例を示しています。
use core::cell::RefCell;
use core::ops::DerefMut;
use cortex_m::interrupt::{self, Mutex};
use cortex_m::asm::wfi;
use stm32f4::stm32f405;
static G_TIM: Mutex<RefCell<Option<Timer<stm32::TIM2>>>> =
Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
let mut cp = cm::Peripherals::take().unwrap();
let dp = stm32f405::Peripherals::take().unwrap();
// 何らかのタイマー設定関数。
// これが TIM2 タイマーとその NVIC 割り込みを設定し、
// 最後にタイマーを開始すると仮定する。
let tim = configure_timer_interrupt(&mut cp, dp);
interrupt::free(|cs| {
G_TIM.borrow(cs).replace(Some(tim));
});
loop {
wfi();
}
}
#[interrupt]
fn timer() {
interrupt::free(|cs| {
if let Some(ref mut tim)) = G_TIM.borrow(cs).borrow_mut().deref_mut() {
tim.start(1.hz());
}
});
}
ふう!これは安全ですが、少し扱いにくくもあります。ほかにできることは あるでしょうか?
RTIC
代替案の 1 つは RTIC framework です。これは Real Time Interrupt-driven Concurrency の略です。これにより静的優先度が強制され、static mut 変数
(「resources」)へのアクセスが追跡されることで、常にクリティカルセクションに入ったり
(RefCell のように)参照カウントを使用したりするオーバーヘッドを必要とせずに、
共有リソースへのアクセスが常に安全であることを静的に保証します。これには、デッドロックが
起こらないことを保証できることや、時間およびメモリのオーバーヘッドが非常に低いことなど、
多くの利点があります。
RTIC には非同期エグゼキュータも含まれているため、ソフトウェアタスクは async 関数となり、
通常の同期 API に加えて async API を使用できます。
このフレームワークには、明示的な共有状態の必要性を減らすメッセージパッシングや、 指定した時刻に実行されるようタスクをスケジュールする機能など、ほかの機能も含まれています。 これらは周期的タスクの実装に使用できます。 詳細については the documentation を確認してください。
Embassy
Embassy は、Rust に含まれている async / await 構文を並行性に活用することに焦点を当てたライブラリ群のエコシステムです。embassy の中核は
非同期エグゼキュータ
であり、一般的な MCU アーキテクチャの大半をサポートしています。
embassy は全部入りのアプローチも採っており、たとえば次のような多くのコンポーネントも提供しています。
- Time library
- time library のサポートも提供するさまざまな HAL ライブラリ
- 同期プリミティブのための embassy-sync
詳細については、website と book を参照してください。
リアルタイムオペレーティングシステム
組み込みの並行性におけるもう 1 つの一般的なモデルは、リアルタイムオペレーティングシステム (RTOS)です。現在のところ Rust ではまだあまり深く探究されていませんが、 従来の組み込み開発では広く使われています。オープンソースの例には FreeRTOS と ChibiOS があります。これらの RTOS は複数のアプリケーションスレッドの実行をサポートし、 スレッドが制御を明け渡したとき(協調的マルチタスクと呼ばれます)、 または定期タイマーや割り込みに基づいて(プリエンプティブマルチタスク)、 CPU がそれらの間を切り替えます。RTOS は通常、ミューテックスやそのほかの同期 プリミティブを提供し、しばしば DMA エンジンのようなハードウェア機能とも連携します。
本書の執筆時点では、参照できる Rust の RTOS の例はあまり多くありませんが、 興味深い分野なので今後に注目してください!
複数コア
組み込みプロセッサで 2 つ以上のコアを持つことが一般的になりつつあり、
これにより並行性にはさらに複雑さが加わります。クリティカルセクションを使用する
すべての例(cortex_m::interrupt::Mutex を含む)は、唯一の別実行スレッドが
割り込みスレッドであることを前提としていますが、マルチコアシステムでは
もはやそうではありません。代わりに、複数コア向けに設計された同期プリミティブ
(対称型マルチプロセッシングを表す SMP とも呼ばれます)が必要になります。
これらは通常、先ほど見たアトミック命令を使用します。処理システムが、 すべてのコアにわたってアトミック性が維持されることを保証するためです。
これらのトピックを詳細に扱うことは、現時点では本書の範囲を超えていますが、 一般的なパターンはシングルコアの場合と同じです。