Semaphore(セマフォ)とは?FreeRTOSのタスク同期を初心者向けに解説

組み込み基礎

RTOSでは、複数のタスクを動かすだけでなく、タスク同士の実行タイミングを合わせたり、イベントが発生するまでタスクを待たせたりする必要があります。

たとえば、ボタンが押されたら処理を開始するタスクを考えてみましょう。

ボタンが押される
      ↓
GPIO割り込み
      ↓
???
      ↓
待機していたTaskを起こす
      ↓
Taskで処理

このような「イベントが発生したことを別のタスクへ知らせる」「条件が成立するまでタスクを待たせる」といった処理に利用できる仕組みがSemaphore(セマフォ)です。

FreeRTOSでは、Binary Semaphore(バイナリセマフォ)やCounting Semaphore(カウンティングセマフォ)などを利用できます。

この記事では、セマフォとは何かという基本から、Take・Give、タスク間同期、割り込みからの通知、KUMITATE-C3での実装例、さらにMutexとの違いまで順番に解説します。

FreeRTOSのSemaphoreの仕組み。Binary SemaphoreとCounting Semaphore、TakeとGive、タスク間同期、割り込みからタスクへの通知を示した図
FreeRTOSのSemaphoreを使ったタスク間同期と、Binary Semaphore・Counting Semaphoreの基本

Semaphore(セマフォ)とは?

Semaphoreは、RTOSでタスク間の同期や、利用できるリソース数の管理などに使われる仕組みです。

初心者のうちは、セマフォを「タスクへ合図を送るための仕組み」と考えると理解しやすいでしょう。

Task A
  ↓
処理完了
  ↓
SemaphoreをGive
  ↓
Semaphore
  ↓
待っていたTask B
  ↓
処理開始

Task Bは、Task Aの処理が完了するまで待機できます。

イベントが発生するまでCPUを使って何度も状態を確認するのではなく、セマフォを待つことでタスクをBlocked状態にできるのが重要なポイントです。

セマフォは「合図」や「許可証」と考えると分かりやすい

セマフォの動作は、「合図」や「利用許可証」に例えると分かりやすくなります。

許可証あり
   ↓
処理できる


許可証なし
   ↓
待つ

FreeRTOSでは、セマフォを取得する操作をTake、セマフォを与える操作をGiveと表現します。

Give
 ↓
セマフォを与える


Take
 ↓
セマフォを取得する

FreeRTOSで使う主なセマフォ

FreeRTOSでは、用途に応じていくつかの同期・排他機構を利用できます。

まずは次の3つを区別しておくとよいでしょう。

種類主な用途
Binary Semaphoreイベント通知・タスク間同期
Counting Semaphore複数イベントのカウント・資源数の管理
Mutex共有リソースの排他制御

MutexもFreeRTOSではSemaphore関連のAPIとして扱われますが、目的や性質が異なります。

この記事ではBinary SemaphoreとCounting Semaphoreを中心に扱い、Mutexは別の記事で詳しく解説します。

Binary Semaphoreとは?

Binary Semaphore(バイナリセマフォ)は、基本的に「0」または「1」の2状態を持つセマフォです。

0
↓
セマフォなし


1
↓
セマフォあり

そのため、

  • ボタンが押された
  • UARTでデータを受信した
  • ADC変換が完了した
  • 別のタスクの処理が完了した
  • タイマーイベントが発生した

といったイベントの発生をタスクへ知らせる用途に使えます。

Binary Semaphoreの動きを見てみよう

たとえばTask Bがセマフォを待っているとします。

Task B
  ↓
SemaphoreをTake
  ↓
Semaphoreなし
  ↓
Blocked

この状態でTask Aが処理を完了し、セマフォをGiveします。

Task A
  ↓
処理完了
  ↓
SemaphoreをGive
  ↓
Task Bが実行可能になる

Task Bはセマフォを取得できるようになり、処理を続行できます。

Binary Semaphoreを作成する

FreeRTOSでは、xSemaphoreCreateBinary()を使ってBinary Semaphoreを作成できます。

SemaphoreHandle_t semaphore;

semaphore = xSemaphoreCreateBinary();

作成に失敗するとNULLが返されるため、実際のプログラムでは確認しておきましょう。

if (semaphore == NULL) {

    Serial.println("Semaphore create failed");

    return;
}

xSemaphoreTake()でセマフォを取得する

セマフォを取得するにはxSemaphoreTake()を使用します。

if (xSemaphoreTake(
        semaphore,
        portMAX_DELAY
    ) == pdTRUE) {

    // セマフォを取得できた
}

