多线程的同步与互斥

mutex互斥锁实现互斥
为什么需要互斥?      
    前边已经说过了,线程是在进程内部的,即线程是共享进程的地址空间的。线程拥有仅属于自己的一部分数据的:线程ID、一组寄存器(用于保存自己的上下文信息)、栈、errno、信号屏蔽字、调用优先级。所以大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程空间内,其他线程是不能访问到的,但是,通常同一个进程内的线程之间是需要交互的,那就代表着很多变量需要在线程间共享,所有的线程都是可以看见的(比如说全局变量),这些变量就叫做共享变量。在进程内部设置全局变量,所有线程都可以看到,这让线程间的交互变得简单,但是,同时又带来了一些问题。怎么保证大家看到的数据是一致的和多个线程多共享的数据进行并发的操作不会出错?
       多个线程看到了同一个变量,变量也是一种资源,也就是说,这些被多个线程看到的变量都是属于临界资源,而对于临界资源的访问,必须是原子的。比如说在对一个变量进行增加的操作,虽然在我们看来就只有一行代码,但是,计算机在实现的时候是分为三步来完成的:
(1)、从内存单元中将变量读入到寄存器中。
(2)、在寄存器中进行变量值的增加。
(3)、把新的值写回内存单元。
如果多个线程同时在对该数据进行增加操作,或者说,有的在读,有的在写,那都很有可能导致对数据的修改和读取跟我们预期的不一样。多个线程读到的数据不一致,也就失去了交互的意义。看如下一个例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 10;
void* thread_run(void* arg)
{
    char* s = (char*)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s selled a tecket %d\n",s,ticket);
            ticket--;
        }
        else
            break;
    }
    return (void*)0;
    
}
int main()
{
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,thread_run,"thread1:");
    pthread_create(&tid2,NULL,thread_run,"thread2:");
    pthread_create(&tid3,NULL,thread_run,"thread3:");
    pthread_create(&tid4,NULL,thread_run,"thread4:");
    
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);
    return 0;
}

如上程序是在模拟一个买票系统,虽然做出了控制,如果票卖完了就停止,然而,从结果来看,还是在卖完的情况下额外卖了两张(本来不存在的票)

所以,必须保证对共享数据的访问时原子的,一个线程对数据的访问要么一次性完成,要么访问失败,在访问期间其他线程是不能对它进行访问的。
代码必须要有互斥行为:
(1)、当代码进入临界区执行时,不允许其他线程进入该临界区。
(2)、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进 入该临界区。
(3)、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

通过互斥量实现线程的同步与互斥
       通过对访问数据进行加锁来确保同一时间只有一个线程访问数据。Linux将这把锁叫做互斥量(mutex)。在访问数据之前先对互斥量进行加锁,加锁后其他的线程就不能对该互斥量进行访问,如果此时任意某个线程尝试着也去给该互斥量加锁的话,就会将该线程阻塞。直到当前正在访问的线程访问结束后,将锁释放。阻塞的线程就可以重新申请到锁并对互斥量进行加锁访问了。每一个只允许一个线程获得锁,并且让所有的线程都遵守这样的规则。数据访问不一致的问题就解决了。
具体要怎么实现呢?
看一下Linux系统为我们提供的 互斥量的接口

初始化互斥量
方法1:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITAILIZER
方法2:动态分配
int pthread_mutex_init(pthread_mutex_t*  restrict mutex,const pthread_mutexattr_t* restrict attr);
    参数:mutex是要初始化的互斥量
             attr一般置为NULL

销毁互斥量
intpthread_mutex_destroy(pthread_mutex_t*  mutex);
销毁互斥量时需要注意:
    使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
    不能销毁一个已经加锁的互斥量。
    确保对于一个已经销毁的互斥量,不会再尝试对其加锁。

互斥量加锁和解锁
加锁使用:int pthread_mutex_lock(pthread_mutex_t*  mutex);
解锁使用:int pthread_mutex_unlock(pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误号。
在调用pthread_mutex_lock函数进行加锁时,如果互斥量处于未锁状态,那么就会对该互斥量进行加锁,然后返回0;如果该互斥量已经被加锁或者有其他线程也正在申请加锁但没有竞争到互斥量,那么,该线程会陷入阻塞状态,等到该互斥量被解锁。

使用互斥量将上边的售票系统进行改进:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 10;
pthread_mutex_t mutex  = PTHREAD_MUTEX_INITIALIZER;
void* thread_run(void* arg)
{
    char* s = (char*)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s selled a tecket %d\n",s,ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return (void*)0;
    
}
int main()
{
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,thread_run,"thread1:");
    pthread_create(&tid2,NULL,thread_run,"thread2:");
    pthread_create(&tid3,NULL,thread_run,"thread3:");
    pthread_create(&tid4,NULL,thread_run,"thread4:");
    
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);
    return 0;
}
经过改进,售票系统正常运行。使用互斥量的方法实现同步与互斥就讲到这里。

