线程的同步与互斥,消费者生产者,POSIX信号量,读者写者

mutex(互斥量)

在大部分情况下,线程使用的数据都是局部变量,那么变量的地址空间在线程栈的空间内,这样的变量属于单一线程,而其它的线程是无法访问的.那么,线程间通信就是要访问相同的资源,从而完成线程间的交互.
可是,很多个线程并发的操作某个资源时,就会带来一些问题:
例如:下面是一个简单的购票系统.

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

int Total_Ticket = 100;
void* Entry(void* arg)
{
    int64_t id = (int64_t)arg;
    while(1)
    {
        if(Total_Ticket > 0)
        {
            usleep(1000);
            printf("now ticket is %d,%lu is buying ticket\n",Total_Ticket,id);
            --Total_Ticket;
        }
        else
        {
            break;
        }
    }
    return NULL;
}
int main()
{
    pthread_t t[4];
    int64_t i = 0;
    for(i = 0;i < 4;++i)
    {
        pthread_create(&t[i],NULL,Entry,(void*)i);
    }
    for(i = 0;i < 4;++i)
    {
        pthread_join(t[i],NULL);
    }
    return 0;
}

运行结果:
这里写图片描述
从上述图片中可以发现:多个线程同时访问一个全局变量存在较大的问题.
问题:

  • 在Entry函数中,当if条件判断为真后,代码可能会被并发的切换到其它线程.
  • 在usleep这个等待过程中,可能会有多个线程进入改代码块.
  • –Total_Ticket不是一个原子操作.(①将共享变量Total_Ticket加载到寄存器中;②将寄存器中的值进行减一操作③将寄存器中的新值写回到共享变量对应的地址中)

因此:为了解决上述的问题,引进了互斥量:
目的:对于临界资源同一时刻只能有一个线程对其操作.

解决:

  • 当代码进入临界区执行时,其它线程不允许进入该临界区.
  • 如果同时多个线程同时要求执行临界区的代码,而此时临界区没有任何线程在使用,那么只能允许一个线程进入该临界区.
  • 如果线程不在临界区执行,那么该线程也不能阻止其他线程进入该临界区.(即要及时释放)

这里写图片描述
下面是上面代码的修改:

