同步

什么是同步

操作系统中存在多个进程并发访问和操作同一个数据,并且执行结果和进程执行的特定顺序有关,称为:竞争条件。为了防止竞争条件发生,我们需要确保一段时间内只有一个进程能操作这个数据。为了实现这个保证,进程之间必须要同步。在保证进程或线程间互斥的情况下,实现相互依赖方的有序执行,即为进线程的同步。

同步的场景

线程间同步

原因

由于进程中的线程共享资源,所以在多线程的情况下,对共享资源的操作必须考虑互斥的问题,而如果多线程间存在一定执行顺序,那个就需要通过一定的方式实现线程间的同步。

线程间同步的方式

  1. 互斥锁(mutex)
操作 函数 说明
静态初始化锁 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 底层宏
动态初始化锁 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr); <1>
阻塞加锁 int pthread_mutex_lock(pthread_mutex *mutex); 如果获取锁失败,将一直等待,知道返回结果
非阻塞加锁 int pthread_mutex_trylock( pthread_mutex_t *mutex); 该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
解锁 int pthread_mutex_unlock(pthread_mutex *mutex); 要求锁是lock状态,并且由加锁线程解锁
销毁锁 int pthread_mutex_destroy(pthread_mutex *mutex); 此时锁必需unlock状态,否则返回EBUSY

<1> 其中参数 mutexattr 用于指定锁的属性,如果为NULL则使用缺省属性。 互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:

  1. PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  2. PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  3. PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  4. PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

实例

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int gn;

void* thread(void *arg) {
    printf("thread's ID is  %d\n",pthread_self());
    pthread_cleanup_push(pthread_mutex_unlock, &mutex);  // 防止线程被异常终止,而mutex无法释放,保证线程正常或者异常退出都能释放互斥锁。
    
    pthread_mutex_lock(&mutex);
    gn = 12;
    printf("Now gn = %d\n",gn);
    // pthread_mutex_unlock(&mutex);

    pthread_exit(0); //  退出才能出发clean_up
    pthread_cleanup_pop(0);
}
 
int main() {
    pthread_t id;
    printf("main thread's ID is %d\n",pthread_self());
    gn = 3;
    printf("In main func, gn = %d\n",gn);
    if (!pthread_create(&id, NULL, thread, NULL)) {
        printf("Create thread success!\n");
    } else {
        printf("Create thread failed!\n");
    }
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}
  1. 读写锁

资源访问分为两种情况:读操作和写操作。

读写锁比mutex有更高的适用性,可以多个线程同时占用读模式的读写锁,但是只能一个线程占用写模式的读写锁。

- 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞;
- 当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行枷锁的线程将阻塞;
- 当读写锁在读模式锁状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求长期阻塞;这种锁适用对数据结构进行读的次数比写的次数多的情况下,因为可以进行读锁共享。
  • 比喻

    • 新闻发布会
    • 领导发言与聊天
  • 概念

    • 共享独占
    • 读取锁(共享)
    • 写入锁(独占)
操作 函数 说明
静态分配读写锁 pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER 简单
动态分配读写锁 pthread_rwlock_init(&rwlock, NULL);pthread_rwlock_destroy(&rwlock); 可以设置更多的选项
读取锁 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
读取锁 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
写入锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
写入锁 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)

实例

#include <stdio.h>
#include <pthread.h>
 
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int count = 10000;
int put_cur = 0;

// 错误回滚
void rollback(void* arg){
    count -= put_cur;
    printf("rollback %d  to %d\n",put_cur,count);
    pthread_rwlock_unlock(&rwlock);
}

// 一个查看
void* search(void* arg){
    for(;;){
        pthread_rwlock_rdlock(&rwlock);
        printf("leave %d\n",count); 
        usleep(500000);
        pthread_rwlock_unlock(&rwlock);
    }
}

// 一个放票
void* put(void* arg){
    pthread_cleanup_push(rollback,NULL);
    for(;;){
        pthread_rwlock_wrlock(&rwlock);
        put_cur = rand()%1000;
        count += put_cur;
        printf("put %d ok,leave %d\n",put_cur,count);
        usleep(500000);
        pthread_rwlock_unlock(&rwlock);
    }
    pthread_cleanup_pop(0);
}

// 一个随机破坏。
void* hacker(void* arg){
    sleep(2);
    pthread_t* ptids = arg;
    pthread_cancel(ptids[0]);
    printf("\033[41;34mcancel %lu\033[0m\n",ptids[0]);
}