通过条件变量实现同步
为什么需要同步?
        相信大家都有过网上购物的经验,我们可以通过自己的订单来查看物流,当快要到达目的地的时候,查看物流的地方会有显示正在派送中......这个时候我们就知道买的东西已经到达目的地附近了。但是,这个时候我们并不会立马就去取,因为我们知道收快递是有一套流程的,到了目的的还需要给我们的快递编号之类的。所以我们会在等到收到一条信息告诉我们什么时候去哪,快递编号是什么后,才会去取。这里的信息其实就相当于是一个 条件变量
        我们 可以把快递驿站看做是一个线程A,把取快递的人看做是另一个线程B,在这里就简化为线程A是修改数据,线程B是取数据的,只有线程A将数据写好了线程B取得的数据才有效,才会进行取数据的操纵。通常情况下,为了保证一个共享数据的一致性和有效性,我们会将共享数据设置成是互斥访问的,也就是上边所说的互斥。线程要访问数据就必须先申请锁,申请成功后进行上锁,保证其他线程无法进入临界区进行访问,访问结束后,解锁,其他线程就可以申请锁并访问了。如果线程B进入到临界区里边发现数据还没有被写入,然后就在那等着数据被写入,但是由于临界区是被互斥访问的,因为线程B一直在临界区里边,线程A就没法申请到锁进入到临界区去写数据,这也就意味着线程B想要的数据永远也得不到满足了, 这就造成了死锁。所以为了避免死锁,线程B应该在发现数据在没有被写入的时候就立马释放锁,然后等待数据写入,然后不时的去查看数据是否被写入。(这样也依然可能会影响数据的写入)。 如果线程A将数据写好了,然后通过条件变量通知线程B,然后线程B就知道自己可以去取数据了,这样就可以避免如上的问题了。 这个过程就叫做线程的同步。
        条件本身是有互斥量保护的。线程在改变条件变量前必须先锁住互斥量。使用 pthread_cond_t 数据类型代表条件变量, 条件变量使用之前必须首先对其进行初始化,和互斥量相似,它同样有两种方式:

通过宏常量 PTHREAD_COND_INITALIZER 初始化静态分配的条件变量;
通过调用函数pthread_cond_init 初始化动态分配的条件变量。
     原型:int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t*  restrict attr);
     参数:cond 要初始化的条件变量。
                 attr 通常设置为NULL就行了
条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足
#include <pthread.h>
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 timeout); 
参数: cond:要在这个条件变量上等待 
mutex:互斥量,对条件进行保护 
timeout:指定等待时间,它是通过timespec结构指定。时间值用秒数或者分秒数来表示。
struct timespec 
{
time_t tv_sec;
long tv_nsec; 
}
返回值:成功返回0,错误返回错误编号。
说明:
        1、当线程发现自己所需的数据还没有得到满足的时候进行解锁然后等待这个过程必须是原子操作。如果不是原子操作。调⽤用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,并且条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。
       2、解锁和等待这个原子操作是通过pthread_ cond_ wait这一个函数完成的,所以调用该函数会执行如下操作,会去看条件量是否等于0(0表示不满足),等于,也就是条件变量不满足,就把互斥量变成1(即释放锁),直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);  用于唤醒当前等待的所有的线程。
int pthread_cond_signal(pthread_cond_t *cond); 用于唤醒指定的线程。

条件变量的使用规范
等待条件变量满足
    pthread_mutex_lock(&mutex);  //上锁
    while (条件为假)
           pthread_cond_wait(cond, mutex);//等待
    比人修改了条件
    pthread_mutex_unlock(&mutex);//解锁

给条件发送信号代码
    pthread_mutex_lock(&mutex);//上锁
    设置条件为真
    pthread_cond_signal(cond);//发送信号
    pthread_mutex_unlock(&mutex); //解锁

举个例子
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex ;
pthread_cond_t cond ;
void* thread_run1(void* arg)
{    
    pthread_cond_wait(&cond,&mutex);
    printf("条件满足\n");
    return (void*)0;
}
void* thread_run2(void* arg)
{
    sleep(2);
    pthread_cond_signal(&cond);
    printf("已发送条件满足信号\n");
    return (void*)0;
}
int main()
{
    pthread_t tid1,tid2;
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_create(&tid1,NULL,thread_run1,"thread1:");
    pthread_create(&tid2,NULL,thread_run2,"thread2:");
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}
 

猜你喜欢

转载自blog.csdn.net/guaiguaihenguai/article/details/80092365