int Total_Ticket = 100;
pthread_mutex_t mutex;
void* Entry(void* arg)
{
    int64_t id = (int64_t)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);  //加锁,如果此时也有其他进程准备访问,那么此时只能阻塞的等待解锁后再访问该临界区的资源.
        if(Total_Ticket > 0)
        {
            usleep(1000);
            printf("now ticket is %d,%lu is buying ticket\n",Total_Ticket,id);
            --Total_Ticket;
            pthread_mutex_unlock(&mutex);//解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);//解锁
            break;
        }
    }
    return NULL;
}
int main()
{
    pthread_t t[4];
    int64_t i = 0;
    pthread_mutex_init(&mutex,NULL);
    for(i = 0;i < 4;++i)
    {
        pthread_create(&t[i],NULL,Entry,(void*)i);
    }
    for(i = 0;i < 4;++i)
    {
        pthread_join(t[i],NULL);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

只要保证某一时刻临界资源只有一个线程在访问.

死锁问题:
产生死锁原因:

  • 连续两次加锁.
  • n个线程同时加n把锁.(例如:哲学家进餐)
    线程安全和可重入函数:
    可重入函数:在多个执行流中调用不会存在逻辑问题. 可重入函数对应的是线程and信号处理函数.
    线程安全函数:在多线程中被同时调用不会存在问题.线程安全对应的是:线程
    所以:可重入函数一般是线程安全的,而线程安全函数不一定是可重入的.
    例如:以下的代码:
#include <stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<signal.h>

pthread_mutex_t mutex;
void Func()
{
    pthread_mutex_lock(&mutex);
    printf("lock\n");
    sleep(2);     //如果在此时按下ctrl-c就会触发信号,然后两次加锁.
    printf("unlock\n");
    pthread_mutex_unlock(&mutex);
}
void* Pthread(void* arg)
{
    (void)arg;
    while(1)
    {
        Func();   //一直循环的进行Fun()函数
    }
    return NULL;
}
void Handler(int sig)
{
    (void)sig;
    Func();
}
int main()
{
    signal(SIGINT,Handler);
    pthread_mutex_init(&mutex,NULL);
    pthread_t t1;
    pthread_create(&t1,NULL,Pthread,NULL);
    pthread_join(t1,NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

在上述的代码中,如果在线程入口Fun()函数中的lock()后而unlock()前按下ctrl-c就会导致死锁问题.线程入口函数的Fun()函数在等待信号处理函数执行结束后释放锁,而信号处理函数在等待线程入口函数Fun()函数结束后释放锁.那么此时两方就会处于僵持状态,阻塞着去等待另一方去释放锁.


条件变量

互斥量是一种互斥的机制,即在访问临界资源时同一时刻只能有一个线程去访问.而条件变量时一种同步互斥机制.比如:如果此时只有一个线程,那么如果执行某个条件时不满足条件,那么该线程即使一直等一直等也都不会满足条件.那么此时如果有另外一个线程通过某些操作使得那个变量满足条件了,那么第一个线程就可以去执行了.
下面是一个传球–投球的例子:

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

pthread_mutex_t mutex;
pthread_cond_t cond;

void* Pass(void* arg)
{
    while(1)
    {
        printf("传球:%s\n",arg);
        usleep(456879);   //注意usleep的时间的长短.
        pthread_cond_signal(&cond);
    }
    return NULL;
}
void* Shoot(void* arg)
{
    while(1)
    {
        pthread_cond_wait(&cond,&mutex);   //这个函数时原子性的,否则就会引起竞态条件问题.()
        printf("投球:%s\n",arg);
        usleep(123432);    //注意usleep的时间的长短.
    }
    return NULL;
}
int main()
{
    pthread_t pass;
    pthread_t shoot;
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);

    pthread_create(&pass,NULL,Pass,(void*)"Pass");
    pthread_create(&shoot,NULL,Shoot,(void*)"Shoot");

    pthread_join(pass,NULL);
    pthread_join(shoot,NULL);

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

结果:先传球再投球,一直循环.

—————————————————————————————-

等待条件代码的使用规范:

pthread_mutex_lock(&mutex);
while(条件) //使用while而不使用if:因为可能该条件可能为假,那么就需要一直去判断
    pthread_cond_wait(&cond,&mutex);  //该函数有3个操作步骤:①解锁②等待条件变量③重新加锁.
pthread_mutex_unlock(&mutex);

给条件发送信号的代码:

pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);//所以这个函数也有3小步要进行执行.
pthread_mutex_unlock(&mutex);

—————————————————————————————-

生产者消费者模型

前提:生产者生产物品,然后将物品放入仓库,消费者去仓库中取东西进行消费.仓库中在同一时刻只能有一个人,而若仓库为空,若有消费者去取物品,那么该消费者就要阻塞着去等待物品放入仓库中才可以进入取;同样的,若仓库已满,那么生产者就不能再往仓库中方物品,而要阻塞着等待消费者取后仓库中有空位置生产者才可以再次进去.

生产者消费者模型是一个同步互斥模型
互斥关系:
生产者与生产者之间
消费者与消费者之间
同步互斥:
生产者与消费者之间

下面是基于动态链表的生产者消费者模型.
这里写消费者生产者模型,若看代码请戳

POSIX信号量

目的:对于临界资源的访问没有冲突.
可用于线程间同步.
下面是POSIX信号量支持的函数:

#include<semaphone.h>
1.初始化信号量:
int sem_init(sem_t *sem,int pshared,unsigned int value);
参数:pshared:
0:表示线程间共享
非0:表示进程间共享.
value:信号量的初值.

2.销毁信号量
int sem_destroy(sem_t *sem);

3.等待信号量 :会将信号量的值减一
int sem_wait(sem_t *sem);

4.发布信号量:表示资源使用完毕,可以归还给系统.信号量的值将加一
int sem_post(sem_t *sem);

同样的,也可以基于POSIX信号量来实现上述的生产者消费者模型.
基于固定大小的环形队列实现一个消费者生产者模型.
这里是POSIX版本的生产者消费者模型的代码,若需要请戳

读者写者问题

读者写者是一个经典的同步机制,即读者多而写着少,并且在写时不能进行读,在读时不能进行写,但是多个读者可以同时读,而写时只能有一个写者进行写.

  • 即:写独占,读共享,写着优先级高于读者.

这里是读者写者的代码实现,若需要请戳
这儿的读写锁是一种自旋锁.

互斥锁与自旋锁:
自旋锁:就是当在获取锁时,发现该锁已经被其他线程上锁,但是自旋锁则会一直去请求获取锁,知道拿到了锁.这样对于CPU无疑是一种开销,会浪费CPU资源,但是性能高.
互斥锁:当在获取锁时发现锁已经上锁,那么此时就会阻塞着去等待锁的释放,而不是一直去请求获取锁.

猜你喜欢

转载自blog.csdn.net/yinghuhu333333/article/details/80866156