// 一个取票
void* get(void* arg){
    for(;;){
        pthread_rwlock_wrlock(&rwlock);
        int cur = rand()%1000;
        if(count>cur){
            count -= cur;
            printf("crash %d leave %d\n",cur,count);
        }else{
            printf("leave not enought %d\n",count);
        }
        usleep(500000);
        pthread_rwlock_unlock(&rwlock);
    }
}
 
int main(){
    pthread_t tids[4];
    typedef void*(*func_t)(void*);
    func_t funcs[4] = {put,get,search,hacker};
    int i=0;
    pthread_setconcurrency(4);
    for(i=0;i<4;i++){
        pthread_create(&tids[i],NULL,funcs[i],tids);
    }
    for(i=0;i<4;i++){
        pthread_join(tids[i],NULL);
    }
}
  1. 条件变量

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

操作 函数 说明
静态分配条件变量 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 简单
动态分配静态变量 pthread_cond_init(&cond, NULL);pthread_cond_destroy(&cond); 尽管POSIX标准中为条件变量定义了属性,但在Linux中没有实现,因此cond_attr值通常为NULL,且被忽略。
条件等待 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 结合mutex <1>
计时等待 int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) <1>
单个激活 int pthread_cond_signal(pthread_cond_t *cond) 激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)
全部激活 int pthread_cond_broadcast(pthread_cond_t *cond) 激活所有等待线程
销毁条件变量 int pthread_cond_destroy(pthread_cond_t *cond); 只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY

<1> 无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait()pthread_cond_timedwait() 请求)竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

说明

1. pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
2. 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。
3. 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁

实例

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

pthread_mutex_t mutex;
pthread_cond_t cond;
void hander(void *arg) {
    free(arg);
    (void)pthread_mutex_unlock(&mutex);
}

void *thread1(void *arg) {
    pthread_cleanup_push(hander, &mutex);
    while(1) {
        printf("thread1 is running\n");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        printf("thread1 applied the condition\n");
        pthread_mutex_unlock(&mutex);
        sleep(4);
    }
    pthread_cleanup_pop(0);
}

void *thread2(void *arg) {
    while(1) {
        printf("thread2 is running\n");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        printf("thread2 applied the condition\n");
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

int main() {
    pthread_t thid1,thid2;
    printf("condition variable study!\n");
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_create(&thid1,NULL,thread1,NULL);
    pthread_create(&thid2,NULL,thread2,NULL);
    sleep(1);
    do {
        pthread_cond_signal(&cond);
    }while(1);
    sleep(20);
    pthread_exit(0);
    return 0;
}

/////////////////////////实例2//////////////////////////

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.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(void) {
    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);
    printf("All done -- exiting\n");
    return 0;
}

疑问:

  • 为什么不在pthread_cond_t 内部已经封装好mutext,而是在外面创建mutex ?
  1. 信号量

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。

操作 函数 说明
创建 int sem_init(sem_t *sem, int pshared, unsigned int value) sem - 指定要初始化的信号量pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量value - 信号量 sem 的初始值。
触发 int sem_post(sem_t *sem) 给参数sem指定的信号量值加1。0 成功, -1失败
阻塞等待 int sem_wait(sem_t *sem) 给参数sem指定的信号量值减1。 如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。0 成功, -1失败
非阻塞等待 int sem_trywait(sem_t * sem) 试图给参数sem指定的信号量值减1。如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。0 成功, -1失败
销毁 int sem_destroy(sem_t *sem) 销毁指定的信号量。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!\n", __func__);return;}

typedef struct _PrivInfo
{
    sem_t s1;
    sem_t s2;
    time_t end_time;
}PrivInfo;

static void info_init (PrivInfo* prifo);
static void info_destroy (PrivInfo* prifo);
static void* pthread_func_1 (PrivInfo* prifo);
static void* pthread_func_2 (PrivInfo* prifo);

int main (int argc, char** argv) {
    pthread_t pt_1 = 0;
    pthread_t pt_2 = 0;
    int ret = 0;
    PrivInfo* prifo = NULL;
    prifo = (PrivInfo* )malloc (sizeof (PrivInfo));
    if (prifo == NULL) {
        printf ("[%s]: Failed to malloc priv.\n");
        return -1;
    }
    info_init (prifo);
    ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, prifo);
    if (ret != 0) {
        perror ("pthread_1_create:");
    }
    ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, prifo);
    if (ret != 0) {
        perror ("pthread_2_create:");
    }
    pthread_join (pt_1, NULL);
    pthread_join (pt_2, NULL);
    info_destroy (prifo);
    return 0;
}

