并发控制:线程的同步与互斥


学习视频: 并发控制 [南京大学2022操作系统]
优质文章: 多线程的同步与互斥(互斥锁、条件变量、读写锁、自旋锁、信号量)_青萍之末的博客-CSDN博客线程的同步与互斥_线程互斥_小小酥诶的博客-CSDN博客

一、线程互斥

1.线程互斥产生的原因

多线程对临界资源(多线程执行流共享访问的资源)访问时很可能会产生二义性。由于某些操作并非原子性的(也就是反映到汇编语言时,它是多行代码),在该操作还未执行完成时,发生了线程的上下文切换,导致结果与预期不符。

例如:现在有一个共享变量sum=0。设置三个线程,让它们共同执行sum++,知道sum=1000为止

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#define N 1000
using namespace std; 

long sum = 0;
void* Tsum(void* arg)
{
    while(1)
    	if(sum<N)
    	{
    		usleep(10000);
        	sum++;
        	cout << "我是线程[" << pthread_self() << "], " <<"sum = "<<sum<<endl;
		}
		else 
		{
			break;
		}
    }
    return NULL;
}

int main()
{
    pthread_t thread1;
    pthread_t thread2;
    pthread_t thread3;
    
    pthread_create(&thread1, NULL, Tsum, NULL);
    pthread_create(&thread2, NULL, Tsum, NULL);
    pthread_create(&thread3, NULL, Tsum, NULL);
    
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    
    cout<<"结果:"<<"sum = "<<sum<<endl;
    return 0;
}

运行结果:
在这里插入图片描述
预期结果的sum应该等于1000,但实际的sum等于1002,这是为什么呢?
让我们观察一下临界区

    	if(sum<N)
    	{
    		usleep(10000);
        	sum++;
        	cout << "我是线程[" << pthread_self() << "], " <<"sum = "<<sum<<endl;
		}
  • 首先,usleep()的存在就是巨大的问题,它是一个漫长的业务。观察上面的运行结果,原本最后一次循环只有线程[4]能进入到if内部,但由于它陷入睡眠,还没来得及对sum进行自加操作时,线程[3][2]看到sum还等于999,于是就趁机进入了if内部,最终导致了sum多加了两次

你可以想象线程就是一个在闭着眼睛做事的人,判断一下(if),闭眼做事,再判断一下(if),再闭眼做事。。。所以,在他闭眼做事的过程中,原本的东西被偷换了都不知道。。。

2、线程互斥的解决办法——加锁

想要让这个闭着眼睛做事的人不被人偷换东西,你必须命令他进入房间做事情(判断if)之前,要先把桌上的钥匙拿在手里,然后把自己锁进房间做事情。其他没有拿到钥匙,但也想进这个房间的人必须在房间外等待。。。
(这边的房间就等同于临界资源。也就是这个例子中的sum)在这里插入图片描述

①自旋锁的代码实现

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#define N 1000
using namespace std;
typedef struct 
{
    int lock;
} spinlock_t;

void spinlock_init(spinlock_t* lock) 
{
    lock->lock = 0;
}

void spinlock_lock(spinlock_t* lock) 
{
    while (__sync_lock_test_and_set(&lock->lock, 1)) 
	{
        // 自旋等待锁释放
    }
}

void spinlock_unlock(spinlock_t* lock) 
{
    __sync_lock_release(&lock->lock);
}

spinlock_t lock;
long sum = 0;

void* Tsum(void* arg) {
    while(1)
    {
    	spinlock_lock(&lock);
    	if(sum<N)
    	{
    		usleep(10000);
    	    sum++;
    	    cout << "我是线程[" << pthread_self() << "], " <<"sum = "<<sum<<endl;
        	spinlock_unlock(&lock);
		}
		else
		{
			spinlock_unlock(&lock);
			break;
		}
    }
    return NULL;
}
int main() {
	pthread_t thread1;
    pthread_t thread2;
    pthread_t thread3;
	spinlock_init(&lock);
    
    pthread_create(&thread1, NULL, Tsum, NULL);
    pthread_create(&thread2, NULL, Tsum, NULL);
    pthread_create(&thread3, NULL, Tsum, NULL);
    
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);

    cout<<"结果:"<<"sum = "<<sum<<endl;
    return 0;
}

运行结果:Bingo!!!
在这里插入图片描述

但细心的你一定会发现自旋锁的弊端,可以想象房间外等待的人不停焦虑地去看桌子上的锁被放回了没有,急得原地旋转(while),他们很忙,但确实啥事没成。直到房间里的人出来,把锁放回桌上,在外等待的一个“幸运儿”才能顺利拿到钥匙,进入房间,其他人继续焦头烂额地等待。。。这似乎很占cpu

②互斥锁的代码实现

与其让其他人在门外焦急地等待,不如让他休息一下,或者让他去做其他事情。设置一个管理员(操作系统),来管理这项事情。

但“让”显然不是C能够做到的,所以这里用库里的锁实现

#include <pthread.h>
 pthread_mutex_t name//互斥量
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex, 
						const pthread_mutexattr_t *attr);

// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex)

// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#define N 1000
using namespace std; 

long sum = 0;
pthread_mutex_t mutex;
void* Tsum(void* arg)
{
    while(1)
    {
    	pthread_mutex_lock(&mutex);//加锁,如果互斥锁已经被其他线程占用,则当前线程会被阻塞,直到互斥锁可用
    	if(sum<N)
    	{
    		usleep(10000);
        	sum++;
        	cout << "我是线程[" << pthread_self() << "], " <<"sum = "<<sum<<endl;
        	pthread_mutex_unlock(&mutex);//解锁,允许其他线程获取互斥锁。
		}
		else 
		{
			pthread_mutex_unlock(&mutex);//解锁
			break;
		}
    }
    return NULL;
}

int main()
{
    pthread_t thread1;
    pthread_t thread2;
    pthread_t thread3;
    pthread_mutex_init(&mutex, NULL);//初始化锁
    
    pthread_create(&thread1, NULL, Tsum, NULL);
    pthread_create(&thread2, NULL, Tsum, NULL);
    pthread_create(&thread3, NULL, Tsum, NULL);
    
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    
    pthread_mutex_destroy(&mutex);//释放锁
    
    cout<<"结果:"<<"sum = "<<sum<<endl;
    return 0;
}

3、总结

  • 自旋锁是一种忙等待的锁,线程在尝试获取锁时会不断地循环检查锁的状态,直到成功获取锁为止。自旋锁适用于锁的持有时间很短的情况,因为它不会使线程进入阻塞状态,减少了线程切换的开销。但是,如果锁的持有时间较长或竞争激烈,自旋锁可能会导致线程占用过多的CPU资源。
    好处:lock成功,立即进入临界区,开销小
    坏处:lock失败,浪费cpu自旋等待
  • 互斥锁是一种阻塞锁,线程在尝试获取锁时如果锁已经被其他线程占用,则会被阻塞,直到锁被释放后才能继续执行。互斥锁使用操作系统的原语来实现线程的阻塞和唤醒,确保了线程之间的同步和互斥。互斥锁适用于锁的持有时间较长或竞争不激烈的情况,因为它会主动释放CPU资源给其他线程使用,避免了忙等待的问题
    好处:lock失败,不占用cpu
    坏处:lock成功,需要进出内核

二、线程同步

1、线程同步的概念

同步并非同时,它指的是线程之间相互依赖的关系,简单来说就是A 任务的运行依赖于 B 任务产生的数据,它侧重指的是在某个时间点线程间达到相互已知的状态。而之前提到的线程互斥是相互排斥临界资源的关系,要区别一下两个概念

2、引入→条件变量

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用

#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond,
						pthread_condattr_t *cond_attr);

// 阻塞等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

// 超时等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,
						const timespec *abstime);

// 解除所有线程的阻塞
int pthread_cond_destroy(pthread_cond_t *cond);

// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);  

现在,你肯定对这个概念一知半解,所以我们来举个栗子吧~

2、经典问题:生产者-消费者

为了方便表示,把左括号“(”表示生产资源/任务,放入队列;把右括号“)”表示消费者从队列中取出资源/任务。这个队列的深度我们把它设置成3

#include<stdio.h>
#include<pthread.h>
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥所
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量

int n=3;//队列深度
int count=0;//队列里剩余的任务个数[0,3]

//生产者线程
void Tproduce()
{
	while(1)
	{
		pthread_mutex_lock(&mutex);//加锁
		while(!(count!=n))
		{
			pthread_cond_wait(&cond,&mutex);//等待条件变量,该函数会释放互斥锁并将当前线程置于睡眠状态,直到其他线程通过pthread_cond_broadcast或pthread_cond_signal函数唤醒该线程
		}
		printf("(");count++;//打印"("符号,表示生产一个元素
		pthread_cond_broadcast(&cond);//发送条件变量的广播通知,唤醒所有等待该条件变量的线程
		pthread_mutex_unlock(&mutex);//解锁
	}
}

//消费者线程
void Tconsume()
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		while(!(count!=0))
		{
			pthread_cond_wait(&cond,&mutex);
		}
		printf(")");count--;
		pthread_cond_broadcast(&cond);
		pthread_mutex_unlock(&mutex);
	}
}

int main()
{
	pthread_t producers[8];
    pthread_t consumers[8]; 
    for(int i = 0; i < 8; i++)
    {
        pthread_create(producers + i, NULL, (void* (*)(void*))Tproduce, NULL);
        pthread_create(consumers + i, NULL, (void* (*)(void*))Tconsume, NULL);
    }
	return 0;
}

