linux线程间同步(一)互斥锁与条件变量

linux线程间同步(1)互斥锁与条件变量

线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量以及读写锁。

互斥锁(mutex)

互斥锁,是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。可以保证以下三点:

  • 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量。
  • 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
  • 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

从以上三点,我们看出可以用互斥量来保证对变量(关键的代码段)的排他性访问。

互斥锁常用函数

#include <pthread.h>

//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
//互斥锁静态赋值
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//阻塞加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//非阻塞加锁,成功则返回0,否则返回EBUSY
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

如果要正确的使用pthread_mutex_lock与pthread_mutex_unlock,请参考pthread_cleanup_push和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!

互斥锁属性

#include <pthread.h>

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

//锁的范围:PTHREAD_PROCESS_PRIVATE(进程内),PTHREAD_PROCESS_SHARED(进程间)
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
      restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
      int pshared);

//锁的类型
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,
              int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *
              restrict attr, int *restrict protocol);
int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr,
       int protocol);

int pthread_mutexattr_getprioceiling(const pthread_mutexattr_t *
      restrict attr, int *restrict prioceiling);
int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *attr,
      int prioceiling);   

说明:pthread库不是Linux系统默认的库,连接时需要使用静态库libpthread.a,所以在使用pthread_create()创建线程,以及调用pthread_atfork()函数建立fork处理程序时,需要链接该库。在编译中要加 -lpthread参数。

条件变量(cond)

利用线程间共享的全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。

int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞

使用说明:

  • 动态初始化调用pthread_cond_init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER静态初始化;属性置为NULL

  • pthread_cond_wait与pthread_cond_timedwait,在使用前应用程序必须执行了加锁互斥量,两函数在调用时自动解锁互斥量,等待条件互斥量触发。这时线程挂起,不占用CPU,前者直到条件变量被触发,后者等待条件变量被触发或者超时(返回ETIMEOUT)。函数返回前,自动重新对互斥量自动加锁。

  • 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争— —个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。

  • pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程,否则返回EBUSY

  • 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。

代码实例

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

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

struct node
{
    int n_number;
    struct node *n_next;
} *head = NULL;

static void cleanup_handler(void *arg)
{
    printf("Cleanup handler of second thread./n");
    free(arg);
    (void)pthread_mutex_unlock(&mtx);
}

static void *thread_func(void *arg)
{
    struct node *p = NULL;
    pthread_cleanup_push(cleanup_handler, p);
    while (1)
    {
        //这个mutex主要是用来保证pthread_cond_wait的并发性
        pthread_mutex_lock(&mtx);
        while (head == NULL)
        {
            //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何
            //这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线
            //程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。
            //这个时候,应该让线程继续进入pthread_cond_wait
            // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,
            //然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立
            //而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
            //用这个流程是比较清楚的
            pthread_cond_wait(&cond, &mtx);
            p = head;
            head = head->n_next;
            printf("Got %d from front of queue/n", p->n_number);
            free(p);
        }
        pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁
    }
    pthread_cleanup_pop(0);

    return 0;
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    int i;
    struct node *p;
    //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而
    //不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
    pthread_create(&tid, NULL, thread_func, NULL);
    sleep(1);
    for (i = 0; i < 10; i++)
    {
        p = (struct node*)malloc(sizeof(struct node));
        p->n_number = i;
        pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁,
        p->n_next = head;
        head = p;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mtx); //解锁
        sleep(1);
    }
    printf("thread 1 wanna end the line.So cancel thread 2./n");
    //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出
    //线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。
    pthread_cancel(tid);
    pthread_join(tid, NULL);

    return 0;
}

线程取消点

一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。

相关概念

线程取消的方法是向目标线程发送Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。

线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出。

根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用 pthread_testcancel(),从而达到POSIX标准所要求的目标.

相关API

  1. 取消线程运行
    int pthread_cancel(pthread_t thread);发送终止信号给thread线程,成功则返回0,否则返回非0. 成功发送并不意味着thread会终止.

  2. 设置取消点
    如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用。
    pthread_testcancel:

    • 设置取消点
    • 检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。
  3. 设置线程取消状态与类型

    int pthread_setcancelstate(int state, int *oldstate);
    int pthread_setcanceltype(int type, int *oldtype);

    pthread_setcancelstate设置线程对Cancel的反应,PTHREAD_CANCEL_ENABLE(default)与PTHREAD_CANCEL_DISABLE分别表示接受信号后设为CANCEL转态或者忽略CANCEL信号继续运行

    pthread_setcanceltype设置本线程取消动作的执行时机,PTHREAD_CANCEL_DEFFERED(default)和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出).

参考:

  • 多线程编程之线程取消