static void info_init (PrivInfo* prifo) {
    return_if_fail (prifo != NULL);
    prifo->end_time = time(NULL) + 10;
    sem_init (&prifo->s1, 0, 1);
    sem_init (&prifo->s2, 0, 0);
    return;
}

static void info_destroy (PrivInfo* prifo) {
    return_if_fail (prifo != NULL);
    sem_destroy (&prifo->s1);
    sem_destroy (&prifo->s2);
    free (prifo);
    prifo = NULL;
    return;
}

static void* pthread_func_1 (PrivInfo* prifo) {
    return_if_fail (prifo != NULL);
    while (time(NULL) < prifo->end_time) {
        sem_wait (&prifo->s2);
        printf ("pthread1: pthread1 get the lock.\n");
        sem_post (&prifo->s1);
        printf ("pthread1: pthread1 unlock\n");
        sleep (1);
    }
    return;
}

static void* pthread_func_2 (PrivInfo* prifo) {
    return_if_fail (prifo != NULL);
    while (time (NULL) < prifo->end_time) {
        sem_wait (&prifo->s1);
        printf ("pthread2: pthread2 get the unlock.\n");
        sem_post (&prifo->s2);
        printf ("pthread2: pthread2 unlock.\n");
        sleep (1);
    }
    return;
}
  1. 条件变量与互斥锁、信号量的区别

    1.互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。 2.互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。 3.由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。 4.互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。

同步方式的细则

概述

最常见的进程/线程的同步方法有互斥锁(或称互斥量Mutex),读写锁(rdlock),条件变量(cond),信号量(Semophore)等。在Windows系统中,临界区(Critical Section)和事件对象(Event)也是常用的同步方法。

- 互斥锁保护了一个临界区,在这个临界区中,一次最多只能进入一个线程。如果有多个进程在同一个临界区内活动,就有可能产生竞态条件(race condition)导致错误。    
- 读写锁从广义的逻辑上讲,也可以认为是一种共享版的互斥锁。如果对一个临界区大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。
- 条件变量允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生时,线程会一直处于休眠状态。当被其它线程通知条件已经发生时,线程才会被唤醒从而继续向下执行。条件变量是比较底层的同步原语,直接使用的情况不多,往往用于实现高层之间的线程同步。使用条件变量的一个经典的例子就是线程池(Thread Pool)了。

在学习操作系统的进程同步原理时,讲的最多的就是信号量了。通过精心设计信号量的PV操作,可以实现很复杂的进程同步情况(例如经典的哲学家就餐问题和理发店问题)。而现实的程序设计中,却极少有人使用信号量。能用信号量解决的问题似乎总能用其它更清晰更简洁的设计手段去代替信号量。 

可递归锁与非递归锁

  • 概念

Mutex可以分为递归锁(recursive mutex)和非递归锁(non-recursive mutex)。可递归锁也可称为可重入锁(reentrant mutex),非递归锁又叫不可重入锁(non-reentrant mutex)。

二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。

Windows下的Mutex和Critical Section是可递归的。Linux下的pthread_mutex_t锁默认是非递归的。可以显示的设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁。

在大部分介绍如何使用互斥量的文章和书中,这两个概念常常被忽略或者轻描淡写,造成很多人压根就不知道这个概念。但是如果将这两种锁误用,很可能会造成程序的死锁。请看下面的伪代码程序。

MutexLock mutex;  
  
void foo()  
{  
    mutex.lock();  
    // do something  
    mutex.unlock();  
}  
  
void bar()  
{  
    mutex.lock();  
    // do something  
    foo();  
    mutex.unlock();   
}

foo函数和bar函数都获取了同一个锁,而bar函数又会调用foo函数。如果MutexLock锁是个非递归锁,则这个程序会立即死锁。因此在为一段程序加锁时要格外小心,否则很容易因为这种调用关系而造成死锁。

不要存在侥幸心理,觉得这种情况是很少出现的。当代码复杂到一定程度,被多个人维护,调用关系错综复杂时,程序中很容易犯这样的错误。庆幸的是,这种原因造成的死锁很容易被排除。

