ncs: マルチスレッド
2024/09/04
一昔前、組み込みソフトの仕事は起動処理を作ったりドライバーを作ったりして、最後にアプリがあった。 まあ、並行して進めるので最後にということもないのだが、下から上まで一から作ることが多かった。
比較的小さいマイコンで OS は載っていなかった。
なので、そういうのにかかりっきりな感じでサイズも大きくならない、CPU もそんなに速くない。
そういう状況なので、アプリも制御が主で複雑というよりはリソースが少ないのをやりくりするのが面倒というところだった。
しかし、今やどうかね。
そういう分野ももちろん残っているだろうが、マイコンメーカーはサンプルではなく標準で OS をセットにしているところも少なくない。
Nordic のマイコンが ncs/Zephyr を採用したのも最近の流れだろう。
OS を採用して頼りたくなるのはリソース管理、中でもタスク管理だろう(個人の感想です)。
メインループを 10msec ごとに回して、それをカウントして回数に達したらタスクを呼び出す、みたいなのは自分でやりたくないものだ。
というわけで、今回はマルチスレッドをやっていこう。
前置きが長くなって済まん。
Lesson 7 – Multithreaded applications
OS を使ってタスク管理できると楽になるとはいっても、そもそもタスク管理自体が面倒なものだ。
それを実装しなくて済んだからといっても、しくみを把握していないと
Threads
- Zephyrでは
main()
はオプションだそうな。- まあ、単なるエントリーポイントだからなんでもいいよね
int main()
だと戻り値がいるけど、使わないならvoid main()
でもよいのかな?- サブルーチン呼び出しではなく、
noreturn
- まあ、その分をケチってもたかが知れてるだろうし、変なことはしないほうがよいか
- サブルーチン呼び出しではなく、
- スレッドの種類
- cooperative thread(優先度値がマイナス)
- 優先度が低い上に用途がかなり制限されているので(very limited usage)ここでは説明しない。
- preemptible thread(優先度値が非マイナス)
- “plus” ではなく “non-negative” ということは 0以上ということかな?
- cooperative thread(優先度値がマイナス)
- スレッドの状態
- Running: CPUによって実行中
- Runnable(Ready): 実行待ち
- CPU時間待ちだけになっている
- Non-runnable(Unready): 実行不可
- 2つ以上の要因(1つは CPU時間かもしれない)で妨げられている
- 終了しているのもこの状態になるようだ
- スレッドの実行単位
- System thread
- Zephyr RTOS が初期化時に自動で実行するスレッド
- デフォルトで以下が実行される
- main thread: いわゆる
main()
相当。なかったらそのまま終了する。 - idle thread: 何もやることがないとき
- main thread: いわゆる
- User-created thread
- ユーザが作ったスレッド
- Workqueue thread
- kernel が持つ workqueu というオブジェクトに突っ込まれた作業を順番にやっていく
- 割り込みハンドラや緊急度が高い処理が workqueue に作業を突っ込んで負荷分散させるような使い方らしい
- “work item” はスレッドではなくスレッドで実行したい処理のことか?
- workqueue はシステムに複数持たせることができる
- デフォルトは “system workqueue” と呼ばれる
- system workqueue の処理を行うスレッドは System thread
- デフォルトは “system workqueue” と呼ばれる
- “other equal priority threads are not blocked for a long time” とはどういう意味だろう?
- work item ごとにスレッドを作るわけではなく、同じスレッドを使い回すような動きか
- FIFO なので優先度とか関係なくいつかは実行されるということを言いたい?
- System thread
- スレッドの優先度
- 値が小さい方が優先度が高い
- cooperative thread が Running になると、それを Non-runnable にするまでそのままになってしまう
- preemptible thread に関連付けられた優先度の数はデフォルトで 15(
CONFIG_NUM_PREEMPT_PRIORITIES
)- 優先度値ではなく、優先度の数よね?
- cooperative thread のそれはデフォルトで 16(
CONFIG_NUM_COOP_PRIORITIES
)
Scheduler
- Zephyr は定期的なタイマー割込み(tick)を持たないタイプだそうだ
- tickless RTOS
- イベント駆動のみ
- 再スケジュールは次に実行するスレッドの選択時に行われる
k_yield()
: Running –> Ready- kernel synchronize object(semaphore, mutex, alert): Unready –> Ready
- receiving thread: Waiting –> Ready
- time slice: Running –> Ready
- “Ready” の中から優先度で “Running” にするスレッドを選ぶのだろう
- tickがないのであれば、トリガになるのは割り込みハンドラか今動いているスレッドよりも高い優先度のスレッドが来た、あるいはスレッドが動いていない(idle thread)のどれかだろう。
ISR
割込みハンドラー。
スレッドの優先度は Arm の割り込みレベルと連動しているのだろうか?
多重割込みを許したのと同じような動作だと考えたのだが。
Exercise にあるようだからそこまで待ってみよう。
2024/09/05
続き。
Exercise
Exercise 1
- スタックサイズは
2^n
- 32bit CPU なので
4^n
が無難?- 正確にスタックサイズの必要量を計算することもないだろうし、がさっと確保するしかないか
- nRF5340 の RAM は 512KBある
- Zephyr にどのくらい RAM がいるのかわからんが、そんだけあれば 1つのスレッドに 1KB 当ててもバチは当たるまい
- 関係ないけど「low leakage RAM」ってなんだろう?
- nRF5340 Low Leakage RAM description
- 特にスタンバイやアイドルの時に RAM の消費電力が少なくなるそうだ
- 関係ないけど「low leakage RAM」ってなんだろう?
- 32bit CPU なので
- exercise の途中で一度動かしてみるが、そこでは
thread0
しか動かない- 優先度が同じなので、どちらか先に動いた方になるかと思ったが実装で先に出てくる順ということでよいのかな
- 動いていない方を “starved” しているという
- starved は「飢えた」「ひもじい」みたいな意味
k_yield()
を実行して他をディスパッチさせる- “Running” スレッドを “Runnable” にして実行待ち行列の後ろに突っ込むだけ
- これは優先度が同じかより高いスレッドに制御を移すためのもの
- 高い方は強制的に制御を奪ったりはしないのか
- 低い方に対しては
k_sleep()
だそうな
k_sleep()
すると確実にスレッドが動かない期間が発生するthread0
を 1000msec、thread1
を 500msec のスリープにしたところ、0->1->1->0->1->1->0…、のような順になった- 同じ優先度なので、待ちキューに入れば実行はされる
- そのまま
thread0
の優先度を8
に落としたが、同じペースで実行された- sleepだから待ちキューに入る前は “Unready” になるので優先度が低くても実行されるのか?
- 待ちキューに、既に待っているものよりも高い優先度のスレッドが入ったら優先されるのだろうか
- 関係ないこと
printk()
の改行は\r\n
でなく\n
だけでよいのか。TeraTerm だから?\r\n
はプリンタの制御に近いよな。行の先頭に戻るのと次の行に進めるのと。- 昔のMacは
\r
だったけど、あれは漢字Talk時代までだったのか。
- むかし組み込みLinuxアプリでpthreadで動かしているのをディスパッチしたいときに
sleep()
系のを短い時間で呼び出していたのだが、実はshed_yield()を呼べばよかったのだろうか。
Exercise 2
Exercise 1 では k_yield()
やk_sleep()
を呼び出すことで同じ優先度のスレッドを行き来するようにした。
そういうロジックを考えたくない場合に time slicing を有効にするそうだ。
- k_busy_wait()は実装をぱっと見ても分からんが、
for(;;
)なりwhile()
なりでループしているので、文字通り busy 状態で待つことになる- 単位は usec なのでここでは 1秒
- time slicing すると自動でスレッドがディスパッチするようだ
- その代わりタイミングを指定できないので今回だとコンソールログが乱れたりする
- CONFIG_TIMESLICING
- SysTick が有効になるのかと思ったが、デフォルトで
CONFIG_TIMESLICING=y
のようだ- デフォルトは
CONFIG_TIMESLICE_SIZE=0
なのでそのままビルドしても time slicing しない
- デフォルトは
- 同一優先度に対してしかディスパッチしないので、優先度が高いスレッドがいるとそれだけが動くようになってしまう
ロジックは考えなくてよいかもしれないが、スレッドに優先度があるようなアプリだと難あり。
Exercise 3
優先度高のスレッドによって低優先度スレッドに処理が回らない可能性があるので Workqueue で処理を逃がそう、という感じか。
exercise が分かりづらかったがこういうことだろうか。
K_THREAD_STACK_DEFINE()
で workqueueスレッドのスタックを用意struct k_work_q
型のグローバル変数を用意- これが workqueue のデータを保持している?
struct k_work
型の変数を用意thread0
のwhile
から抜けることがないのだが、グローバル変数である必要があるのか?- 構造体にして
name[]
を持たせているが、このアプリ内では使っていない
thread0
の中struct k_work_q
型変数の初期設定(スタックサイズ、スレッド優先度)struct k_work
型変数の初期設定(work itemになる関数との紐付け)emulate_work()
を直接呼ぶ代わりにk_work_submit_to_queue()
で workqueue に work item を追加
thread0
では 20msec ごとに k_work_submit_to_queue()
で workqueue に work item を追加する。
1回の処理(emulate_work()
)は 20msec 以上かかるのと、workqueue の優先度は thread1
よりも低いのとで workqueue での work item は thread1
が実行する emulate_work()
に1回程度処理を中断させられる。
thread0
の work item は結果を出力することがないので実際はどのくらいかかっているのか分からない。
thread0
は 20msec ごとに処理をキューに追加しているが、work item でかかる時間はそれより長いのでキューがいつかあふれるんじゃなかろうか。
API を見たがキューにたまった work item の数を取得するのはなさそうだった。
k_work_submit_to_queue()
がエラーを返すだろうが、事前に調べられるのだろうか?
Workqueue Threadsはページが長いのだが、API の種類がいくつあるためだ。
- work queue
-
work user queue
- Delayable Work
-
Triggered Work(poll queue)
- schedule/reschedule
よくわかってない。。。