锁与原子操作
锁
以自增操作为例子:
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++
正常是这样:
从内存加载到寄存器 =》 寄存器自增 =》 从寄存器加载到内存
但可能是:
在线程1切换2时,没问题,在线程2切换线程1时,线程1栈中保存的寄存器的值INC是20,写入内存就是21,而不是22.
如果开启O3优化,编译器会将自增转换为原子操作(后面再将)
那我们在编码层怎么做:
- 加锁(对临界资源是否需要加锁,看它在汇编层面是否是原子操作 )
- 直接把操作变成原子操作
互斥锁、自旋锁、原子操作
- 锁,就是对临界资源加锁,这里加一把互斥锁
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);
}
}
但其实,这里更应该用自旋锁
- 自旋锁:和互斥锁使用一样的,互斥锁在哪用,自旋锁就在哪里用(不让出cpu,一直等着锁被释放)
使用场景:
- 临界资源复杂的,有系统调用的操作用互斥锁,简单的用自旋(因为等的时间短,消耗的资源还少于线程切换的资源),
- 有系统调用的就别用自旋了,文件的读写,只允许一个线程访问的,用互斥锁,自增操作,队列读取可以用自旋锁
- 操作简单,且cpu提供了指令集的,用原子操作
- 原子操作
把三条指令(读、自增、写)变成一条: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,还需线程私用空间