但是这并不意味着应该用递归锁去代替非递归锁。递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。因此在能使用非递归锁的情况下,应该尽量使用非递归锁,因为死锁相对来说,更容易通过调试发现。程序设计如果有问题,应该暴露的越早越好。

  • 如何避免

为了避免上述情况造成的死锁,AUPE v2一书在第12章提出了一种设计方法。即如果一个函数既有可能在已加锁的情况下使用,也有可能在未加锁的情况下使用,往往将这个函数拆成两个版本—加锁版本和不加锁版本(添加nolock后缀)。

例如将foo()函数拆成两个函数。

// 不加锁版本  
void foo_nolock()  
{  
    // do something  
}  
// 加锁版本  
void fun()  
{  
    mutex.lock();  
    foo_nolock();  
    mutex.unlock();  
}  

读写锁的递归性

读写锁(例如Linux中的pthread_rwlock_t)提供了一个比互斥锁更高级别的并发访问。读写锁的实现往往是比互斥锁要复杂的,因此开销通常也大于互斥锁。在我的Linux机器上实验发现,单纯的写锁的时间开销差不多是互斥锁十倍左右。

在系统不支持读写锁时,有时需要自己来实现,通常是用条件变量加读写计数器实现的。有时可以根据实际情况,实现读者优先或者写者优先的读写锁。

读写锁的优势往往展现在读操作很频繁,而写操作较少的情况下。如果写操作的次数多于读操作,并且写操作的时间都很短,则程序很大部分的开销都花在了读写锁上,这时反而用互斥锁效率会更高些。

#include <pthread.h>  
int main()  
{  
    pthread_rwlock_t rwl;  
    pthread_rwlock_rdlock(&rwl);  
    pthread_rwlock_wrlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    return -1;  
}  
  
/*程序2*/  
#include <pthread.h>  
int main()  
{  
    pthread_rwlock_t rwl;  
    pthread_rwlock_wrlock(&rwl);  
    pthread_rwlock_rdlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    return -1;  
}  

/*程序3*/  
#include <pthread.h>  
int main()  
{  
    pthread_rwlock_t rwl;  
    pthread_rwlock_rdlock(&rwl);  
    pthread_rwlock_rdlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    return -1;  
}  
/*程序4*/  
#include <pthread.h>  
int main()  
{  
    pthread_rwlock_t rwl;  
    pthread_rwlock_wrlock(&rwl);  
    pthread_rwlock_wrlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    pthread_rwlock_unlock(&rwl);  
    return -1;  
}  
  • 你会很疑惑的发现,程序1先加读锁,后加写锁,按理来说应该阻塞,但程序却能顺利执行。而程序2却发生了阻塞。
  • 在POSIX标准中,如果一个线程先获得写锁,又获得读锁,则结果是无法预测的。这就是为什么程序1的运行出人所料。需要注意的是,读锁是递归锁(即可重入),写锁是非递归锁(即不可重入)。因此程序3不会死锁,而程序4会一直阻塞。

读写锁是否可以递归会可能随着平台的不同而不同,因此为了避免混淆,建议在不清楚的情况下尽量避免在同一个线程下混用读锁和写锁

在系统不支持递归锁,而又必须要使用时,就需要自己构造一个递归锁。通常,递归锁是在非递归互斥锁加引用计数器来实现的。简单的说,在加锁前,先判断上一个加锁的线程和当前加锁的线程是否为同一个。如果是同一个线程,则仅仅引用计数器加1。如果不是的话,则引用计数器设为1,则记录当前线程号,并加锁。需要注意的是,如果自己想写一个递归锁作为公用库使用,就需要考虑更多的异常情况和错误处理,让代码更健壮一些。

