C++后端开发(3.2.1)—— 原子操作CAS与锁实现
小节提纲
1.互斥锁的使用场景与原理
2.自旋锁的性能分析
3.原子操作的汇编实现
简介
本文介绍互斥锁自旋锁的区别与应用,原子操作CAS、线程私有数据的操作、setjmp / longjmp 与 try / catch 的实现以及CPU的亲缘性
1 互斥锁与自旋锁使用
C++11多线程之mutex等锁机制和atomic原子操作对比
#include <pthread.h>
1.1互斥锁(mutexlock):
最常使用于线程同步的锁;标记用来保证在任一时刻,只能有一个线程访问该对象,同一线程多次加锁操作会造成死锁;临界区和互斥量都可用来实现此锁,通常情况下锁操作失败会将该线程睡眠等待锁释放时被唤醒
pthread_mutex_t mutex;
// 测试
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);
}
}
int main()
{
pthread_t thid[THRHEAD_COUNT] = {0};
int count = 0;
int i = 0;
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < THRHEAD_COUNT; i++)
{
pthread_create(&thid[i], NULL, func, &count);
}
for (i = 0; i < 100; i++)
{
printf("count --> %d\n", count);
sleep(1);
}
}
1.2自旋锁(spinlock):
同样用来标记只能有一个线程访问该对象,在同一线程多次加锁操作会造成死锁;使用硬件提供的swap指令或test_and_set指令实现;同互斥锁不同的是在锁操作需要等待的时候并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁,这样做的好处是节省了线程从睡眠状态到唤醒之间内核会产生的消耗,在加锁时间短暂的环境下这点会提高很大效率
pthread_spinlock_t spinlock;
// 测试
void *func(void *arg)
{
int *pcount = (int *)arg;
int i = 0;
while (i++ < 100000)
{
pthread_spin_lock(&spinlock);
(*pcount)++;
pthread_spin_unlock(&spinlock);
usleep(1);
}
}
int main()
{
pthread_t thid[THRHEAD_COUNT] = {0};
int count = 0;
int i = 0;
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
for (i = 0; i < THRHEAD_COUNT; i++)
{
pthread_create(&thid[i], NULL, func, &count);
}
for (i = 0; i < 100; i++)
{
printf("count --> %d\n", count);
sleep(1);
}
}
1.3 Linux自旋锁与互斥锁的区别
- 实现方式上的区别:互斥锁是基于自旋锁实现的,所以自旋锁相较于互斥锁更加底层。
- 开销上的区别:获取不到互斥锁时会发生上下文切换并休眠,而自旋锁则“自旋”在原地直到被获取。
- 使用场景的区别:互斥锁只能在进(线)程中使用,不能在中断里使用,而自旋锁可以在中断里使用。
- 使用方式上区别:互斥锁只能由获取到该锁的进(线)程来释放,而自旋锁没有这个限制,但上锁和解锁一般都是成对使用的。
1.4 Linux自旋锁与互斥锁的选用原则
根据上述区别可得出以下三条选用原则:
- **当需要保护的临界区较小时,宜选用自旋锁,否则选用互斥锁。**因为当锁不能被获取到时,互斥锁的开销是上下文切换,上下文切换的开销是很大的。但当临界区执行时间的开销大于上下文切换的开销时,就适合使用互斥锁了,这种情况下使用自旋锁会让CPU空转直到其他执行单元解锁为止(还不如发生一次上下文切换),降低了系统效率。
- **自旋锁保护的临界区不能有引起切换上下文(休眠)的函数,但互斥锁可以。**若自旋锁保护的临界区发生上下文切换,而切换上下文后执行的进(线)程又来获取该自旋锁,这样就必然会导致死锁的发生。另外,互斥锁保护的临界区也应该尽量避免阻塞(例如请求另外一个互斥锁),否则也容易出现死锁的情况。
- **如果被保护的临界区处于中断里,那么只能使用自旋锁。**因为互斥锁可能会导致阻塞,而中断是不能被阻塞的。
2 原子操作CAS
2.1 原子操作
所谓原子操作是指不会被线程调度机制打断的操作,当某次操作一旦开始,就一直运行到结束,中间不会有任何中断。
举个例子:
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
- 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
- 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。
2.2 CAS
Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,采用这种无锁的原子操作可以实现线程安全,避免加锁的笨重性。
CAS操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B)。
如果内存位置的值与预期原值相等,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
CAS是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,循环这个指令,直到成功为止。
2.3 C函数实现
// 原子操作
int inc(int *value, int add)
{
int old;
__asm__ volatile(
"lock; xaddl %2, %1;"
: "=a"(old)
: "m"(*value), "a"(add)
: "cc", "memory");
return old;
}
int main()
{
pthread_t thid[THRHEAD_COUNT] = {0};
int count = 0;
int i = 0;
pthread_key_create(&key, NULL);
for (i = 0; i < THRHEAD_COUNT; i++)
{
pthread_create(&thid[i], NULL, func, &count);
}
for (i = 0; i < 100; i++)
{
printf("count --> %d\n", count);
sleep(1);
}
3 线程私有数据
函数 pthread_key_create() 用来创建线程私有数据。该函数从 TSD 池中分配一项,将其地址值赋给 key 供以后访问使用。第 2 个参数是一个销毁函数,它是可选的,可以为 NULL,为 NULL 时,则系统调用默认的销毁函数进行相关的数据注销。如果不为空,则在线程退出时(调用 pthread_exit() 函数)时将以 key 锁关联的数据作为参数调用它,以释放分配的缓冲区,或是关闭文件流等。
不论哪个线程调用了 pthread_key_create(),所创建的 key 都是所有线程可以访问的,但各个线程可以根据自己的需要往 key 中填入不同的值,相当于提供了一个同名而不同值的全局变量(这个全局变量相对于拥有这个变量的线程来说)。
注销一个 TSD 使用 pthread_key_delete() 函数。该函数并不检查当前是否有线程正在使用该 TSD,也不会调用清理函数(destructor function),而只是将 TSD 释放以供下一次调用 pthread_key_create() 使用。在 LinuxThread 中,它还会将与之相关的线程数据项设置为 NULL。
// 线程私有数据测试
typedef void *(*thread_cb)(void *);
void print_thread1_key(void)
{
int *p = (int *)pthread_getspecific(key);
printf("thread 1 : %d\n", *p);
}
void *thread1_proc(void *arg)
{
int i = 5;
pthread_setspecific(key, &i);
print_thread1_key();
}
void print_thread2_key(void)
{
char *ptr = (char *)pthread_getspecific(key);
printf("thread 2 : %s\n", ptr);
}
void *thread2_proc(void *arg)
{
char *ptr = "thread2_proc";
pthread_setspecific(key, ptr);
print_thread2_key();
}
struct pair
{
int x;
int y;
};
void print_thread3_key(void)
{
struct pair *p = (struct pair *)pthread_getspecific(key);
printf("thread 3 x: %d, y: %d\n", p->x, p->y);
}
void *thread3_proc(void *arg)
{
struct pair p = {1, 2};
pthread_setspecific(key, &p);
print_thread3_key();
}
int main()
{
thread_cb callback[THRHEAD_COUNT] = {
thread1_proc,
thread2_proc,
thread3_proc};
for (i = 0; i < THRHEAD_COUNT; i++)
{
pthread_create(&thid[i], NULL, callback[i], &count);
}
for (i = 0; i < THRHEAD_COUNT; i++)
{
pthread_join(thid[i], NULL);
}
}
4 setjmp / longjmp 与 try / catch实现
4.1 setjmp / longjmp
// setjmp / longjmp
struct ExceptionFrame
{
jmp_buf env;
int count;
struct ExceptionFrame *next;
};
// stack --> current node save
jmp_buf env;
int count = 0;
void sub_func(int idx)
{
printf("sub_func : %d\n", idx);
longjmp(env, 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 item\n");
}
}
4.2 基于setjmp / longjmp 的 try / catch实现
// setjmp / longjmp
struct ExceptionFrame
{
jmp_buf env;
int count;
struct ExceptionFrame *next;
};
// stack --> current node save
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
#endif
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 item\n");
}
}
5 CPU亲缘性
htop使用详解–史上最强
所谓CPU亲缘性可以分为两大类:软亲缘性和硬亲缘性。
Linux 内核进程调度器天生就具有被称为 CPU 软亲缘性(soft affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。但不代表不会进行小范围的迁移。
CPU 硬亲缘性是指通过Linux提供的相关CPU亲缘性设置接口,显示的指定某个进程固定的某个处理器上运行。本文所提到的CPU亲缘性主要是指硬亲缘性。
void process_affinity(int num)
{
// gettid();
pid_t selfid = syscall(__NR_gettid);
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(1, &mask);
// selfid
sched_setaffinity(0, sizeof(mask), &mask);
while (1)
;
}
int main()
{
#if CPU_affinity_test
// 8
int num = sysconf(_SC_NPROCESSORS_CONF);
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)
usleep(1);
#endif
}