锁与原子操作CAS的原理

一、互斥锁mutex

锁的概念
对一块临界资源加锁,临界资源在使用中只能被一个线程使用,例如厕所的坑位,只能一个人去蹲

//创建10个线程
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define THREAD_COUNT 10

void *func(void *args) 
{
	int *pcount = (int*)args;
	int i=0;
	while(i++<100000){
      (*pcount)++;//10个线程共用的资源
      usleep(1);
	}
}
int main(){
     pthread_t thid[10]={0};
     int count = 0;
     for (int i = 0;i < THREAD_COUNT; i++) {
         pthread_create(&thid[i], null, func, &count);
         /*第一个参数是线程的id,第二个是线程的属性,第三个是入口函数,第四个是传到子线程的参数*/
     }
     for(int i=0; i < 100; i++){//每隔一秒打印一次,最后理想结果是100万
         printf("count -->%d\n",count);
        sleep(1);
     }
     return 0;
}

我们来运行一下,十个线程共有count这个资源同时加一的过程,发觉最终的值小于100万,为什么会出现这种事呢
在这里插入图片描述
我们来看看自加一的操作在汇编当中的实现,先在内存中把idx move到eax寄存器里边,再通过寄存器进行一个自增,再把idx写回到原来的内存中
在这里插入图片描述
问题出现在哪呢?比如说在下面这张图里最开始idx=20,先是线程1切换时保存了寄存器的状态,然后从线程2切换回来时,线程1里边的值还是20,线程1再执行+1操作,因此在线程1和2中都执行了+1,但是效果只是执行了一次+1,所以总共执行100万次的+1会导致最后的值小于100万

在这里插入图片描述
我们该怎么改呢,我们试试加锁,只允许一个线程运行
我们用互斥锁

//创建10个线程
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define THREAD_COUNT 10
pthread_mutex_t mutex;
void *func(void *args) 
{
	int *pcount = (int*)args;
	int i=0;
	//外边加锁的话,其中usleep是没有必要,一定要对需要加锁的临界资源加锁,非临界资源就不用加了
	while(i++<100000){
		 pthread_mutex_lock(&mutex);
     (*pcount)++;
     pthread_mutex_unlock(&mutex);
     usleep(1);
	}
}
int main(){
     pthread_t thid[10]={0};
     int count = 0;
     pthread_mutex_init(&mutex, NULL);
     for (int i = 0;i < THREAD_COUNT; i++) {
         pthread_create(&thid[i], NULL, func, &count);
     }
     for(int i=0; i < 100; i++){//每隔一秒打印一次,最后理想结果是100万
         printf("count -->%d\n",count);
        sleep(1);
     }
     return 0;
}

实现效果
在这里插入图片描述

二、自旋锁

我们来看看自旋锁,自旋锁的使用和互斥锁是一样的

//创建10个线程
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define THREAD_COUNT 10
//pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
void *func(void *args) 
{
	int *pcount = (int*)args;
	int i=0;
	//外边加锁的话,其中usleep是没有必要,一定要对需要加锁的临界资源加锁,非临界资源就不用加了
	while(i++<100000){
		 //pthread_mutex_lock(&mutex);
		 pthread_spin_lock(&spinlock);
     (*pcount)++;
     //pthread_mutex_unlock(&mutex);
     pthread_spin_unlock(&spinlock);
     usleep(1);
	}
}
int main(){
     pthread_t thid[10]={0};
     int count = 0;
     pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
     for (int i = 0;i < THREAD_COUNT; i++) {
         pthread_create(&thid[i], NULL, func, &count);
     }
     for(int i=0; i < 100; i++){//每隔一秒打印一次,最后理想结果是100万
         printf("count -->%d\n",count);
        sleep(1);
     }
     return 0;
}

区别在哪呢
mutex互斥锁当遇到锁被占用时,会引起线程切换,使用场景在于操作复杂,有系统调用
spinlock自旋锁当遇到锁被占用时会一直在锁这里停留,直到锁被让出,,使用场景在于临界资源操作简单,没有系统调用
如何去判断复杂和简单呢,这里的复杂指的是比线程切换还重的复杂

三.原子操作

cpu执行的最小单元是指令,那我们有没有办法把这三条指令变成一条指令执行呢,这里我们引入原子操作。
在这里插入图片描述

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define THREAD_COUNT 10
//pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
int inc(int *value, int add) {
	int old;
	__asm__ volatile(
		"lock; xaddl %2, %1;"|
		:"=a" (old)
		:"m" (*value), "a" (add)
		: "cc", "memory"
	);
	return old;
}
void *func(void *args) 
{
	int *pcount = (int*)args;
	int i=0;
	while(i++<100000){
		//(*pcount)++;
     inc(pcount,1);
     usleep(1);
	}
}
int main(){
     pthread_t thid[10]={0};
     int count = 0;
     pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
     for (int i = 0;i < THREAD_COUNT; i++) {
         pthread_create(&thid[i], NULL, func, &count);
     }
     for(int i=0; i < 100; i++){//每隔一秒打印一次,最后理想结果是100万
         printf("count -->%d\n",count);
        sleep(1);
     }
     return 0;
}

