锁
自旋锁
当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。
自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。由于这个原因,操作系统的内核经常使用自旋锁。但是,如果长时间上锁的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。线程持有锁的时间越长,则持有该锁的线程将被 OS(Operating System) 调度程序中断的风险越大。如果发生中断情况,那么其他线程将保持旋转状态(反复尝试获取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。
解决上面这种情况一个很好的方式是给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。但是如何去选择自旋时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
在计算任务轻的情况下使用自旋锁可以显著提升速度,这是因为线程切换的开销大于等锁的开销,但是计算任务重的话自旋锁的等待时间就成为主要的开销了。
互斥锁
互斥锁实际是一个互斥量,为获得互斥锁的线程会挂起,这就涉及到线程切换的开销,计算任务重的情况下会比较适合使用。
读写锁
读写锁即只能由一人写但可以由多人读的锁,适用于读操作很多但写操作很少的情况下。
原子操作
多线程下使用原子操作确保我们的操作不会被其他线程参与,一般内联汇编 memory 内存屏障,只允许这一缓存写回内存,确保多线程安全。
多进程下对共享内存的操作使用内联汇编lock,锁住总线,同一时刻只允许一个进程通过总线操作内存。
粒度大小排序
互斥锁 > 自旋锁 > 读写锁 > 原子操作
代码实现
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>
#define THREAD_SIZE 10
int count = 0;
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
pthread_rwlock_t rwlock;
// MOV dest, src; at&t
// MOV src, dest; x86
int inc(int *value, int add) {
int old;
__asm__ volatile ( //lock 锁住总线 只允许一个cpu操作内存(通过总线)确保多进程安全
"lock; xaddl %2, %1;" // "lock; xchg %2, %1, %3;"
: "=a" (old)
: "m" (*value), "a" (add)
: "cc", "memory" //memory 内存屏障,只允许这一缓存写回内存,确保多线程安全
);
return old;
}
//
void *func(void *arg) {
int *pcount = (int *)arg;
int i = 0;
while (i++ < 100000) {
#if 0//无锁
(*pcount) ++;
#elif 0// 互斥锁版本
pthread_mutex_lock(&mutex);
(*pcount) ++;
pthread_mutex_unlock(&mutex);
#elif 0// 互斥锁非阻塞版本
if (0 != pthread_mutex_trylock(&mutex)) {
i --;
continue;
}
(*pcount) ++;
pthread_mutex_unlock(&mutex);
#elif 0 //自旋锁
pthread_spin_lock(&spinlock);
(*pcount) ++;
pthread_spin_unlock(&spinlock);
#elif 0//读写锁
pthread_rwlock_wrlock(&rwlock);
(*pcount) ++;
pthread_rwlock_unlock(&rwlock);
#else //原子操作
inc(pcount, 1);
#endif
usleep(1);
}
}
int main() {
#if 0 //多线程
pthread_t threadid[THREAD_SIZE] = {0};
pthread_mutex_init(&mutex, NULL);
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
pthread_rwlock_init(&rwlock, NULL);
int i = 0;
int count = 0;
for (i = 0;i < THREAD_SIZE;i ++) {
int ret = pthread_create(&threadid[i], NULL, func, &count);
if (ret) {
break;
}
}
for (i = 0;i < 100;i ++) {
pthread_rwlock_rdlock(&rwlock);
printf("count --> %d\n", count);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
#else //多进程
int *pcount = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
//将pcount设置为共享内存区域,这样多进程都可以访问
int i = 0;
pid_t pid = 0;
for (i = 0;i < THREAD_SIZE;i ++) {
pid = fork();
if (pid <= 0) { //创建固定数量进程的巧妙操作,子进程被创建后会休眠1微秒后退出这个创建循环开始下面的加法运算,确保只有主进程在创建子进程。
usleep(1);
break;
}
}
if (pid > 0) { // 主进程不做加法,只打印信息
for (i = 0;i < 100;i ++) {
printf("count --> %d\n", (*pcount));
sleep(1);
}
} else {//子进程在这里做加法运算
int i = 0;
while (i++ < 100000) {
#if 0 //总线不锁,进程读取到的共享内存更新存在延迟,最终结果小于一百万
(*pcount) ++;
#else//锁总线,一次只有一个cpu能操作共享内存,也告诉我们临界资源就是很难发挥出多核性能
inc(pcount, 1);
#endif
usleep(1);
}
}
#endif
}