多线程同步——学习笔记

来自“小林coding的图解系统”

多线程同步

我们知道同一个进程中的线程可以共享地址空间,也即:线程可以共享代码段,数据段,堆空间和打开的文件资源等。当然每个线程也都有自己独立的栈和寄存器空间。

当多个线程竞争共享空间时,如果不进行一定的控制,会发生什么呢?
会造成共享数据的混乱。

举例:

#include<iostream>
using namespace std;
#include<thread>

int i = 0;   //共享数据

//线程函数:对共享变量i自增1执行10000次
void test() {
	int num = 10000;
	for (int n = 0; n < num; n++) {
		i = i + 1;
	}
}

int main() {
	cout << "start all threads." << endl;
	//创建线程
	thread thread_test1(test);
	thread thread_test2(test);

	//等待线程执行完成
	thread_test1.join();
	thread_test2.join();

	cout << "All thread joined." << endl;
	cout << "i = " << i << endl;
	return 0;
}

按理说结果应该是20000,但是在Linux下运行的结果小于20000;

原因就是:
当想要给 i + 1 的时候,汇编指令执行的过程需要有三步:

如果此时从内存获取的i = 50,线程1在第二步执行完成的时候(此时i = 51,但是还未放回内存中去),突然发生了时钟中断,开始执行线程2,同样先从内存获取i = 50,假设线程2将三步都执行完成了(i = 51并放回内存中去),然后又发生上下文切换,回到线程1,然后接着线程1中的第二步继续执行下一步,将i = 51放回内存中,此时全局变量i再次被设置为51

概念

互斥的概念

:当多线程相互竞争操作共享变量的时候,由于运气不好,发生了上下文切换,从而得到了错误的结果。


互斥:保证一个线程在临界区执行时,其他线程应该被阻止进入临界区。
临界区:访问共享资源的代码段(例如:上例的test函数),不能给多线程同时执行。

同步的概念

:多线程的执行时无顺序,各自独立的,以不可预知的速度向前推进,但是有时我们期望线程之间相互合作,能共同完成一个任务。

例如:

好比:操作A应在操作B之前执行,操作C必须在操作A和操作B都完成之后才能执行。

:并发的线程在一些关键点上可能需要互相等待互通消息,这种相互制约的等待与互通信息称为进程/线程同步。

互斥与同步的实现和使用

实现方法:有两种

  1. :加锁、解锁操作
  2. 信号量:P、V操作

用锁实现互斥

任何想进入临界区的线程,必须先加锁,若加锁顺利方可进入临界区,完成临界区后解锁。

根据锁的实现方式可以分为两种:忙等待锁无忙等待锁

忙等待锁

使用的前提:需要用到原子操作指令——测试和置位指令(Test-And-Set)

Test-And-Set的代码实现:

int TestAndSet(int *old_ptr, int new_i) {
	int old = *old_ptr;
	*old_ptr = new_i;
	return old;
}

实现逻辑:把old_ptr更新为new_i, 返回old_ptr的旧值。
注意上述代码是原子执行(要么全部执行,要么都不执行)。

忙等待的实现:利用Test-And-Set来实现。

typedef struct lock_t{
	int flag;
} lock_t;

void init(lock_t *lock){      //初始化
	lock->flag = 0;
}

void lock(lock_t *lock){      //加锁
	while(TestAndSet(&lock->flag, 1) == 1);
	//do somthing;
}

void unlock(lock_t *lock){    //解锁
	lock->flag = 0;
}

实现过程:
在这里插入图片描述
忙等待锁也被称为自旋锁
在单处理器上需要抢占式的调度器。

无等待锁

无等待锁就是当前线程没获得锁的时候,不用自旋,将当前线程放到锁的等待队列中,将CPU让给其他线程使用。

无等待锁的实现

type struct lock_t{
	int flag;
	queue_t *q; //等待队列
}lock_t;

void init(lock_t *lock){
	lock->flag =0;
	queue_init(lock->q);
}

void lock(lock_t *lock){
	while(TestAndSet(&lock->flag, 1) == 1){
		1. 保存现在运行线程的TCB;
		2. 将现在运行的线程TCB插入到等待队列;
		3. 设置该程为等待状态;
		4. 调度程序
	}
}

void unlock(lock_t *lock){
	if(lock->q != NULL){
		1. 移出等待队列的队头元素;
		2. 将该线程的TCB插入到就绪队列;
		3. 设置该线程为就绪状态;
	}
	lock->flag =0;
}

信号量

通常信号量表示资源的数量。对应的变量是一个整型(sem)变量。

采用两个原子操作的系统调用函数来控制信号量。

  1. P操作:将sem减1,sem<0 ? 线程进入阻塞等待 : 继续
  2. V操作:将sem加1,sem<=0 ? 唤醒一个等待中的线程 : 继续

P操作是用在进入临界区之前,V操作是用在离开临界区之后,这两个操作是必须成对出现的。

PV操作的实现
type struct sem_t{
	int sem;    //资源个数
	queue_t *q; //等待队列
}sem_t;

void init(sem_t *s, int sem){     //初始化信号量
	s->sem = sem;
	queue_init(s->q);
}

//P操作
void P(sem_t *s){
	S->sem--;
	if(s->sem < 0){
		1. 保留调用线程CPU现场;
		2. 将该线程的TCB插入到s的等待队列;
		3. 设置该线程为等待状态;
		4. 执行调度程序;
	}
}

//V操作
void V(sem_t *s){
	s->sem++;
	if(s->sem <= 0){
		1. 移出s等待队列首元素;
		2. 将该线程的TCB插入就绪队列;
		3. 设置该线程为「就绪」状态;
	}
}

有点和无忙等待类似,都是采用等待队列的方式。

PV操作的函数是由操作系统管理和实现的,所以操作系统已经使得执行PV函数时是具有原子性的。

PV操作的使用

信号量实现临界区的互斥访问
将每类的共享资源的信号量sem设初值为1,表示该临界区未被占用。

信号量实现事件同步
设置信号量sem,初值设为0.

代码实现同步的例子:

semaphore s1 = 0; //表示不需要吃饭
semaphore s2 = 0; //表示饭还没做完

//儿子线程函数
void son(){
	while(TRUE){
		肚子饿;
		V(s1); //叫妈妈做饭
		P(s2); //等待妈妈做完饭
		吃饭;
	}
}

//妈妈线程函数
void mon(){
	while(TRUE){
		P(s1);//询间需不需要做饭
		做饭;
		V(s2); //做完饭,通知儿子吃饭
	}
}

实现说明:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值