pthread 学习笔记 (4)

自旋锁

除了前面提到过的互斥锁和读写锁外,还有另外一种锁:自旋锁(spinlock)。自旋锁的原理比较简单,就是当某个资源不可用时不断查询,直到资源可用:

void tese_and_set(bool* condition)
{
    bool tmp = *condition;
    *condition = true;
    return tmp;
}

void func()
{
    while (test_and_set(&condition));

    /* visit */

    condition = false;
}

其中的操作 test_and_set() 可以看成一个原子操作,通常由硬件来实现。如果 condition 为 false,while 循环会不断地查询 condition 的状态,直到其可用为止。因为在条件不满足时程序也一直在忙,这样会消耗 cpu 资源,所以自旋锁适合用在执行时间不长的临界区中,例如修改一个数值等。

由于自旋锁比较简单,pthread 提供的接口也比较简单。下面是使用自旋锁实现的生产者-消费者问题(其实就是将 mutex 替换为 spinlock):

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

struct pool {
    pthread_spinlock_t lock;
#define MAX_NUM 5
    int num;
};

static inline int is_full(struct pool* pool)
{
    return (pool->num == MAX_NUM);
}

static inline int is_empty(struct pool* pool)
{
    return (pool->num == 0);
}

static inline void put(struct pool* pool)
{
    ++pool->num;
}

static inline void get(struct pool* pool)
{
    --pool->num;
}

void init(struct pool* pool)
{
    pool->num = 0;
    pthread_spin_init(&pool->lock, 0);
}

static void* consumer(void* arg)
{
    struct pool* pool = (struct pool*)arg;

    while (1) {
        pthread_spin_lock(&pool->lock);
        if (is_empty(pool))
            printf("get fail.\n");
        else {
            get(pool);
            printf("get successfully. available: %d\n", pool->num);
        }
        pthread_spin_unlock(&pool->lock);
    }

    return arg;
}

static void* producer(void* arg)
{
    struct pool* pool = (struct pool*)arg;

    while (1) {
        pthread_spin_lock(&pool->lock);
        if (is_full(pool))
            printf("put fail.\n");
        else {
            put(pool);
            printf("put successfully. available: %d\n", pool->num);
        }
        pthread_spin_unlock(&pool->lock);
    }

    return arg;
}

int main(void)
{
    struct pool pool;
    pthread_t producer_id, consumer_id;

    init(&pool);

    pthread_create(&producer_id, NULL, producer, &pool);
    pthread_create(&consumer_id, NULL, consumer, &pool);

    pthread_join(producer_id, NULL);
    pthread_join(consumer_id, NULL);

    return 0;
}

初始化函数 init() 中使用了

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

第二个参数 pshared 的取值可以为 PTHREAD_PROCESS_SHARED 或 PTHREAD_PROCESS_PRIVATE,即是否允许在多个线程中共享该自旋锁(详细解释请 man pthread_spin_init)。访问临界区前使用函数

int pthread_spin_lock(pthread_spinlock_t *lock);

对自旋锁进行加锁。与互斥锁和读写锁相似的是,这个函数是阻塞版本,也就是一直到加锁成功才返回。另外的立即返回的函数为

int pthread_spin_trylock(pthread_spinlock_t *lock);

访问完临界区后使用函数

int pthread_spin_unlock(pthread_spinlock_t *lock);

释放占有的锁。最后使用函数

int pthread_spin_destroy(pthread_spinlock_t *lock);

释放不需要的资源。

终止线程

很多时候线程里都是一个 while(1) 循环,想退出的话要么加个开关变量,每次执行前检查一下变量的值,但是这样有点罗嗦;要么用暴力点的方法:直接杀掉。

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

static void* thread_func(void* nil)
{
    while (1) {
        printf("sleep 1 s...\n");
        sleep(1);
    }

    printf("will never reach here.\n");
    return NULL;
}

int main(void)
{
    pthread_t pid;

    if (pthread_create(&pid, NULL, thread_func, NULL) != 0)
        fprintf(stderr, "pthread_create error\n");

    sleep(3);

    pthread_cancel(pid);
    pthread_join(pid, NULL);
    printf("thread is killed.\n");

    return 0;
}

第 25 行使用函数

int pthread_cancel(pthread_t thread);

向线程发了一个 cancel 的信号,但是线程并不会马上结束。线程能否被杀死取决于两个属性:线程的 cancel 状态(state)和类型(type)。使用函数

int pthread_setcancelstate(int state, int *oldstate);

可以设置线程是否可被 pthread_cancel() 杀死,state 的值只能是 PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE。如果 oldstate 不为空,线程原来的状态会保存在 oldstate 中,方便以后恢复。线程默认的状态是 enable,即可被 pthread_cancel() 杀死。线程的类型(type)可使用函数

int pthread_setcanceltype(int type, int *oldtype);

来设置,type 的值只能是 PTHREAD_CANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS。如果值为 PTHREAD_CANCEL_DEFERRED,线程并不会立即被杀死,而是当线程运行到某个可处理 cancel 信号的函数时才会被杀死(man 7 pthread搜关键字“cancellation point”)。如果值为 PTHREAD_CANCEL_ASYNCHRONOUS,理论上线程会被立即杀掉,但是由于调度或中断等其它原因可能会有延迟。

如果线程中没有用到 cancellation point 中提到的函数,可以使用

void pthread_testcancel(void);

手动创建一个 cancellation point,这样当线程运行到这个函数的时候,如果有 cancel 信号就会被杀死。

信号传递

pthread 提供了一个给线程传递信号的函数:

int pthread_kill(pthread_t thread, int sig);

千万不要被函数名字骗了,以为随便发个信号就能杀掉进程(我刚开始就是这么以为的……)。函数给指定的线程发一个信号,如果进程没有实现相应的信号处理函数,该信号默认的行为将会作用于线程所在的整个进程,例如向某个线程发送一个 SIGINT 信号,如果没有实现对应的信号处理函数,则整个进程都会退出。

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

static void sigint_action(int sig, struct siginfo* s, void* nil)
{
    pthread_exit(NULL);
}

static inline void install_sighandler(void)
{
    struct sigaction handler;

    sigemptyset(&handler.sa_mask);
    sigaddset(&handler.sa_mask, SIGINT);
    handler.sa_flags = SA_SIGINFO; /* use sa_sigaction instead of sa_handler */
    handler.sa_sigaction = sigint_action;

    if (sigaction(SIGINT, &handler, NULL) != 0)
        perror("sigaction");
}

static void* thread_func(void* tid)
{
    while (1) {
        printf("thread %d is running ...\n", *(int*)tid);
        sleep(1);
    }

    return tid;
}

#define THREAD_NR 3

int main(void)
{
    int i, tid[THREAD_NR];
    pthread_t pid[THREAD_NR];

    for (i = 0; i < THREAD_NR; ++i) {
        tid[i] = i;
        if (pthread_create(&pid[i], NULL, thread_func, &tid[i]) != 0) {
            fprintf(stderr, "cannot create thread %d.\n", i);
            return -1;
        }
    }

    install_sighandler();

    for (i = THREAD_NR - 1; i >= 0; --i) {
        sleep(3);
        if (pthread_kill(pid[i], SIGINT) != 0)
            perror("pthread_kill");
        pthread_join(pid[i], NULL);
        printf("-----> thread %d is killed.\n", i);
    }

    return 0;
}

main() 中创建了 3 个线程,并使用 sigaction() 为信号 SIGINT 指定了一个处理函数 sigint_action()。sigint_action() 调用了 pthread_exit() 终止收到 SIGINT 信号的线程。如果没有这个函数(把第 20-21 行注释掉),SIGINT 默认作用于整个进程,即整个进程退出而不仅仅是收到这个信号的线程。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注