线程同步与互斥——互斥量、条件变量、POSIX信号量

线程的互斥与同步

一. 互斥量(mutex)——实现互斥
        大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程的栈空间内,这种情况,变量归属单个线程,其他线程都无法获得这种变量。但有时候,很多变量需要在线程间共享,这样的变量叫做 共享变量。我们可以通过数据间的共享,完成线程之间的交互。
        但是,多个线程并发的操作共享变量,会带来问题,比如下面这样:
//售票系统                                                                                                                                    
#include <stdio.h>
#include <pthread.h>

int ticket = 10000;

void* buy_ticket(void* arg)//买票
{
    const char* id = (const char*)arg;
    while(1)
    {   
        if(ticket > 0)
        {   
            printf("%s get a ticket: %d\n", id, ticket);
            ticket--;
        }   
        else
        {   
            break;
        }   
    }   
}

int main()
{
    pthread_t tid1, tid2,tid3, tid4;
    pthread_create(&tid1, NULL, buy_ticket, "thread_1");
    pthread_create(&tid2, NULL, buy_ticket, "thread_2");
    pthread_create(&tid3, NULL, buy_ticket, "thread_3");
    pthread_create(&tid4, NULL, buy_ticket, "thread_4");

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

    return 0;
}           

        如上所示为售票系统,有四个执行流都对全局变量ticket(票数)进行操作,它们对ticket的同时操作会导致ticket变量出现错误,比如下面:

这里只是执行结果的最后一小部分截图。可以看到票数本来都到1了,却突然变成了9753,这就是由于多个线程并发的对共享变量进行操作,导致了数据出现错误。因为if语句判断为真后,代码可以并发的切换到其他进程,而且ticket--操作本身就不是原子操作。
        这里学习一条命令:
objdump -d +可执行文件名//反汇编
        为了解决以上代码出现的问题,我们要做到以下要求:
(1)代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区;
(2)若有多个线程同时要求访问临界区,且临界区无线程在执行,则只能让一个线程进入该临界区;
(3)若线程不在该临界区中执行,那么该线程不能阻止其他线程进入临界区。
        要做到以上要求,本质上需要一把锁,Linux下提供的这把锁就叫做 互斥量
以下介绍一些互斥量的接口:
1. 初始化互斥量
(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  
2. 销毁互斥量
要注意:
(1)使用上述静态分配方法初始化的互斥量不需要销毁;
(2)不用销毁一个已经加锁的互斥量;
(3)已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t* mutex);   

3. 互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t* mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);//解锁
//返回值:成功返回0,失败返回错误号                       

调用pthread_lock加锁时,可能会遇到以下情况:

(1)互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;
(2)发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock会阻塞等待,直至互斥量解锁。
        所以,我们利用互斥量对上面的售票系统进行改进:
//互斥锁:售票系统                                                                                                                                    
#include <stdio.h>
#include <pthread.h>
            
int ticket = 10000;
pthread_mutex_t lock;//定义一个互斥锁
    