第2引数には、セマフォを取得できなかった場合にどれだけ待つかを指定します。

portMAX_DELAYを使うと、FreeRTOSの設定などの条件のもとで、セマフォを取得できるまで非常に長い時間、実質的な無期限待ちとして利用できます。

待っている間はBlocked状態になる

セマフォが利用できない状態で待ち時間を指定すると、タスクはBlocked状態になります。

Task B
  ↓
xSemaphoreTake()
  ↓
セマフォなし
  ↓
Blocked


別のTask
  ↓
xSemaphoreGive()
  ↓
Task BがReadyへ

待機中にCPUを占有し続けないため、他のReady状態のタスクを実行できます。

xSemaphoreGive()でセマフォを与える

セマフォを与えるにはxSemaphoreGive()を使用します。

xSemaphoreGive(semaphore);

これによって、セマフォを待っているタスクが処理を進められるようになります。

タスク同士を同期してみよう

Task Aの処理が完了したらTask Bを実行する例を考えてみます。

SemaphoreHandle_t semaphore;


void taskA(void *pvParameters)
{
    while (1) {

        Serial.println("Task A");

        // Task Bへ通知
        xSemaphoreGive(semaphore);

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}


void taskB(void *pvParameters)
{
    while (1) {

        if (xSemaphoreTake(
                semaphore,
                portMAX_DELAY
            ) == pdTRUE) {

            Serial.println("Task B");
        }
    }
}

Task Bは普段はセマフォを待ってBlocked状態になり、Task AがGiveすると処理を実行します。

ポーリングとの違い

セマフォを使わず、フラグを何度も確認する方法も考えられます。

while (1) {

    if (eventFlag) {

        eventFlag = false;

        // 処理
    }
}

このように繰り返し状態を確認する方法をポーリングと呼びます。

セマフォを待つ設計なら、イベントが発生するまでタスクをBlocked状態にできます。

ポーリング

イベント来た?
 ↓
まだ

イベント来た?
 ↓
まだ

イベント来た?
 ↓
来た!


Semaphore

Take
 ↓
Blocked
 ↓
イベント発生
 ↓
Give
 ↓
Ready

割り込みからタスクへ通知する

Binary Semaphoreの代表的な用途の1つが、割り込みからタスクへの通知です。

GPIO
 ↓
ボタン押下
 ↓
割り込み
 ↓
Semaphore Give
 ↓
待機Task
 ↓
本格的な処理

割り込み処理では時間のかかる処理を行わず、「イベントが発生した」という情報だけをタスクへ渡し、実際の処理をタスク側で行う設計がよく使われます。

ISRからはxSemaphoreGiveFromISR()を使う

割り込みハンドラからセマフォをGiveするときは、通常のxSemaphoreGive()ではなく、ISR用のAPIを使用します。

BaseType_t higherPriorityTaskWoken = pdFALSE;

xSemaphoreGiveFromISR(
    semaphore,
    &higherPriorityTaskWoken
);

セマフォをGiveしたことで、現在実行中のタスクより高い優先度のタスクが実行可能になる場合があります。

そのためISR終了時に必要であればコンテキストスイッチを要求します。

portYIELD_FROM_ISR(
    higherPriorityTaskWoken
);

実際に使用する際は、対象となるFreeRTOSポートやESP-IDFのAPI仕様も確認してください。

KUMITATE-C3で試してみよう

KUMITATE-C3にはLEDとSW1が搭載されているため、Binary Semaphoreの動作を試すことができます。

今回は、SW1が押されたことをGPIO割り込みで検出し、Binary Semaphoreを使ってLED制御タスクへ通知します。

SW1
 ↓
GPIO割り込み
 ↓
Binary Semaphore
 ↓
LED Task
 ↓
LEDの状態を反転
const int LED_PIN = 0;
const int SW_PIN  = 21;

SemaphoreHandle_t buttonSemaphore;


void IRAM_ATTR buttonISR()
{
    BaseType_t higherPriorityTaskWoken = pdFALSE;

    xSemaphoreGiveFromISR(
        buttonSemaphore,
        &higherPriorityTaskWoken
    );

    if (higherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}


void ledTask(void *pvParameters)
{
    bool ledState = false;

    while (1) {

        if (xSemaphoreTake(
                buttonSemaphore,
                portMAX_DELAY
            ) == pdTRUE) {

            ledState = !ledState;

            digitalWrite(
                LED_PIN,
                ledState ? HIGH : LOW
            );
        }
    }
}


void setup()
{
    Serial.begin(115200);

    pinMode(LED_PIN, OUTPUT);
    pinMode(SW_PIN, INPUT_PULLUP);

    buttonSemaphore =
        xSemaphoreCreateBinary();

    if (buttonSemaphore == NULL) {

        Serial.println(
            "Semaphore create failed"
        );

        return;
    }

    xTaskCreate(
        ledTask,
        "LED Task",
        2048,
        NULL,
        1,
        NULL
    );

    attachInterrupt(
        digitalPinToInterrupt(SW_PIN),
        buttonISR,
        FALLING
    );
}


void loop()
{
    vTaskDelay(pdMS_TO_TICKS(1000));
}

SW1が押されると割り込みが発生し、ISRからBinary SemaphoreがGiveされます。

それまでBlocked状態だったLED Taskが実行可能になり、LEDの状態を切り替えます。

なお、物理スイッチにはチャタリングがあります。このサンプルはSemaphoreと割り込みの関係を理解するために単純化しているため、実際にはデバウンス処理も組み合わせる必要があります。

チャタリングについては「チャタリングとは?スイッチ入力が何度も反応する原因と対策を解説」も参照してください。

Counting Semaphoreとは?

Counting Semaphore(カウンティングセマフォ)は、0と1だけではなく、複数のカウント値を持てるセマフォです。

0 → 1 → 2 → 3 → ...

Binary Semaphoreが「イベントがある・ない」を表現するのに向いているのに対して、Counting Semaphoreでは複数回発生したイベントや、利用可能な資源の数を表現できます。

Counting Semaphoreを作成する

FreeRTOSではxSemaphoreCreateCounting()を使用できます。

SemaphoreHandle_t semaphore;

semaphore = xSemaphoreCreateCounting(
    5,
    0
);

主な引数は次のとおりです。

引数意味
5最大カウント値
0初期カウント値

この例では、0~5の範囲でカウントを保持できます。

イベント回数を保持する

Binary Semaphoreは0または1なので、タスクが処理する前に同じイベントが何度も発生した場合、その回数をすべて保持する用途には向きません。

イベント
 ↓
Give

イベント
 ↓
Give

イベント
 ↓
Give

Counting Semaphoreなら、最大値の範囲内で、

0
↓ Give
1
↓ Give
2
↓ Give
3

のように発生回数をカウントできます。

複数の同じリソースを管理する

Counting Semaphoreは、利用できる同種リソースの数を管理する用途にも使えます。

たとえば、同時に利用できる通信バッファが3個あるとします。

利用可能バッファ

[1] [2] [3]

Semaphore = 3

Task Aが1個使用すると、

Take

3 → 2

Task Bも使用すると、

Take

2 → 1

使い終わったらGiveして、利用可能数を戻します。

Give

1 → 2

QueueとSemaphoreは何が違う?

前回学習したQueueとSemaphoreは、どちらもタスク間の連携に使えますが、主な目的が異なります。

QueueSemaphore
主な目的データを渡す同期・通知・資源数管理
データ保持する基本的にはデータ本体を渡さない
待機できるできる
ISRからの利用可能可能
代表的な用途センサーデータの受け渡し処理開始の通知

「温度25.4℃という値を送りたい」ならQueue、「ボタンが押されたことだけ知らせたい」ならBinary Semaphore、と考えると違いが分かりやすくなります。

SemaphoreとMutexは何が違う?

Semaphoreを学習すると、よく一緒に登場するのがMutex(ミューテックス)です。

どちらもTakeとGiveのような操作を行うため似ていますが、目的が異なります。

Binary Semaphore

イベント発生
     ↓
    Give
     ↓
待機Taskを起こす


Mutex

Task A
 ↓
共有リソースをロック
 ↓
使用
 ↓
アンロック
 ↓
Task Bが使用可能

Mutexは、複数のタスクが同じ共有リソースへ同時にアクセスしないようにする排他制御を主な目的としています。

またFreeRTOSのMutexには、優先度逆転の影響を抑えるための優先度継承(Priority Inheritance)という仕組みがあります。

そのため、共有リソースの排他制御を目的とする場合に「Binary Semaphoreでも似たことができそうだから」と安易に代用するのではなく、Mutexを検討することが重要です。

Semaphoreはデータを渡すものではない

初心者が混乱しやすいポイントです。

Semaphoreそのものは、基本的に「温度」「文字列」「構造体」といったデータを受け渡すためのものではありません。

データを渡す
     ↓
Queue


イベントを知らせる
     ↓
Semaphore

もちろん実際のシステムでは、Semaphoreで「データが準備できた」と通知し、別の共有領域からデータを取得する設計もあります。

その場合は共有データへの同時アクセスについても考える必要があります。

volatileを付ければSemaphoreは不要?

イベントフラグにvolatileを付ければ、Semaphoreは不要なのではないかと思うかもしれません。

volatile bool eventFlag = false;

しかしvolatileには、タスクをBlocked状態にしてイベント発生時に起こすようなRTOSの同期機能はありません。

また、volatileを付けるだけで複数タスク間の操作が安全になるわけでもありません。

volatileについては「constとvolatileとは?組み込み開発での意味と使い方を初心者向けに解説」も参照してください。

Semaphoreを使うメリット

  • タスク同士を同期できる
  • イベント発生までタスクをBlocked状態にできる
  • 割り込みからタスクへ通知できる
  • ポーリングを減らせる
  • 複数回のイベントをCounting Semaphoreで数えられる
  • 利用可能な同種リソース数を管理できる
  • FreeRTOSのスケジューラと連携して待ち合わせができる

Semaphoreを使うときの注意点

  • Binary SemaphoreとCounting Semaphoreを用途に合わせて使い分ける
  • 共有リソースの排他制御にはMutexを検討する
  • ISRからはFromISR系APIを使用する
  • 待ち時間を適切に設定する
  • GiveとTakeの対応を考える
  • Counting Semaphoreの最大値を適切に決める
  • セマフォを待つタスクの優先度も考慮する
  • セマフォを使えば共有データが自動的に安全になるわけではない

QueueとSemaphoreを組み合わせることもできる

QueueとSemaphoreは競合する仕組みではなく、システムによっては組み合わせて使うこともできます。

センサー
   ↓
割り込み
   ↓
Semaphore
   ↓
Sensor Task
   ↓
データ取得
   ↓
Queue
   ↓
Display Task

この例では、Semaphoreが「センサーイベントが発生した」という通知を担当し、Queueが取得したデータの受け渡しを担当します。

Queueについては「Queue(キュー)とは?FreeRTOSのタスク間通信を初心者向けに解説」も参照してください。

RTOSの記事と合わせて理解しよう

Semaphoreだけを見ると少し分かりにくいですが、RTOSのタスク状態と合わせると役割が見えてきます。

Task
 ↓
SemaphoreをTake
 ↓
利用できない
 ↓
Blocked
 ↓
イベント発生
 ↓
SemaphoreをGive
 ↓
Ready
 ↓
スケジューラに選ばれる
 ↓
Running

つまりSemaphoreは、単なるフラグではなく、RTOSのタスク状態やスケジューリングと連携してタスクを待機・再開させられることが大きな特徴です。

RTOSそのものについては「RTOSとは?タスク・スケジューラ・優先度の仕組みを初心者向けに解説」も参照してください。

まとめ

Semaphoreは、FreeRTOSでタスク間の同期やイベント通知、リソース数の管理などに利用できる重要な仕組みです。

  • Semaphoreはタスクの同期などに利用できる
  • 取得する操作をTake、与える操作をGiveと呼ぶ
  • Binary Semaphoreは0または1の状態を持つ
  • Binary Semaphoreはイベント通知に使いやすい
  • Counting Semaphoreは複数のカウントを保持できる
  • Counting Semaphoreはイベント回数やリソース数の管理に利用できる
  • セマフォを取得できないタスクをBlocked状態にできる
  • ISRからはxSemaphoreGiveFromISR()などのISR用APIを使用する
  • Queueは主にデータの受け渡し、Semaphoreは主に同期や通知に使う
  • 共有リソースの排他制御ではMutexを検討する
  • volatileだけではSemaphoreの代わりにはならない

RTOSでは、複数のタスクを作るだけでは十分ではありません。

どのタスクをいつ動かすのか、イベントをどう通知するのか、複数のタスクをどう協調させるのかを設計する必要があります。

Semaphoreは、そのための基本的な仕組みの1つです。まずは「割り込みでBinary SemaphoreをGiveし、待機しているタスクがTakeする」という構成を実際に動かしてみると、RTOSの同期処理を理解しやすくなるでしょう。

KUMITATE

読むだけでなく、
実際に動かして学びませんか?

クミタテは、組み込み開発や電子工作を 実践しながら学べる学習プラットフォームです。 ESP32を使ったプログラミングから、電子回路、センサー、 通信、基板設計まで、自分のペースで学習できます。

  • 無料で学べる実践的な教材を掲載
  • すぐに試せるサンプルプログラム付き
  • Googleアカウントですぐに登録可能
クミタテに無料登録する
組み込み基礎
スポンサーリンク