您的位置:首页 > 运维架构 > Linux

Linux系统编程笔记:线程同步

2020-05-23 14:23 225 查看

线程同步

一、基本概念

1.1线程同步

线程不同步导致共享数据混乱,是由于资源共享、调度随机,缺乏同步机制。

情景:程序中有一个全局变量Val,且有两个线程,线程A需要对变量Val进行读取操作,线程B需要对变量Val进行写操作,此时可能会出现对Val资源的抢夺,会导致不能按照客户的意愿进行下去,这时就需要实行线程同步。

线程同步:协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。

1.2线程同步的方式

常见的线程同步方法有:互斥锁、条件变量、信号量等。

二、同步方法:互斥锁(互斥量)

为避免资源争夺共享资源的问题,提出互斥量(mutex)来确保同一时间只有一个线程占有资源。更为全面的说法是,可以使用互斥量来保证对任意共享资源的原子访问,而保护共享变量是其常见的用法。

没有锁的资源争夺情景如下:

void *thread_fun1(void *arg){   //读全局变量val
for(int i = 0;i<10;i++){
printf("read thread val=%d\n",val);
}
return NULL;
}

void *thread_fun2(void *arg){  //写全局变量val
for(int i = 0;i <10;i++){
val++;
printf("write  thread doing val++ \n");
}
return NULL;
}

int main(void){
pthread_t tid1,tid2;
int ret;

ret = pthread_create(&tid1,NULL,thread_fun1,NULL);    //创建线程1
if(ret != 0){
printf("create failed\n");
exit(1);
}

ret = pthread_create(&tid2,NULL,thread_fun2,NULL);    //创建线程2
if(ret != 0){
printf("create failed\n");
exit(1);
}

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

return 0;
}


从图中看出,在读数据时,数据还被写了。

2.1锁的两种状态

互斥锁有两种状态:已锁定(locked)和未锁定(unlocked),任何时候,只有一个线程可以获得锁。可以换个角度看,锁在任何时候只有0,1两个状态的一种。

锁的初始状态为1,当有线程执行lock动作后,lock相当于执行减一操作,锁的状态变成0;

当有线程执行unlock动作后,unlock相当于执行加一操作,锁的状态右0变成1。

当一个线程A获得资源的锁之后,在没有解锁之前,另一个线程B想要操作同一资源时,会发生阻塞,直到线程A解锁。

2.2使用互斥锁的一般步骤

第一步:pthread_mutex_t mute 创建互斥锁

第二步:pthread_mutex_init; 初始化互斥锁

第三步:pthread_mutex_lock; 对资源加锁

第四步:操作资源

第五步:pthrad_mutext_unlock();解锁

第六步:pthead_mutex_destroy;销毁锁

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<string.h>

int val = 0;   //全局变量
pthread_mutex_t mutex;  //第一步: 创建互斥锁

void *thread_fun1(void *arg){   //读全局变量val
for(int i = 0;i<10;i++){
pthread_mutex_lock(&mutex);   //第三步:锁;
printf("read thread val=%d\n",val); //第四部:操作
pthread_mutex_unlock(&mutex); //第五步:解锁
}
return NULL;
}

void *thread_fun2(void *arg){
for(int i = 0;i <10;i++){
pthread_mutex_lock(&mutex)  ; //锁;
val++;
printf("write  thread doing val++ \n");
pthread_mutex_unlock(&mutex); //解锁
}
return NULL;
}

int main(void){
pthread_t tid1,tid2;
int ret;

pthread_mutex_init(&mutex,NULL);   //第二步:初始化互斥锁

ret = pthread_create(&tid1,NULL,thread_fun1,NULL);    //创建线程1
if(ret != 0){
printf("create failed\n");
exit(1);
}

ret = pthread_create(&tid2,NULL,thread_fun2,NULL);    //创建线程2
if(ret != 0){
printf("create failed\n");
exit(1);
}

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

pthread_mutex_destroy(&mutex);    //第六步:销毁互斥锁

return 0;
}


结果:读写数据不会混乱

2.3互斥锁死锁

互斥锁能够有效的解决线程间资源抢夺的问题。但是,互斥锁使用的不好的时候,就会出现死锁。现象表现为互斥锁一致被占有,线程迟迟不能拿到锁,而一致阻塞中。

互斥锁死锁一般分为两种:

(1)线程自己将自己锁死

(2)多线程抢占锁资源被困

2.3.1 线程自己将自己锁死

同一个线程先后调用同一个pthread_lock()两次。由于第一次加锁后,锁一直处于0状态(被占用),当程序往下执行到第二次加锁时,就会阻塞在第二次加锁处,且不能解锁,就会一直处于阻塞状态。