运行结果:
在这里插入图片描述
目测是挺对的,可以通过输出重定向,来检查其正确性,这里就不展示了

  • 这里为什么会用while(!(count!=n))和while(!(count!=0)),而不是if(!(count!=n))和if(!(count!=n))呢?
    答:
    1、在条件变量中使用while循环是为了防止虚假唤醒(spurious wakeup)的情况发生。虚假唤醒是指当线程被唤醒时,条件实际上可能并不满足,因此线程应该继续等待。
    2、就这个栗子而言,一个生产者在count++后,使得count已经等于3了,之后执行pthread_cond_broadcast(&cond),对这个生产者而言,它做这个函数的目的就是要唤醒在等待这个条件变量的消费者,但这个时候,生产者也是会被唤醒的,如果用if,被唤醒的这个生产者就会直接从if中跳出,不会再次检查还需不需要生产任务,直接开始生产任务,导致count>3,出现错误。所以,为了避免这种情况,我们while让被唤醒的生产者再次检查一下count满了没,确保正确性。
    3、虚假唤醒可能发生在多线程环境中,尤其是在多核处理器上。操作系统可能会因为各种原因(例如中断、资源调度等)导致线程被意外地唤醒,即使条件并没有满足。这可能导致线程在条件不满足的情况下继续执行,从而导致错误的结果。为了避免虚假唤醒,我们使用while循环来检查条件是否满足。当线程被唤醒时,它会重新检查条件,如果条件仍然不满足,则继续等待。这样可以确保在条件满足之前,线程不会被错误地唤醒。

消费者——生产者问题的万能模板:

		pthread_mutex_lock(&mutex);
		while(!(condition))
		{
			pthread_cond_wait(&cond,&mutex);
		}
		//可以保证while出来后,condition一定是成立的
		/*
		一顿操作。。。。
		*/
		pthread_cond_broadcast(&cond);pthread_cond_signal(&cond);
		pthread_mutex_unlock(&mutex);

4、例题

  • 这个程序无法运行或是会导致运行的结果不符合预期
  • 利用学习的并发编程知识修改这段程序,使它能够正常运行
#include <pthread.h>
#include <stdio.h>
char buffer[1024];

int write_idx = 0;
int read_idx = 0;
FILE* fp;
void producer() 
{
	for (int i = 0; i < 2048; i ++) 
	{
		buffer[(write_idx ++) % 1024] = i % 26 + 'a';
	}
}
void consumer() {
	for (int i = 0; i < 2048; i++) 
	{
		putc(buffer[(read_idx ++) % 1024], fp);
	}
}
void check(FILE* fp)
{
	for (int i = 0; i < 2048; i ++) 
	{
		char c = fgetc(fp);
		char another = i % 26 + 'a';
		if (c != another) 
		{
			printf("at line %d, expect %c, get %c", i, another, c);
		}
	}
	printf("Finish, no Error\n");
}
int main() {
	fp = fopen("tmp.txt", "w+");
	pthread_t producer_thread;
	pthread_t consumer_thread;
	pthread_create(&producer_thread, NULL, (void* (*)(void*))producer, NULL);
	pthread_create(&consumer_thread, NULL, (void* (*)(void*))consumer, NULL);
	
	pthread_join(producer_thread, NULL);
	pthread_join(consumer_thread, NULL);
	fclose(fp);
	fp = fopen("tmp.txt", "r");
	check(fp);
	return 0;
}

错误输出:
在这里插入图片描述
错误的tmp.txt:
在这里插入图片描述
解答:

#include <pthread.h>
#include <stdio.h>
char buffer[1024];

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int write_idx = 0;
int read_idx = 0;
FILE* fp;
void producer() 
{
	for (int i = 0; i < 2048; i ++)
	{
		pthread_mutex_lock(&mutex);
		while(!(write_idx>=read_idx))
		{
			pthread_cond_wait(&cond,&mutex);
		}
		buffer[(write_idx++) % 1024] = i % 26 + 'a';
		pthread_cond_broadcast(&cond);
		pthread_mutex_unlock(&mutex);
	}
}
void consumer() {
	for (int i = 0; i < 2048; i++) 
	{
		pthread_mutex_lock(&mutex);
		while(!(write_idx>read_idx))
		{
			pthread_cond_wait(&cond,&mutex);
		}
		putc(buffer[(read_idx++) % 1024], fp);
		pthread_cond_broadcast(&cond);
		pthread_mutex_unlock(&mutex);
	}
}
void check(FILE* fp)
{
	for (int i = 0; i < 2048; i ++) 
	{
		char c = fgetc(fp);
		char another = i % 26 + 'a';
		if (c != another) 
		{
			printf("at line %d, expect %c, get %c", i, another, c);
		}
	}
	printf("Finish, no Error\n");
}
int main() {
	fp = fopen("tmp.txt", "w+");
	pthread_t producer_thread;
	pthread_t consumer_thread;
	pthread_create(&producer_thread, NULL, (void* (*)(void*))producer, NULL);
	pthread_create(&consumer_thread, NULL, (void* (*)(void*))consumer, NULL);
	
	pthread_join(producer_thread, NULL);
	pthread_join(consumer_thread, NULL);
	fclose(fp);
	fp = fopen("tmp.txt", "r");
	check(fp);
	
	pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
	return 0;
}

正确输出:
在这里插入图片描述
正确的tmp.txt:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值