多线程(2)---(同步&互斥&互斥量)

    上一个博客我们学习了线程的创建,如如何查看线程ID,线程与进程之间的区别等等,可以参考上篇博客线程(1)。而这一篇博客我们继续来学习线程,本篇博客我们的目标是:

1、学会线程同步

2、学会使用互斥量、条件变量

线程同步与互斥

mutex(互斥量)

    大部分情况下,线程使用的数据都是局部变量,变量的地址空间在栈空间内,这种情况变量属于单个线程,其它线程无法获得这种变量。但是有的时候,很多变量都需要在线程间共享,这样的变量称之为共享变量,可以通过数据的共享,来实现县城之间的交互。如果多个线程之间的并发操作共享变量,那么一定会出现一些问题的。

接下来我们来看一了例子。这个例子是我们生活中常见的买票问题:

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

int ticket=500;//总票数
void* sell_ticket(void *arg)
{
    while(1)
    {   
        if(ticket>0)
        {
            usleep(1000);
            printf("%s get ticket%d\n",(char*)arg,ticket);
            ticket--;
        }
        else
           break;  
    }
}

int main()
{
    //创建四个线程进行买票操作
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,sell_ticket,"thread 1");  
    pthread_create(&tid2,NULL,sell_ticket,"thread 2");  
    pthread_create(&tid3,NULL,sell_ticket,"thread 3");  
    pthread_create(&tid4,NULL,sell_ticket,"thread 4");  

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);
    return 0;
}

结果如下:



    我们发现竟然出现了 0 ,-1,-2。现实生活中我们是不会出现这种问题的,因为票数不可能为0,-1,-2。票务系统多卖出了三张票。这时我们就要来分析问题出现的原因了。

    首先我们先来看一下代码ticket--,这个代码的意思是对ticket进行减一操作。在这个操作中,整数变量刚开始存储于内存中,当我们要执行这个操作时就需要先将数据从内存拿到CPU,然后在CPU中对整数--,最后再把数据写回到内存。这一句代码如果换成汇编的话就是三条了。 我们做个假设,线程A先执行代码,一直到ticket--的操作,当它在CPU中对票数进行--操作后,准备将数据写回到内存的时候它被切走了,这个时候线程B进来了,线程B买完所有的票,这个时候票数变成了0,B买完票后,线程B走了。这时线程A回来了继续执行自己刚刚未完成的操作,即将数据写回内存。然而我们知道此时已经没有票了,但是线程A不清楚,还在执行自己的操作,这时就发生了数据不一致问题。

我们知道根本原因是:在对临界资源(即票数)进行操作的时候,该操作不是一个原子操作。

为了解决上述问题,我们需要做到三点:

1、代码必须要有互斥行为:当代码进入到临界区时,不允许其他线程进入该临界区。

2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个新城进入该临界区。

3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入该临界区。

要做到这三点,其本质上就是需要一个锁,Linux上提供这把锁叫做互斥量


现在我们来看看互斥量的接口有哪些

初始化互斥量

初始化互斥量有两种方法:

方法1:静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2:动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
    mutex:要初始化的互斥量
    attr:NULL

销毁互斥量

注意:

1、用静态分配方法初始化的互斥量不用进行销毁

2、不要销毁一个已经加锁的互斥量

3、一个已经销毁的互斥量要保证后面不会在有别的线程来尝试加锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

原型:

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

参数:

    mutex:需要加锁和解锁的互斥量

返回值:成功返回0,失败返回错误码

调用pthread_lock时,可能会遇到以下问题:

1、互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

2、发起函数调用时,其它线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁。

现在我们对之前的售票系统进行改进:

代码如下:

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

int ticket=500;//总票数
mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex;//定义互斥量

void* sell_tic(void *arg)
{
    while(1)
    {   
        pthread_mutex_lock(&mutex);//加锁
        if(ticket>0)
        {
            usleep(1000);
            printf("%s get ticket%d\n",(char*)arg,ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);//解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);//解锁
            break;   
        }
    }
}

int main()
{
    pthread_mutex_init(&mutex,NULL);//初始化互斥量
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,sell_tic,"thread 1");  
    pthread_create(&tid2,NULL,sell_tic,"thread 2");  
    pthread_create(&tid3,NULL,sell_tic,"thread 3");  
    pthread_create(&tid4,NULL,sell_tic,"thread 4");  

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

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

结果如下:



看到结果,我们发现已经解决了之前票务系统的问题了!!!

条件变量

    当一个线程互斥地访问某一个变量时,它可能发现在其他线程改变状态之前,什么都改变不了。例如一个线程访问队列时,队列为空,它只能等待,直到其它线程将一个节点添加到其它队列中,这时就需要条件变量。

那么条件变量有哪些函数呢?

初始化

原型:

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)

等待条件满足

原型

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
    cond:要在这个条件变量上等待

    mutex:互斥量,后⾯面详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

现在我们来看一了例子:

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

pthread_cond_t cond;
pthread_mutex_t mutex;

void* run1(void* arg)
{
    while(1)
    {
        pthread_cond_wait(&cond,&mutex);
        printf("active\n");
    }
}
void* run2(void* arg)
{
    while(1)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

int main()
{
    pthread_t t1,t2;

    pthread_cond_init(&cond,NULL);
    pthread_mutex_init(&mutex,NULL);

    pthread_create(&t1,NULL,run1,NULL);
    pthread_create(&t2,NULL,run2,NULL);

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

结果如下:


    大家可以自行验证,如果没有条件变量,那么线程1就会一直不停的打印active,但是如果加上条件变量之后,线程2每隔一秒唤醒一次线程1,线程1每隔一秒打印一次active

现在可能大家还会有一个问题,为什么需要pthread_cond_wait需要互斥量呢??

    这里我解释一下:条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,那么一直等下去还是不满足的话,这时就必须有一个线程通过某些操作来改变共享变量,使原先不满足的条件变为满足,并且通知等待在条件变量上的线程。因为条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来进行保护。如果没有互斥锁,就无法安全的获取和修改共享数据了。

我们现在来设计一个代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上。

    // 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
    pthread_mutex_unlock(&mutex);
    //解锁之后,等待之前,条件可能已经满⾜足,信号已经发出,但是该信号可能被错过
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
     由于解锁和等待不是原子操作。调用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。int pthread_ cond_ wait(pthread_ cond_ t *cond,pthread_ mutex_ t * mutex); 进入该函数后,会去看条件量是否等于0?如果等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。




猜你喜欢

转载自blog.csdn.net/lu_1079776757/article/details/79884691