これはやや話題の匂いがしますが、軌道に乗って戻そうと思います。
プリエンプティブマルチタスキングとは、オペレーティングシステムまたはカーネルが現在実行中のスレッドを中断し、スケジューリングヒューリスティックに基づいて別のスレッドに切り替えることができることを意味します。ほとんどの場合、実行中のスレッドにはシステム上で他のことが行われているという概念はありません。これがコードにとって意味することは、カーネルがスレッドの途中でスレッドを中断することを決定した場合マルチステップ操作(PWM出力の変更、新しいADCチャネルの選択、I2Cペリフェラルからのステータスの読み取りなど)と、これら2つのスレッドが互いに干渉しないように、しばらく別のスレッドを実行します。
任意の例:マルチスレッドの組み込みシステムを初めて使用し、I2C ADC、SPI LCD、およびI2C EEPROMを備えた小さなシステムがあるとします。ADCから読み取り、EEPROMにサンプルを書き込むスレッドと、最後の10個のサンプルを読み取り、それらを平均してSPI LCDに表示するスレッドの2つを用意することをお勧めします。経験の浅いデザインは、次のようになります(非常に単純化されています)。
char i2c_read(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_READ);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
char i2c_write(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_WRITE);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
adc_thread()
{
int value, sample_number;
sample_number = 0;
while (1) {
value = i2c_read(ADC_ADDR);
i2c_write(EE_ADDR, EE_ADDR_REG, sample_number);
i2c_write(EE_ADDR, EE_DATA_REG, value);
if (sample_number < 10) {
++sample_number;
} else {
sample_number = 0;
}
};
}
lcd_thread()
{
int i, avg, sample, hundreds, tens, ones;
while (1) {
avg = 0;
for (i=0; i<10; i++) {
i2c_write(EE_ADDR, EE_ADDR_REG, i);
sample = i2c_read(EE_ADDR, EE_DATA_REG);
avg += sample;
}
/* calculate average */
avg /= 10;
/* convert to numeric digits for display */
hundreds = avg / 100;
tens = (avg % 100) / 10;
ones = (avg % 10);
spi_write(CS_LCD, LCD_CLEAR);
spi_write(CS_LCD, '0' + hundreds);
spi_write(CS_LCD, '0' + tens);
spi_write(CS_LCD, '0' + ones);
}
}
これは非常に粗雑で高速な例です。このようにコーディングしないでください!
ここで、プリエンプティブマルチタスクOSは、これらのスレッドのいずれかをコードの任意の行(実際には任意のアセンブリ命令)で一時停止し、他のスレッドに実行時間を与えることができます。
それについて考えてください。OS adc_thread()
がEEアドレスの設定と実際のデータの書き込みの間に中断することを決定した場合に何が起こるか想像してみてください。lcd_thread()
I2C周辺機器をいじって実行し、必要なデータを読み取り、adc_thread()
再び実行するようになったとき、EEPROMは以前と同じ状態になりません。物事はまったくうまくいきません。さらに悪いことに、それはほとんどの場合でも動作するかもしれませんが、常に動作するわけではありません。
これは最良の例です。OSはのコンテキストを横取りしてi2c_write()
からadc_thread()
実行を再開することを決定するかもしれませんlcd_thread()
!物事は非常に速く非常に乱雑になります。
プリエンプティブなマルチタスク環境で動作するコードを作成するときは、ロックメカニズムを使用する必要がありますで動作するコードを作成する場合、を使用して、コードが不適切なタイミングで中断された場合にすべての地獄が崩れないようにする必要があります。
一方、協調マルチタスクは、各スレッドが実行時間を放棄するタイミングを制御することを意味します。コーディングは簡単ですが、すべてのスレッドが実行に十分な時間を確保できるように、コードを慎重に設計する必要があります。別の不自然な例:
char getch()
{
while (! (*uart_status & DATA_AVAILABLE)) {
/* do nothing */
}
return *uart_data_reg;
}
void putch(char data)
{
while (! (*uart_status & SHIFT_REG_EMPTY)) {
/* do nothing */
}
*uart_data_reg = data;
}
void echo_thread()
{
char data;
while (1) {
data = getch();
putch(data);
yield_cpu();
}
}
void seconds_counter()
{
int count = 0;
while (1) {
++count;
sleep_ms(1000);
yield_cpu();
}
}
そのコードは思ったとおりに機能しません。また、機能しているように見えても、エコースレッドのデータレートが増加すると機能しません。繰り返しますが、少し見てみましょう。
echo_thread()
バイトがUARTに現れるのを待ってからそれを取得し、書き込む余地ができるまで待ってから書き込みます。その後、他のスレッドに実行の順番を与えます。seconds_counter()
カウントをインクリメントし、1000ms待機してから、他のスレッドに実行の機会を与えます。その間に2バイトがUARTに到着した場合sleep()
、CPUが他のことをしている間、仮想のUARTに文字を格納するFIFOがないため、それらを見逃す可能性があります。
この非常に貧弱な例を実装する正しい方法yield_cpu()
は、ビジーループがある場所に置くことです。これは物事を進めるのに役立ちますが、他の問題を引き起こす可能性があります。たとえば、タイミングが重要であり、予想よりも時間がかかる別のスレッドにCPUを譲る場合、タイミングをスローすることができます。プリエンプティブマルチタスクOSでは、すべてのスレッドが正しくスケジュールされるようにスレッドを強制的に中断するため、この問題は発生しません。
さて、これはタイマーとバックグラウンドループと何の関係があるのでしょうか?タイマーとバックグラウンドループは、上記の協調マルチタスクの例に非常に似ています。
void timer_isr(void)
{
++ticks;
if ((ticks % 10)) == 0) {
ten_ms_flag = TRUE;
}
if ((ticks % 100) == 0) {
onehundred_ms_flag = TRUE;
}
if ((ticks % 1000) == 0) {
one_second_flag = TRUE;
}
}
void main(void)
{
/* initialization of timer ISR, etc. */
while (1) {
if (ten_ms_flag) {
if (kbhit()) {
putch(getch());
}
ten_ms_flag = FALSE;
}
if (onehundred_ms_flag) {
get_adc_data();
onehundred_ms_flag = FALSE;
}
if (one_second_flag) {
++count;
update_lcd();
one_second_flag = FALSE;
}
};
}
これは、協調スレッドの例にかなり近いように見えます。イベントを設定するタイマーと、イベントを検索してアトミックに動作するメインループがあります。ADCとLCDの「スレッド」が互いに干渉することを心配する必要はありません。一方が他方を中断することはないからです。それでも、「スレッド」に時間がかかりすぎることを心配する必要があります。たとえば、get_adc_data()
30msかかるとどうなりますか?キャラクターを確認してエコーする3つの機会を逃すことになります。
ループ+タイマーの実装は、手元のタスクに固有のコードを設計できるため、協調マルチタスクマイクロカーネルよりも実装がはるかに簡単な場合がよくあります。各サブシステムに非常に具体的かつ予測可能な方法でタスクを実行する時間を与える固定システムを設計するほど、実際にはマルチタスクではありません。協調的マルチタスクシステムでさえ、各スレッドに汎用タスク構造を持たなければならず、実行する次のスレッドは非常に複雑になる可能性があるスケジューリング機能によって決定されます。
3つのシステムすべてのロックメカニズムは同じですが、それぞれに必要なオーバーヘッドはまったく異なります。
個人的に、私はほとんど常にこの最後の標準であるループ+タイマー実装にコーディングしています。スレッドは非常に控えめに使用する必要があるものだと思います。記述とデバッグがより複雑になるだけでなく、オーバーヘッドも大きくなります(プリエンプティブなマルチタスクマイクロカーネルは、愚かな単純なタイマーとメインループイベントフォロワーよりも常に大きくなります)。
スレッドで作業している人なら誰でも感謝するということわざもあります。
if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls
:-)