原子操作核心需要cpu指令集支持才可以,原子操作只能在操作比较简单的情况下运用
CAS是原子操作的一种,扩写为compare and swap,
例如:cmppxchg(a,b,c);—>if(a==b) a=c;这就是CAS的一种

四、线程私有空间

解决函数内部调用时,在线程当中使用次数较多,且不能当作函数的参数传进去时,我们就需要线程的私有空间

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define THREAD_COUNT 3
pthread_key_t key;
typedef void *(*thread_cb)(void*);
void print_tread1_key(void) {
   int *p = (int*)pthread_getspecific(key);//我们利用的key来获取这个值
   printf("thread 1:%d\n", *p);
}
void print_tread2_key(void) {
   int *ptr = (char*)pthread_getspecific(key);
   printf("thread 2:%d\n", ptr);
}
void print_tread3_key(void) {
   struct pair *p =(struct pair*)pthread_getspecific(key);
   printf("thread 2:%d, %d\n", p->x, p->y);
}
void *thread1_proc(void *args) {
	int i=5;
	pthread_setspecific(key, &i);//私有空间设在key这里
	print_tread1_key();//我们并没有把key当做参数传进去,我们线程的私有空间就是key
}
void *thread2_proc(void *args) {
	char *ptr = "thread2_proc";
	pthread_setspecific(key, ptr);
	print_tread2_key();
}
struct pair() {
	int x;
	int y;
}
void *thread3_proc(void *args) {
	struct pair p = {1,2};
	pthread_setspecific(key, &p);
	print_tread3_key();
}
int main(){
     pthread_t thid[10]={0};
     int count = 0;
     pthread_mutex_init(&mutex, NULL);
     pthread_key_create(&key, NULL);
     thread_cb_callback[THREAD_COUNT] = {//运行三个线程
     	thread1_proc,
     	thread2_proc,
     	thread3_proc
     }
     int i=0;
     for(i=0;i<THREAD_COUNT;i++){
     	ptread_create(&thid[i], NULL, callback[i], &count);
     }
     for(i=0;i<THREAD_COUNT;i++){
     	ptread_join(thid[i], NULL);
     }
     return 0;
}

我们来看下运行结果
在这里插入图片描述

五、cpu的亲缘性

让cpu做一个粘合的作用,更大地利用cpu的性能

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define __USE_GNU
#include <sched.h>
#include<unistd.h>
#include<pthread.h>
#include<setjmp.h>
#include <sys/syscall.h>
jmp__buf env;//
int count = 0;
void process_affinity(int num) {
   //gettid();
	pid_t selfid = syscall(__NR_gettid);//返回进程id
	cpu_set_t mask;//bit set
	CPU_ZERO(&mask);
	CPU_SET(selfid%num, &mask);
	//第一个参数进程id号,第二个是mask的大小,第三个参数是绑定的cpu 
	sched_setaffinity(selid, sizeof(mask), &mask);
	while(1);
}
int main(){
     int num = sysconf(_SC_NPROCESSORS_CONF);//cpu的数量
     int i = 0;
     pid_t pid = 0;
     for (i=0;i<num/2;i++) {//比如说num=8,那么我们就创建4个进程
     	pid = fork();
     	if(pid<=(pid_t)0) {
     		break;
     	}
     }
     if(pid==0) {
     	process_affinity(num);//黏合
     }
     while(1) usleep(1);
     return 0;
}

运行结果,8核的cpu,粘合之后有4个核是满的占用率100%,粘合的作用在于能够更大程度去利用CPU
在这里插入图片描述

六、setjmp/longjmp

longjmp是c++中唯一一个可以破坏栈的,执行不用出栈
setjmp一设置,运行到longjmp以后,直接跳转到setjmp这里去

//创建10个线程
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<setjmp.h>
jmp__buf env;//
int count = 0;
void sub_func(int idx) {
	printf("sub_func:%d\n",idx);
	longjmp(env, idx);//执行到这一步直接跳到setjmp(env);这一行
}
int main(){
     count = setjmp(env);//正常状态下返回0,如果有longjmp返回值由longjmp的第二个参数决定
     if(count==0) {
     	sub_func(++count);
     } else if(count==1) {
     	sub_func(++count);
     }  else if(count==2) {
     	sub_func(++count);
     } else if(count==3) {
     	sub_func(++count);
     }else {
     	printf("other items\n");
     }//从count=1开始依次往下运行
     return 0;
}

运行结果
在这里插入图片描述
这里我们就能引出try-catch是怎么做的

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<setjmp.h>
jmp__buf env;//
int count = 0;
#define Try count = setjmp(env); if(count==0)
#define Catch (type) else if(count == type)
#define Throw(type) longjmp(env, type);
#define Finally
void sub_func(int idx) {
	printf("sub_func:%d\n",idx);
	Throw(idx);
}
int main(){
     Try{
      	sub_func(++count);
     } Catch(1) {
     	sub_func(++count);
     } Catch(2) {
     	sub_func(++count);
     } Catch(3) {
     	sub_func(++count);
     } Finally{
     	printf("other items\n");
     }
     return 0;
}

但是我们引出了以下两个问题需要后续好好讨论思考一下
1、嵌套try怎么解决
我想到了用链表实现try嵌套
2、线程安全怎么解决
我想到了线程的私有空间
未完待续。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值