多线程的同步与互斥

当多个控制线程共享相同的内存时,要确保对数据访问的正确性,就需要做线程的同步与互斥工作。先看下面这个例子:

为什么要进行线程的同步与互斥

#include 
#include 
#include 
#include 
#include 

size_t count = 0;

void ModifyCount()
{
    ++count;
}

void* ThreadEntry(void *arg)
{
    (void)arg;
    for(size_t i = 0; i < 10000; ++i){
        ModifyCount();
    }
    return NULL;
}

int main()
{
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, ThreadEntry, NULL);
    pthread_create(&tid2, NULL, ThreadEntry, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("count: %ld\n", count);

    return 0;
}

我们让两个线程同时对一个全局变量做 ++ 操作,各自增加 10000次,如果程序执行无误,结果应该是 20000,实际上运行结果如下:
多线程的同步与互斥_第1张图片

我们让该程序执行了多次,发现只有两次的结果正确,而其他的结果都超出了我们的预期,原因在哪呢?
我看看看++ 操作的汇编代码:
多线程的同步与互斥_第2张图片
对 count 的增加有三步:

  • 将 count 移至寄存器中
  • 给寄存器中的值加1
  • 再将寄存器中的值移回count

显然,对count的增加并不是一个原子操作,如果两个线程同时访问count,就可能会出现下面这种情况:

  • 线程1将count的值移至寄存器(count = 0,eax1 = 0);
  • 线程1对寄存器中的值++(count = 0, eax1 = 1);
  • 线程2拿到count的值(count = 0, eax2 = 0);
  • 线程1将寄存器中的值写回count(count = 1,eax1 = 1);
  • 线程2对寄存器中的值++(count = 1, eax2 = 1);
  • 线程2将寄存器中的值写入count(count = 1, eax2 = 1)。

此时,看似两个线程对 count 各加了一次,实际上,count 的值只被加了一次,如何解决这种问题呢?下面介绍几种方法。

mutex互斥量

可以使用 pthread 的护齿接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完之后解锁。对互斥量进行加锁之后,任何试图对互斥量加锁的操作都会被阻塞,直到当前线程释放该互斥锁。如果释放互斥量时,有一个以上的线程被阻塞,那么该锁上的线程都会变为运行状态,其中第一个变为运行状态的线程就可以申请到互斥量并对其加锁。

互斥量的初始化
方法一,静态分配:

pthread_mutex mutex = PTHREAD_MUTEX_INITIALIZER;

方法二,动态分配:

#include 
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);

其中 pthread_mutex_t 是一个表示互斥量的联合体类型,函数的第一个参数是要初始化的互斥量,第二个参数是要设置的互斥量的属性,参数二为空则表示使用默认的属性。

静态初始化的互斥量不需要销毁,而对于动态初始化的互斥量则需要销毁,其接口函数如下:

#include 
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数为要销毁的互斥量,上面的初始化和销毁的函数执行成功返回 0,出错返回错误码。

下面是对互斥量的加锁和解锁函数:

#include 
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

第一个函数 pthread_mutex_lock 对互斥量阻塞式加锁,如果互斥量已经上锁,则调用线程将则阻塞式等待,直到互斥量被解锁。pthread_mutex_unlock 用于解锁,如果不希望阻塞式的加锁,则使用第二个函数 pthread_mutex_trylock ,该函数尝试对互斥量加锁,如果互斥量未被上锁,则将互斥量锁住,否则,该函数返回 EBUSY

下面是对加了锁的临界资源访问的一个实例:

#include 
#include 
#include 
#include 
#include 

pthread_mutex_t lock;//定义互斥量
size_t count = 0;

void ModifyCount()
{
    pthread_mutex_lock(&lock);
    ++count;
    pthread_mutex_unlock(&lock);
}

void MyHandler()
{
    ModifyCount();
}

void* ThreadEntry(void *arg)
{
    (void)arg;
    for(size_t i = 0; i < 10000; ++i){
        ModifyCount();
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&lock, NULL);//初始化互斥量
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, ThreadEntry, NULL);
    pthread_create(&tid2, NULL, ThreadEntry, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("count: %ld\n", count);

    pthread_mutex_destroy(&lock);//销毁互斥量
    return 0;
}

运行结果:
多线程的同步与互斥_第3张图片

我们在对 count++ 操作前锁住互斥量,操作完成后解锁互斥量,这样就不会在多个线程修改共享资源时出现错误了。

