原子操作CAS与锁实现

原子操作CAS与锁实现

一 多线程运行时临界资源的问题

今天学习多线程编程时遇到了一个问题。以下是代码。

void *thread_callback(void *arg) {
    int *count = (int *)arg;

    int i = 0;
    while (i++ < 100000) {
        (*count)++;
        usleep(1);
    }
}

int main() {
    pthread_t thid[THREAD_COUNT];
    memset(thid, 0, THREAD_COUNT*sizeof(pthread_t));
    int count = 0;

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

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

    return 0;
}

运行这个程序时,按照预测,printf应该会打印出1000000这个数字,但是每次运行后,打印的最大数字都不是10000000,而是接近一百万的数字。
经过分析,发现count应该算作一个临界资源。“count++”这行代码,在汇编后,会形成三行代码。分别是:

  1. Mov [idx], %eax
  2. Inc %eax
  3. Mov %eax, [idx]

这三行代码分别表示将内存中的某个数赋值到寄存器中、寄存器自加1、将寄存器中的数赋值回原内存地址中。当线程一运行到2语句时,可能会被线程二打断,线程二运行完这3条语句后,线程一继续运行,此时寄存器中的值不是内存中的最新值,而是没有被线程二处理过的值。这样,线程二就白处理了。
解决这个问题,有三种方法可以选择,分别是加互斥锁、加自旋锁或者将count++改变为原子操作。
解决方法

1. 互斥锁 mutex

互斥锁本质上就是一个全局变量,它只有 “lock” 和 “unlock” 两个值,通过对资源进行 "加锁(lock)"和 “解锁(unlock)”,可以确保同一时刻最多有 1 个线程访问该资源,从根本上避免了“多线程抢夺资源”的情况发生。其中,对资源进行“加锁”和“解锁”操作的必须是同一个线程。

pthread_mutex_t mutex;//一般生命为全局变量
pthread_mutex_init(&mutex, NULL);//初始化锁变量
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);//解锁和加锁需在一个线程
pthread_mutex_destroy(&mutex);//销毁一个锁变量

当一个线程遇到一个被互斥锁锁住的锁变量,这个线程会被阻塞。

2.自旋锁 spinlock

Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的。即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。

pthread_spinlock_t spinlock;
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);//初始化
pthread_spin_lock(&spinlock);
pthread_spin_unlock(&spinlock);
pthread_spin_destroy(&spinlock);

自旋锁和互斥锁的不同点在于,当一个线程加锁的自旋锁是没有被解锁的锁时,这个线程不会被阻塞,而是会反复检测锁有没有被解开。它的优点在于不会因触发切换线程而导致系统调用,缺点在于会空转CPU浪费时间。

互斥锁适用于会调用系统调用或复杂的对临界资源的操作。
自旋锁适用于不会调用系统调用或不复杂的对临界资源的操作。

3.原子操作

原子操作是指不会被线程调度机制打断的操作。原子操作需要CPU指令集的支持。
CAS原子指令的实现:

int inc(int *value, int add) {
    __asm__ volatile { //volatile指不让编译器优化这些代码
        "lock; xaddl %2, %1;"//%1指的是第二个参数value,%2指的是第三个参数add;lock,指的是锁住总线
        : "=a" (old) //是第一条指令运算的结果
        : "m" (*value), "a" (add)
        : "cc", "memory"
    };
    return old;
}

将某些对临界资源的操作设置成原子操作也是一个好办法,但是前提是CPU支持这种原子操作。只有少数对临界资源的操作能设置成原子操作。

二 线程私有空间:

线程内部的全局变量
如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量,这就需要新的机制来实现,我们称之为Static memory local to a thread (线程局部静态变量),同时也可称之为线程特有数据

pthread_key_create(&key, NULL);//1
...
pthread_getspecific(key);//2
pthread_setspecific(key, ptr);//3

第一个函数在main函数里运行,它声明了此进程的每个线程都有一块私有空间。
第二个函数在某个线程里运行,能获得本线程私有空间的地址。
第三个函数是向私有空间里赋值。

六 CPU的亲缘性:affinity

cpu的亲缘性,指的是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。
进程(线程)只在某一个CPU运行,避免了CPU间的切换,能使此进程较快的运行。
在内核里进程和线程是不分的。

void process_affinity(int num) {
    // gettid();
    pid_t selfid = syscall(__NR_gettid);//获取进程id
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(selfid%num, &mask);
    sched_setaffinity(selfid, sizeof(mask), &mask);//将线程和某个CPU黏合在一起
}

int main() {
    int num = sysconf(_SC_NPROCESSORS_CONF);//获取CPU核数

    int i = 0;
    pid_t pid = 0;
    for (i = 0; i < num/2; i++) {
        pid = fork();//通过复制创建一个子进程,这个进程被称为子进程,调用者被称为父进程
        if (pid <= (pid_t)0) {
            break;
        }
    }
    if (pid == 0) {
        process_affinity(num);
    }
    while (1) {
    }

七 setjmp/longjmp

setjmp 和 longjmp可以在一个线程中的函数间跳转。

jmp_buf env;

void func() {
	longjmp(env, idx);//1
}

void func() {
	setjmp(env);//2
}

程序可以从1直接跳转到2运行,2返回idx。这种特性可以用来做try catch。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值