死锁原因及解决、避免办法

  • 死锁的条件

    • 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
    • 请求与保持条件(Hold and wait):进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
    • 不可抢占条件(No pre-emption):有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
    • 循环等待条件(Circular wait):若干个进程形成环形链,每个都占用对方申请的下一个资源。
  • 处理死锁的策略

    1. 忽略该问题。例如鸵鸟算法。
    2. 检测死锁并且恢复。
    3. 仔细地对资源进行动态分配,以避免死锁。
    4. 通过破除死锁四个必要条件之一,来防止死锁产生。
  • 鸵鸟算法:

    • 该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。
  • 银行家算法:

    • 所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。
  • 按照银行家算法的思想,当进程请求资源时,系统将按如下原则分配系统资源:

    1. 当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。
    2. 进程可以分期请求资源,当请求的总数不能超过最大需求量。
    3. 当系统现有的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。
    4. 当系统现有的资源能满足进程尚需资源数时,必须测试系统现存的资源能否满足该进程尚需的最大资源数,若能满足则按当前的申请量分配资源,否则也要推迟分配。
  • 解决死锁的策略

    1. 死锁预防:破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
    2. 死锁避免:避免是指进程在每次申请资源时判断这些操作是否安全,例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
    3. 死锁检测:死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
    4. 死锁解除:这是与死锁检测结合使用的,它使用的方式就是剥夺。即将某进程所拥有的资源强行收回,分配给其他的进程。
  • 死锁的避免

    死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。

    死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。

    避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:

    1. 如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
    2. 如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;

    显然要避免死锁,必须事先知道系统拥有的资源数量及其属性

进程间同步

进程与线程同步的区别

由于线程天生共享资源,所以必须需要考虑互斥或同步;而进程之间是资源独立的,所以没有必要的情况,无需考虑进程间同步或互斥,除非需要针对某一个独立的资源实现竞争或共享操作,此时需要考虑同步和互斥。

进程间同步的方式

  1. 互斥锁

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

我们重点看第二个参数:pshared,它有以下两个取值: - 线程锁:PTHREAD_PROCESS_PRIVATE (mutex的默认属性即为线程锁,进程间私有) - 进程锁:PTHREAD_PROCESS_SHARED - 要想实现进程间同步,需要将mutex的属性改为PTHREAD_PROCESS_SHARED。

These functions get and set the process-shared attribute in a mutex
attributes object.  This attribute must be appropriately set to
ensure correct, efficient operation of a mutex created using this
attributes object.

The process-shared attribute can have one of the following values:

PTHREAD_PROCESS_PRIVATE
        Mutexes created with this attributes object are to be shared
        only among threads in the same process that initialized the
        mutex.  This is the default value for the process-shared mutex
        attribute.

PTHREAD_PROCESS_SHARED
        Mutexes created with this attributes object can be shared
        between any threads that have access to the memory containing
        the object, including threads in different processes.

pthread_mutexattr_getpshared() places the value of the process-shared
attribute of the mutex attributes object referred to by attr in the
location pointed to by pshared.

pthread_mutexattr_setpshared() sets the value of the process-shared
attribute of the mutex attributes object referred to by attr to the
value specified in pshared.

If attr does not refer to an initialized mutex attributes object, the
behavior is undefined.
  1. 信号量

int sem_init(sem_t *sem, int pshared, unsigned int value); 初始化信号量

主要注意第二个与第三个参数,pshared指明信号量是否进程共享,如果pshared为0, 则只能在同一个进程中使用,如果不等于0,则能在进程之间共享,但需要使用共享内存的方式共享信号量(详情见下图官方文档说明); value指明能够同时运行的线程或者进程数量,wait会使其减1, post会使其加1

sem_init() initializes the unnamed semaphore at the address pointed
to by sem.  The value argument specifies the initial value for the
semaphore.

The pshared argument indicates whether this semaphore is to be shared
between the threads of a process, or between processes.

If pshared has the value 0, then the semaphore is shared between the
threads of a process, and should be located at some address that is
visible to all threads (e.g., a global variable, or a variable
allocated dynamically on the heap).

If pshared is nonzero, then the semaphore is shared between
processes, and should be located in a region of shared memory (see
shm_open(3), mmap(2), and shmget(2)).  (Since a child created by
fork(2) inherits its parent's memory mappings, it can also access the
semaphore.)  Any process that can access the shared memory region can
operate on the semaphore using sem_post(3), sem_wait(3), and so on.

Initializing a semaphore that has already been initialized results in
undefined behavior.
  1. 共享内存

在实现线程同步时并不需要共享内存,因为只要信号量是一个全局变量,线程之间就能够共享该信号量,但进程与线程有所不同.

在创建父子进程时,子进程会将父进程中的变量拷贝一份,所以父子进程在使用同一变量时互不影响,因为子进程只是使用变量的副本.为了实现进程同步,只能使用共享内存的方式共享信号量.

进程与线程的关系和区别