int val = 0;   //全局变量
pthread_mutex_t mutex;  //互斥锁

void *thread_fun1(void *arg){   //读全局变量val

pthread_mutex_lock(&mutex);   //锁
printf("read thread val=%d\n",val);
pthread_mutex_lock(&mutex);   //锁
val++;
printf("read thread val=%d\n",val);

pthread_mutex_unlock(&mutex); //解锁
pthread_mutex_unlock(&mutex); //解锁
return NULL;
}

int main(void){
pthread_t tid;
int ret;

pthread_mutex_init(&mutex,NULL);   //初始化互斥锁

ret = pthread_create(&tid,NULL,thread_fun1,NULL);    //创建线程1
if(ret != 0){
printf("create failed\n");
exit(1);
}

pthread_join(tid,NULL);
pthread_mutex_destroy(&mutex);    //销毁互斥锁
return 0;
}


结果:程序锁死

2.3.2 多线程抢占锁资源被困

线程A获取资源必须要得到锁1和锁2,同样线程B获取资源也必须要得到锁1和锁2。当线程A获得锁1,程B获得锁2时,由于线程A得不到锁2会阻塞,线程B得不到锁1也会阻塞。此时两个线程都处于阻塞状态。

int val = 0;   //全局变量
pthread_mutex_t mutex1,mutex2;  //互斥锁

void *thread_fun1(void *arg){   //读全局变量val
printf("thread_fun1 running!!!\n");

pthread_mutex_lock(&mutex1);   //锁1;
sleep(1);
pthread_mutex_lock(&mutex2);   //锁2;
printf("read thread val=%d\n",val);

pthread_mutex_unlock(&mutex2); //解锁
pthread_mutex_unlock(&mutex1); //解锁
return NULL;
}

void *thread_fun2(void *arg){  //写全局变量
printf("thread_fun2 running!!!\n");

pthread_mutex_lock(&mutex2);   //锁2;
pthread_mutex_lock(&mutex1) ;  //锁1;
val++;
printf("write  thread doing\n");
pthread_mutex_unlock(&mutex1); //解锁
pthread_mutex_unlock(&mutex2); //解锁
return NULL;
}

int main(void){
pthread_t tid1,tid2;
int ret;

pthread_mutex_init(&mutex1,NULL);   //初始化互斥锁
pthread_mutex_init(&mutex2,NULL);   //初始化互斥锁

ret = pthread_create(&tid1,NULL,thread_fun1,NULL);    //创建线程1
if(ret != 0){
printf("create failed\n");
exit(1);
}

ret = pthread_create(&tid2,NULL,thread_fun2,NULL);    //创建线程2
if(ret != 0){
printf("create failed\n");
exit(1);
}

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

pthread_mutex_destroy(&mutex1);    //销毁互斥锁
pthread_mutex_destroy(&mutex2);    //销毁互斥锁

return 0;
}

2.3.3避免死锁的方法

1.保证资源的获取顺序,要求每个线程获取资源的顺序一致;

2.当得不到所需资源时,放弃已获得的资源,等待。

2.4读写锁

锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。

读共享,写独占。

写锁优先级高。

相较于互斥量而言,当读线程多的时候,提高访问效率

相关API

pthread_rwlock_t  rwlock;

pthread_rwlock_init(&rwlock, NULL);

pthread_rwlock_rdlock(&rwlock);		try

pthread_rwlock_wrlock(&rwlock);		try

pthread_rwlock_unlock(&rwlock);

pthread_rwlock_destroy(&rwlock);

三、同步方法:条件变量

互斥锁防止多个线程同时访问同一共享资源。条件变量允许一个线程就某共享资源的状态变化通知其他线程,并让其他线程等待(阻塞)这一通知。条件变量通常结合互斥锁来使用。条件变量典型的场景时生产-消费模型,线程池等。

3.1条件变量要解决的问题

一个未使用条件变量的简单例子有助于理解条件变量的重要性。

int val = 0; //全局变量

//生产函数
void* thread_producer(void* argc){
pthread_mutex_lock(&mutex);   //已经定义且初始化好的互斥锁
val++;   //生产
pthread_mutex_unlock(&mutex);
}

//主函数消费函数
while(1){
pthread_mutex_lock(&mutex);
while(val > 0 ){
val--;
}
pthread_mutex_unlock(&mutex);
}

上诉代码虽然可行,但是由于主线程不停地循环检查val变量,故造成CPU资源的浪费。条件变量就可以解决这一问题:允许一个线程休眠(等待)直至接获另一线程的通知(收到信号)去执行某些操作。当val没有生产时,主线程休眠不去查询,当val生产了,就通知给主线程。