void* buy_ticket(void* arg)//买票
{
    const char* id = (const char*)arg;
    while(1)
    {
        pthread_mutex_lock(&lock);//对临界区加锁
        if(ticket > 0)
        {
            printf("%s get a ticket: %d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
        //不能在这里解锁,因为break后就执行不到这里了
    } 
}

int main()
{
    //pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_init(&lock, NULL);
    pthread_t tid1, tid2,tid3, tid4;
    pthread_create(&tid1, NULL, buy_ticket, "thread_1");
    pthread_create(&tid2, NULL, buy_ticket, "thread_2");
    pthread_create(&tid3, NULL, buy_ticket, "thread_3");
    pthread_create(&tid4, NULL, buy_ticket, "thread_4");

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

    pthread_mutex_destroy(&lock);//释放互斥锁
    return 0;
}              

此时,每个执行流对临界区的操作是互斥的,就不会产生以上出现的数据产生错误的问题。运行结果为:

注意,这里只是执行结果的最后一小部分截图。

二. 条件变量——实现同步

        当一个线程互斥的访问某个变量时,它可能发现在其他线程改变状态之前,它什么都做不了。比如,一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将第一个节点添加到队列中。这种情况就需要用到条件变量。下面介绍一些条件变量函数:

1. 初始化条件变量
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);                                                      
  //参数cond:要初始化的条件变量
  //参数attr:默认为NULL

2. 销毁条件变量

int pthread_cond_destroy(pthread_cond_t* cond);      

3. 等待条件满足

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

注意:

(1)要进入临界区对临界资源进行操作,所以首先要申请互斥量。 
(2)如果发现等待条件满足,则使用wait使线程挂起等待;如果等待条件不满足,就对临界资源进行操作,释放锁。
(3)当对临界资源操作完成后,释放互斥量,退出临界区。

在执行等待操作时,其实完成了以下事情:
(1)当等待条件满足时,挂起调用它的线程 ;
(2)释放互斥量 ;
(3)当再一次被唤醒并且切换到该线程后,会重新自动获得互斥量,并在等待处继续往下执行。
4. 唤醒等待
int pthread_cond_broadcast(pthread_cond_t* cond);//唤醒一群                                                                                         


int pthread_cond_signal(pthread_cond_t* cond);//唤醒某一指定的

当条件满足之后,就要唤醒在条件变量下等待的线程。

        例如,在上述的对队列进行操作时,队列为空时,线程1在该条件下等待并释放互斥量退出临界区。当线程2插入结点使得队列非空时,使线程1等待的条件就不满足了,此时就调用该函数来唤醒等待中的线程1。当重新切换回线程2时,从等待处继续往下执行。

因此,该函数在使用时:
(1)进入临界区对临界资源进行操作,所以要先申请互斥量
(2)使等待条件为假,如插入线程2向队列中插入节点
(3)调用signal。如果此时有线程在等待,则唤醒它。若没有等待的线程,则该函数什么也不做
(4)解锁互斥量,退出临界区

下面编写代码使用条件变量:
//条件变量:每隔一秒打一条消息                                                                                                                        
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;//互斥量
pthread_cond_t cond;//条件变量

void* run1(void* arg)//实现打印功能
{
    while(1)
    {   
        printf("This is Young May\n");
        pthread_cond_wait(&cond, &mutex);
        printf("I am thread %d\n", (int)arg);
    }   
}

void* run2(void* arg)//实现sleep一秒的功能
{
    while(1)
    {   
        pthread_cond_signal(&cond);//唤醒指定的条件变量
        printf("I am thread %d\n", (int)arg);
        sleep(1);
    }   
}

int main()
{
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, run1, (void*)1);
    pthread_create(&tid2, NULL, run2, (void*)2);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}  
线程运行的先后顺序是由调度器决定的,这里调度器先运行了sleep的功能函数。
        上述代码实现的功能就是每个一秒打印一条消息,但是打印消息和sleep一秒是两个执行流完成的事情,而非一个执行流一起完成。上述代码运行之后,会有三个进程,主进程只是创建和销毁新线程。新线程有两个,一个去完成sleep一秒的功能,另一个就负责等待条件满足然后去打印消息。 这里就涉及到线程时序上的问题,也就是涉及到线程的同步问题。所以这里用条件变量来实现线程间的同步,即当在执行完printf之后,要在该条件下等待,等到执行完sleep之后,再来执行printf,以此循环。运行结果如下:

        在上面两个线程中,sleep线程先运行,所以,会先输出2,然后切换到1线程,输出语句this is YoungMay,接着wait进入等待,此时切换回2线程,sleep1s后,执行signal,线程1被唤醒。但是,signal之后的也会继续运行。所以1s后会输出2,执行sleep时,切换到1线程,接着wait继续运行,输出1,输出语句this is YoungMay。再进入wait,切换到2线程,1s后,signal使wait解锁,但会接着signal运行,输出2,进入sleep后,切换到1线程,输出1和语句this is YoungMay,之后就这样循环运行。

还有关于条件变量的相关实现,见另一篇博客——生产者与消费者模型

三. POSIX信号量——实现同步

        POSIX信号量与SystemV信号量作用相同,均是用于同步操作,本质上均是一个临界资源个数的计数器,为达到无冲突的访问共享资源的目的。但是POSIX信号量可以用于线程同步。下面先介绍POSIX信号量的相关接口函数:

1. 初始化信号量

(1)函数原型


(2)函数功能:初始化信号量

(3)函数参数:

        sem:要进行操作的信号量

        pshared:0表示线程间共享,非0表示进程间共享

        value:信号量的初始值

(4)返回值:成功返回0,失败返回-1

2. 销毁信号量

(1)函数原型


(2)函数功能:销毁信号量

(3)函数参数sem:要销毁的信号量

(4)函数返回值:成功返回0,失败返回-1

3. 等待信号量/申请资源

(1)函数原型:


(2)函数功能:等待信号量,会将信号量值减1

(3)函数参数:我们常用第一个函数,参数即为要等待的信号量

(4)函数返回值:成功返回0,失败返回-1

4. 发布信号量/归还资源

(1)函数原型


(2)函数功能:发布信号量,表示资源已使用完毕,可以归还资源了,会将信号量值加1

(3)函数参数sem:要归还资源的信号量

(4)函数返回值:成功返回0,失败返回-1

5. 编写代码使用测试

        见下一篇博客——生产者与消费者模型

猜你喜欢

转载自blog.csdn.net/Lycorisradiata__/article/details/80383245