明日にはでっかい太陽が昇るかもしれません。

「覚悟」とは!! 暗闇の荒野に!!進むべき道を切り開く事だッ!

Java の synchronized ブロックのようなことを C で実現する

職業柄、組込みをやっているので、 C で排他制御を行う機会は多い。

でも、毎回 ***_lock, ***_unlock で囲むのも面倒だし、解放漏れなどによるデッドロックとかわかりにくい割に初歩的なバグに悩まされることも多く、 Java の synchronized ブロックが羨ましいなーと思っていた。 (レビューで対応を見るのも面倒ってのが最近は多い)

そこで、以前は以下のようなマクロを作って、擬似的な synchronized ブロックを構築していたけど、不満な点もいくつかあった。

/* 擬似 synchronized ブロック */
#define SYNCHRONIZED1(obj, ...)                                            \
    do {                                                                   \
        pthread_mutex_lock(obj);                                           \
        pthread_cleanup_push((void (*)(void *))pthread_mutex_unlock, obj); \
        __VA_ARGS__;                                                       \
        pthread_mutex_unlock(obj);                                         \
        pthread_cleanup_pop(0);                                            \
    } while(0)

/* 使用方法 */
void *old_type(void *arg)
{
    for (int i; i < 5000; ++i) {
        SYNCHRONIZED1(&mutex, {
            counter++;
        });
    }
    return NULL;
}

動作上は問題ないけど、以下のような点が不満だった。

  1. 全体がワンライナーになるため、 __LINE__ マクロが期待する通りにならない。 (デバッグ時に困る。。)
  2. () の中に {} が入るのは C の規格上無いため、エディタのハイライトが期待通りにならない。 (vim は構文エラー扱いにする)
  3. 見た目が C っぽくない。

最近、ユニットテストフレームワーク Catch2 のコードを見ていて「できるかも!?」と思って試してみたのが以下のコード。

#define CAT_I(a, b) a ## b
#define CAT(a, b) CAT_I(a, b)
 
/* 擬似 synchronized ブロック */
#define synchronized2(obj)                                \
    void CAT(__caller__, __LINE__)(void (*fn)(void)) {    \
        pthread_mutex_lock(obj);                          \
        fn();                                             \
        pthread_mutex_unlock(obj);                        \
    }                                                     \
    auto void CAT(__callee__, __LINE__)(void);            \
    CAT(__caller__, __LINE__)(CAT(__callee__, __LINE__)); \
    void CAT(__callee__, __LINE__)(void)

/* 使用方法 */
void *new_type(void *arg)
{
    for (int i; i < 5000; ++i) {
        synchronized2(&mutex) {
            counter++;
        }                                                                                                                                                              
    }
    return NULL;
}

旧式の問題点はすべて解決され、かつ、ブロックは関数として実行されるため、 pthread_cleanup_push を使用しなくても排他の解放漏れが発生しなくなった。

nested function を使用しているため、 gcc 専用になるが、 scan-buildoclint を行う際は空にすればロジックに影響を与えずに排除することもできる。

というわけで動作確認。

上で使用方法として示した関数を 4 スレッド起動して排他が効いているか確認する。

# synchronized ブロック未使用
$ make test
gcc -MMD -MP -Wall -Werror  -c -o sync.o sync.c
gcc  -o synchronized sync.o -lpthread
./synchronized
counter: 14977

# synchronized ブロック使用 (synchronized2)
$ make test
gcc -MMD -MP -Wall -Werror  -c -o sync.o sync.c
gcc  -o synchronized sync.o -lpthread
./synchronized
counter: 20000

未使用時は排他を行っていないため期待通りインクリメントできておらず、使用時は正しくインクリメントできることを確認した。

これまで旧式で書いていたコードをすべて新しくしよう!

以下、テストコードの全文。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define SYNCHRONIZED1(obj, ...)                                            \
    do {                                                                   \
        pthread_mutex_lock(obj);                                           \
        pthread_cleanup_push((void (*)(void *))pthread_mutex_unlock, obj); \
        __VA_ARGS__;                                                       \
        pthread_mutex_unlock(obj);                                         \
        pthread_cleanup_pop(0);                                            \
    } while(0)

#define CAT_I(a, b) a ## b
#define CAT(a, b) CAT_I(a, b)

#define synchronized2(obj)                                \
    void CAT(__caller__, __LINE__)(void (*fn)(void)) {    \
        pthread_mutex_lock(obj);                          \
        fn();                                             \
        pthread_mutex_unlock(obj);                        \
    }                                                     \
    auto void CAT(__callee__, __LINE__)(void);            \
    CAT(__caller__, __LINE__)(CAT(__callee__, __LINE__)); \
    void CAT(__callee__, __LINE__)(void)

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static unsigned long counter = 0;

void *old_type(void *arg)
{
    /* 開始タイミングを合わせる */
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);                                                                                                                                  [0/5190]

    for (int i; i < 5000; ++i) {
        SYNCHRONIZED1(&mutex, {
            counter++;
        });
    }
    return NULL;
}

void *new_type(void *arg)
{
    /* 開始タイミングを合わせる */
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);

    for (int i; i < 5000; ++i) {
        synchronized2(&mutex) {
            counter++;
        }
    }
    return NULL;
}

int main(int argc, char **argv)
{
    pthread_t thrd_ids[4];

    for (int i = 0; i < 2; ++i) {
        pthread_create(&thrd_ids[i], NULL, old_type, NULL);
    }
    for (int i = 2; i < 4; ++i) {
        pthread_create(&thrd_ids[i], NULL, new_type, NULL);
    }
    sleep(1);
    pthread_cond_broadcast(&cond);

    for (int i = 0; i < 4; ++i) {
        pthread_join(thrd_ids[i], NULL);
    }
    printf("counter: %ld\n", counter);

    return 0;
}