3.2相关函数

初始化条件变量:

pthread_cond_init(&cond, NULL);   			动态初始化。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;	静态初始化。

阻塞等待条件:

pthread_cond_wait(&cond, &mutex);

作用:

1. 阻塞等待条件变量满足

2. 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex))

3.  当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)

唤醒阻塞线程

pthread_cond_signal(pthread_cond_t*cond); 唤醒阻塞在条件变量上的一个线程。
pthread_cond_broadcast(pthread_cond_t*cond); 唤醒阻塞在条件变量上的 所有线程。

3.3生产消费模型

模拟一个生产消费模型:当生产者没有生产数据给数据区时,消费者拿不到数据,一直阻塞;当生产者生产数据给数据区时,发送一个信号,消费者收到,开始从数据区消费数据。

程序如下:

struct data{   //数据
int val;
struct data *next;
};

struct data *head;   //定义头

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  //静态初始化互斥锁
pthread_cond_t product_con = PTHREAD_COND_INITIALIZER;    //静态初始化条件变量

void *producer_fun(void *p){   //生产
struct data *pNode;
while(1){
//生产一个数据
pNode = (struct data*)malloc(sizeof(struct data));
pNode->val = rand() % 100 +1;
printf("produce ----------%d\n\n",pNode->val);

pthread_mutex_lock(&mutex);   //锁

//数据放入数据区
pNode->next = head;
head = pNode;

pthread_mutex_unlock(&mutex);

pthread_cond_signal(&product_con);  //唤醒等待线程

sleep(rand() % 5);

}
return NULL;
}

void *consumer_fun(void *p){  //消费
struct data *cNode;
while(1){
pthread_mutex_lock(&mutex);   //锁
while(head == NULL) {
pthread_cond_wait(&product_con,&mutex); //阻塞
}

//消费
cNode = head;
head = cNode->next;

pthread_mutex_unlock(&mutex);
printf("consume ----------%d\n\n",cNode->val);
free(cNode);

sleep(rand() % 5);
}
return NULL;
}

int main(int argc,char *argv[]){
pthread_t cid,pid;
int ret;

srand(time(NULL));

ret = pthread_create(&cid,NULL,consumer_fun,NULL);
if(ret != 0){
printf("create c failed\n");
exit(1);
}

ret = pthread_create(&pid,NULL,producer_fun,NULL);
if(ret != 0){
printf("create p failed\n");
exit(1);
}

pthread_join(cid,NULL);
pthread_join(pid,NULL);

return 0;
}

四、信号量

信号量应用于线程、进程间同步。互斥量只有2值,分别为0,1;信号量相当于初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。

4.1相关函数

sem_t sem;	定义类型。

int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem: 信号量
pshared:  0:用于线程间同步;1:用于进程间同步
value:N值。(指定同时访问的线程数)

sem_destroy();   //销毁

sem_wait();		一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。

sem_post();		一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。

信号量的工作可以理解为停车场场景:

在一个停车场里,总共有N个停车位,已经使用m个;

每当进行sem_post()操作就相当于,有一辆车开出去,m的数量就会加一,但空余车位不会大于N;

每当进行sem_wait()操作就相当于,有一辆车开进去,m的数量就会减一,空余车位为0,不能再操作。

4.2生产消费模型

#define NUM 5

int queue[NUM];                                     //全局数组实现环形队列
sem_t blank_number, product_number;                 //空格子信号量, 产品信号量

void *producer(void *arg)
{
int i = 0;

while (1) {
sem_wait(&blank_number);                    //生产者将空格子数--,为0则阻塞等待
queue[i] = rand() % 1000 + 1;               //生产一个产品
printf("----Produce---%d\n", queue[i]);
sem_post(&product_number);                  //将产品数++

i = (i+1) % NUM;                            //借助下标实现环形
sleep(rand()%1);
}
}

void *consumer(void *arg)
{
int i = 0;

while (1) {
sem_wait(&product_number);                  //消费者将产品数--,为0则阻塞等待
printf("-Consume---%d\n", queue[i]);
queue[i] = 0;                               //消费一个产品
sem_post(&blank_number);                    //消费掉以后,将空格子数++

i = (i+1) % NUM;
sleep(rand()%3);
}
}

int main(int argc, char *argv[])
{
pthread_t pid, cid;

sem_init(&blank_number, 0, NUM);                //初始化空格子信号量为5, 线程间共享 -- 0
sem_init(&product_number, 0, 0);                //产品数为0

pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);

pthread_join(pid, NULL);
pthread_join(cid, NULL);

sem_destroy(&blank_number);
sem_destroy(&product_number);

return 0;
}


注:此段代码为黑马培训视频内容。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: