锁与原子操作

130 阅读4分钟

锁与原子操作

以自增操作为例子:

void *func(void *arg) {
	int *pcount = (int *)arg;
	int i = 0;
	//
	while (i ++ < 100000) {
		(*pcount) ++;    // 并不会到达100000
		usleep(1);
	}
}

int main(){
    int i = 0;
	for (i = 0;i < THREAD_COUNT;i ++) {
		pthread_create(&thid[i], NULL, func, &count);
	}
	

	for (i = 0;i < 100;i ++) {
		printf("count --> %d\n", count);
		sleep(1);
	}
}

如果有两个线程对i=20进行自增:idx++

正常是这样:

20230205222319

从内存加载到寄存器 =》 寄存器自增 =》 从寄存器加载到内存

但可能是:

20230205222642

在线程1切换2时,没问题,在线程2切换线程1时,线程1栈中保存的寄存器的值INC是20,写入内存就是21,而不是22.

如果开启O3优化,编译器会将自增转换为原子操作(后面再将)

那我们在编码层怎么做:

  • 加锁(对临界资源是否需要加锁,看它在汇编层面是否是原子操作 )
  • 直接把操作变成原子操作

互斥锁、自旋锁、原子操作

  1. 锁,就是对临界资源加锁,这里加一把互斥锁
void *func(void *arg) {
	int *pcount = (int *)arg;
	int i = 0;
	while (i ++ < 100000) {
        pthread_mutex_lock(&mutex);  
		(*pcount) ++;
		pthread_mutex_unlock(&mutex);
		usleep(1);
	}
}

但其实,这里更应该用自旋锁

  1. 自旋锁:和互斥锁使用一样的,互斥锁在哪用,自旋锁就在哪里用(不让出cpu,一直等着锁被释放)

使用场景:

  • 临界资源复杂的,有系统调用的操作用互斥锁,简单的用自旋(因为等的时间短,消耗的资源还少于线程切换的资源),
  • 有系统调用的就别用自旋了,文件的读写,只允许一个线程访问的,用互斥锁,自增操作,队列读取可以用自旋锁
  • 操作简单,且cpu提供了指令集的,用原子操作
  1. 原子操作

把三条指令(读、自增、写)变成一条:xaddl

原子操作需要cpu指令集的支持,比如这里的xaddl

int inc(int *value, int add) {

	int old;
	__asm__ volatile (
        // xaddl 第2个参数加第1个参数并把值存储到第一个参数;lock,锁cpu操作内存的总线
        // 锁总线、锁缓存的平时也用不到,这里不赘述了
		"lock; xaddl %2, %1;"   
		: "=a" (old)    // old:第0个参数
		: "m" (*value), "a" (add)    // value第一个参数,add是第二个参数
		: "cc", "memory"
	);
	return old;
}

void *func(void *arg) {
	int *pcount = (int *)arg;
	int i = 0;
	while (i ++ < 100000) {
		inc(pcount, 1);   // 原子操作
		usleep(1);
	}
}

而cas是原子操作的一种,也就是cpu指令集中的一个指令:compare and swap,先有比较再有赋值

if (a==b){   // compare
    a=c;      // swap
}

这个就是cmpxchg(a,b,c),用在单例模式中:

if (instance == null){
    instance = malloc(sizeof(object));
}

原子操作记住常用的就行 自增,自减,加减乘除,cas

cpu亲缘性: 在Linux内核中,都是通过task_struct进行调度的,为了避免一个task_struct被切换到其他核(减少系统调用),可以使用系统函数sched_setaffinity() 将一个或多个task_struct绑定到特定的核上

tip1:如果fork指定数量的子进程:

比如说我要创建6个子进程,如果fork()3次,那就是8个了(2,4,8),可以通过以下方式创建:

    int i = 0;
    int num = sysconf(_SC_NPROCESSORS_CONF);  // 获取cpu核心数量
	pid_t pid = 0;
	for (i = 0;i < num/2;i ++) {
		pid = fork();
		if (pid <= (pid_t)0) {    // 如果是子线程(=0),就退出
			break;
		}
	}

tip2:以下内存我没深入理解,放在这里以后补充

线程私有空间:pthrerad_key

线程是进程的一个实体(),是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

线程所独享的资源有:程序计数器、寄存器、栈、状态字,共享堆内存

为什么线程要有私有数据: 比如不同线程监听不同的端口,端口的数据,就属于线程的私有空间,不应该被其他线程读取,而线程的私有数据是通过一个Key结构体来实现的

setjmp/longjmp:函数间的跳转,实现try-catch的核心,要实现try-catch,还需线程私用空间