线程死锁
想想下面这种情况:
线程(也可以是进程)A,B各需要申请 1,2两个互斥量,此时,线程 A 得到了 1 号互斥量(即线程A已对1号互斥量加锁),它在等待互斥量 2,而线程 B 获取到了 2 号互斥量 ,它在等待 1 号互斥量,A,B两个线程既在等待对方占用的互斥量,又在占用着对方所需要的互斥量,由于这种情况造成的两个(或多个)线程都无法前进的情况称为死锁。

产生死锁的必要条件

  • 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放;
  • 请求保持:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源;
  • 不可剥夺:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放;
  • 环路等待:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请地资源。

银行家算法避免死锁
银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。

条件变量

条件变量是线程同步可用的另一种同步机制。当某一条件满足时,线程 A 可以通知阻塞在条件变量上的线程 B , B 所期望的条件已经满足,可以解除在条件变量上的阻塞操作,继续做其他事情。

条件变量 API

#include 
int pthread_cond_init(pthread_cond_t *restrict cond,
        const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:成功0,失败返回错误码

第一个函数用于初始化一个条件变量,参数一为你要初始化的条件变量,参数二为条件变量的属性,为空表示使用默认属性。第二个函数用于销毁一个条件变量。
最后一行是另一种初始化条件变量的方法,这种方法用于静态分配条件变量。

#include 
int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime);
返回值:成功0,失败返回错误码

第一个函数中参数 cond 为要等待的条件变量,参数 mutex 用于保护条件变量,调用者将锁住的互斥量传给函数。函数把调用线程放到等待线程的列表上,然后对互斥量解锁,这两个操作时原子操作。pthread_cond_wait 返回时再次锁住互斥量。第二个函数与第一个类似,知识多了一个指定等待时间的参数 timeout

#include 
int pthread_cond_signal(pthread_cond_t *cond);

该函数向线程或条件变量发送信号,用于唤醒等待的线程,告诉该线程你等待的条件已经成熟。

下面是一个实例:

#include 
#include 
#include 

pthread_cond_t g_cond;
pthread_mutex_t g_lock;

void* Entry1(void *arg)
{
    (void)arg;
    while(1){
        printf("pass!\n");
        pthread_cond_signal(&g_cond);//当传球以后再通知投球
        usleep(678123);
    }
}

void* Entry2(void *arg)
{
    (void)arg;
    while(1){
        pthread_cond_wait(&g_cond, &g_lock);//等待别人给自己传球,若条件成熟,则下一步投球
        printf("shoot!\n");
        usleep(123456);
    }
}

int main()
{
    pthread_cond_init(&g_cond, NULL);//初始化条件变量
    pthread_mutex_init(&g_lock, NULL);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, Entry1, NULL);
    pthread_create(&tid2, NULL, Entry2, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_cond_destroy(&g_cond);
    pthread_mutex_destroy(&g_lock);
    return 0;

在篮球场上,投球的人需要等到别人将球传给自己,而传球的人要通知投球的人“我将球传给你了,你准备投,他们按照这样约定的顺序,擦能实现有序配合”,下面是运行结果:

多线程的同步与互斥_第4张图片

POSIX信号量

POSIX信号量也可以用于保护共享资源,以达到对共享资源的互斥访问。与 systemV 信号量不同的是:

POSIX信号量来源于POSIX技术规范的实时扩展方案 (POSIX Realtime Extension),常用于线程;system v信号量,常用于进程的同步。这两者非常相近,但它们使用的函数调用各不相同。前一种的头文件为semaphore.h,函数调用为sem_init(), sem_wait(), sem_post(),sem_destory()等等。后一种头文件为,函数调用为semctl(),semget(),semop()等函数.

POSIX信号量API:
初始化和销毁信号量:

#include 
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

sem 为要初始化的信号量,pshared 为 0 表示线程间共享,非 0 表示进程间共享。
value表示要初始化为几?

等待信号量和发送信号量:

#include 
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);

如果将信号量看为一个计数器的话,那么对信号量的等待和唤醒则是对计数器的 -1+1 操作。

下面是用 POSIX 信号量重新实现的上面那个“传球-投篮”的例子:

#include 
#include 
#include 
#include 

sem_t sem;

void* Entry1(void *arg)
{
    (void)arg;
    while(1){
        printf("pass!\n");
        sem_post(&sem);
        usleep(678123);
    }
}

void* Entry2(void *arg)
{
    (void)arg;
    while(1){
        sem_wait(&sem);
        printf("shoot!\n");
        usleep(123456);
    }
}

int main()
{
    sem_init(&sem, 0, 1);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, Entry1, NULL);
    pthread_create(&tid2, NULL, Entry2, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    sem_destroy(&sem);
    return 0;
}

运行结果:
多线程的同步与互斥_第5张图片

——完!


【作者:果冻:http://blog.csdn.net/jelly_9】

你可能感兴趣的:(Linux)