C/C++高性能编码指导(5)多线程并发控制优化

真实业务场景下,往往会采用多线程并发的方式提升系统的吞吐率;在多线程场景下,往往会采用并发
控制的手段,预防由并发操作可能带来的错误。
具体地,并发控制作用于多个线程共享的资源,例如变量、数据、外设等,通过锁、无锁机制、原子操
作等技术手段,进行共享资源的保护,避免多个线程同时读写访问等带来的冲突;
多线程编程时,对于共享临界区的保护手段主要是锁。其中从性能角度考虑,由高到低的典型并发控制
机制如下:无锁机制,免锁机制,锁

1. 无锁机制

【简介】 无锁指完全不需要锁,主要有以下几种场景
1. 应用的典型场景为单生产者单消费者模型
2. 每线程变量,即线程变量,对应关键字 __thread
比如多个线程,每个线程通过线程变量作为资源索引,在线程中不需要加锁访问。
其本质是由编译器(加载器)帮程序员通过空间换时间
3. 每CPU变量

典型应用场景是数据面基于多核的编程框架,不同于基于线程的编程框架。
对应代码就是有多少个核就定义多大的数组,数组的每个元素之间是无锁并发的
【注意事项】
有乱序风险,比如芯片支持指令乱序执行。以无锁队列为例说明何为乱序:
在无锁队列中,通过读写指针控制ring buffer的操作读取与写入,即更新读写指针与队列拷贝
两个核心动作
业务预期的行为(即程序员的约定俗成,编译器是不知道的)是先完成拷贝动作(注意该动作
是比较耗时的),再更新读写指针。
乱序的表现是先完成了读写指针更新,此时内容尚未拷贝完成。
可通过必要的内存屏障解决(性能会有一定损失)。
基础数据类型的读取与赋值操作自身就是原子的,属于无锁操作

2. 免锁机制

【简介】 免锁指基于特定指令架构提供CAS指令实现的互斥访问机制
原子操作类接口,比如原子加、原子减、原子交换等。
只能保护基础数据类型,如uint8, uint64等;不支持复杂数据类型,比如结构体、临界区由多
个基础数据类型组成等。
应用的典型场景是多生产者多消费者模型,比如业界的Disruptor队列等;

__atomic Builtins (Using the GNU Compiler Collection (GCC))

【注意事项】
同无锁,亦存在乱序风险。可通过必要的内存屏障解决。
地址对齐问题(对应SIGBUS异常):不同芯片有不同的对齐要求,比如海思某些处理器要求地址必须8字节对
齐,x86芯片支持1字节对齐。

__atomic_compare_exchange_n 函数是 C 语言中用于原子比较并交换操作的函数,在多线程编程中非常有用。该函数的原型如下:

bool __atomic_compare_exchange_n (type *ptr, type *expected, type desired,
               bool weak, int success_memorder, int failure_memorder);

其中,type 表示进行原子操作的数据类型,可以是 char、short、int、long long 
等基本类型,也可以是指针类型或自定义结构体类型。参数 ptr 是要进行操作的变量的地址,
expected 是一个指向期望值的指针,desired 是期望修改成的值。如果 ptr 所指向的值
等于 expected 指向的值,则将其修改为desired,并返回 true;否则不修改,返回 false。
weak 参数表示是否使用弱一致性,success_memorder和 failure_memorder 分别表示
成功和失败时所需的内存顺序(memory order)。
#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
  
#define NUM_THREADS 4  
  
int shared_data = 0;  
  
void* thread_func(void* thread_id) {  
    int tid = (int)thread_id;  
    int local_data = tid;

    for (int i = 0; i < 100000; i++) {  
        int expected = local_data;  
        int desired = tid + 1;  
        if (__atomic_compare_exchange_n(&shared_data, &expected, desired, 
1, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {  
            printf("Thread %ld wrote %d to shared data\n", tid, desired);  
        } else {  
           printf("Thread %ld failed to write to shared data\n", tid);  
        }  
    }  
  
    pthread_exit(NULL);  
}  
  
int main() {  
    pthread_t threads[NUM_THREADS];  
  
    for (int i = 0; i < NUM_THREADS; i++) {  
        pthread_create(&threads[i], NULL, thread_func, (void*)i);  
    }  
  
    for (int i = 0; i < NUM_THREADS; i++) {  
        pthread_join(threads[i], NULL);  
    }  
  
    printf("Final shared data: %d\n", shared_data);  
    return 0;  
}

3. 锁

【简介】 锁主要分为如下几种类型:
用户态轻量锁:基于cas指令实现的自旋锁、读写锁、互斥锁等。常用与数据面的并发控制。一般
用于保护较短小的临界区,比如多个基础数据类型的并发操作等。
典型应用场景基于双链表的头插尾取实现的消息队列
注意事项
这类锁相对于Linux提供的原生锁,效率更高
临界区不能有阻塞性操作,比如IO读写等
存在优先级翻转问题,即低优先级的线程持有锁期间释放了调度权,进而导致高优先级
的线程因等锁而空耗CPU,一般表现是CPU占用率很高或者死锁(比如FIFO线程)
可通过在锁冲突时退化为互斥锁降低该类问题的概率

3.1 自旋锁

Linux提供的一种锁机制。基于cas实现的公平锁,一般用于较短小的临界区保护。其特点
是锁冲突时通过空耗CPU尝试获取锁的使用权。
注意事项
临界区不能有阻塞性操作,比如IO读写等。

编译下面程序的命令是:gcc -o spinlock spinlock.c -lpthread,然后运行程序的命令是:./spinlock

#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
#include <unistd.h>  
  
// 定义一个pthread_spinlock_t类型的变量作为自旋锁  
pthread_spinlock_t lock;  
int shared_data = 0;  
  
void *thread_func(void *arg) {  
    // 获取自旋锁  
    pthread_spin_lock(&lock);  
  
    // 访问共享数据  
    shared_data += 1;  
  
    // 释放自旋锁  
    pthread_spin_unlock(&lock);  
  
    return NULL;  
}  
  
int main() {  
    pthread_t thread1, thread2;  
  
    // 初始化自旋锁  
    if (pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE) != 0) {  
        printf("Error initializing spinlock\n");  
        return -1;  
    }  
  
    // 创建两个线程  
    if (pthread_create(&thread1, NULL, thread_func, NULL) != 0) {  
        printf("Error creating thread 1\n");  
        return -1;  
    }  
  
    if (pthread_create(&thread2, NULL, thread_func, NULL) != 0) {  
        printf("Error creating thread 2\n");  
        return -1;  
    }  
  
    // 等待两个线程结束  
    pthread_join(thread1, NULL);  
    pthread_join(thread2, NULL);  
  
    // 销毁自旋锁  
    pthread_spin_destroy(&lock);  
  
    printf("Shared data: %d\n", shared_data);  
  
    return 0;  
}

3.2 读写锁

Linux提供的一种锁机制,支持多个读者并发操作。写-写,写-读之间存在锁冲突的可能性

Pthread 是 POSIX threads 的简称,是POSIX的线程标准。pthread读写锁把对共享资源的访问者分为读者和写者,读者只对共享资源进行读访问,写者只对共享资源进行写操作。在互斥机制,读者和写者都需要独占互斥量以独占资源,在读写锁机制下,允许同时有多个读者读访问共享资源,只有写者才需要独占资源,读和写是互斥的。相比互斥机制,读写机制由于允许多个读者同时读访问共享资源,进一步提高了多线程的并发度。

1.读写锁机制:

  • 写者:写者使用写锁,如果当前没有读者,也没有其他写者,写者立即获得写锁;否则写者将等待,直到没有读者和写者。
  • 读者:读者使用读锁,如果当前没有写者,读者立即获得读锁;否则读者等待,直到没有写者。 

2.读写锁特性:

  • 同一时刻只有一个线程可以获得写锁,同一时刻可以有多个线程获得读锁。
  • 读写锁出于写锁状态时,所有试图对读写锁加锁的线程,不管是读者试图加读锁,还是写者试图加写锁,都会被阻塞。
  • 读写锁处于读锁状态时,有写者试图加写锁时,之后的其他线程的读锁请求会被阻塞,以避免写者长时间的不写锁。

3.读写锁基本函数:

        头文件: # include<pthread.h>

  • 读写锁初始化:

        int pthread_rwlock_init(pthread_rwlock_t * rwlock,  const pthread_rwlockattr_t *  attr);

        该函数第一个参数为读写锁指针,第二个参数为读写锁属性指针。函数按读写锁属性对读写锁进行初始化。

  • 加读锁:

        int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

        该函数参数为读写锁指针。函数用于对读写锁加读锁。

  • 加写锁:

        int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

        该函数参数为读写锁指针。函数用于对读写锁加写锁。

  • 释放读写锁:

        int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

        该函数参数为读写锁指针。函数用于释放读写锁,包括读锁与写锁。

  • 销毁读写锁:

        int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

        该函数参数为读写锁指针。函数用于销毁读写锁。

#include <iostream>     
#include <pthread.h>      
#include <unistd.h>            
 
pthread_t t1;           //pthread_t变量t1,用于获取线程1的ID
pthread_t t2;           //pthread_t变量t2,用于获取线程2的ID
pthread_rwlock_t rwlock;             //声明读写锁
int data=1;                          //共享资源
void* readerM(void* arg)
{
	while(1)
	{
	pthread_rwlock_rdlock(&rwlock);    //读者加读锁
	printf("M 读者读出: %d \n",data);   //读取共享资源
	pthread_rwlock_unlock(&rwlock);    //读者释放读锁
	sleep(1200);
	}
	return NULL;
}
void* readerN(void* arg)
{
	while(1)
	{
	pthread_rwlock_rdlock(&rwlock);
	printf(" N读者读出: %d \n",data);
	pthread_rwlock_unlock(&rwlock);
	Sleep(700);
	}
	return NULL;
}
void* writerA(void* arg)
{
	while(1)
	{
	pthread_rwlock_wrlock(&rwlock);      //写者加写锁
	data++;                              //对共享资源写数据
	printf("	A写者写入: %d\n",data);
	pthread_rwlock_unlock(&rwlock);      //释放写锁
	sleep(2000);
	}
	return NULL;
}
void* writerB(void* arg)
{
	while(1)
	{
	pthread_rwlock_wrlock(&rwlock);
	 data++;
	printf("	B写者写入: %d\n",data);
	pthread_rwlock_unlock(&rwlock);
	Sleep(2000);
	}
	return NULL;
}
void main(int argc,char** argv)
{
	pthread_rwlock_init(&rwlock, NULL);   //初始化读写锁
    
    pthread_create(&t1,NULL,readerM,NULL);
	pthread_create(&t1,NULL,readerN,NULL);
	pthread_create(&t2,NULL,writerA,NULL);
	pthread_create(&t2,NULL,writerB,NULL);
 
	pthread_rwlock_destroy(&rwlock);      //销毁读写锁
 
	sleep(10000000);
	return;
}

3.3 互斥锁

Linux提供的一种锁机制。存在较大概率的锁冲突,性能开销较大,尤其是锁冲突后引起
的内核调度等一系列开销。

互斥锁是用以保护对共享资源的操作,即保护线程对共享资源的操作代码可以完整执行,而不会在访问的中途被其他线程介入对共享资源访问。通常把对共享资源操作的代码段,称之为临界区,其共享资源也可以称为临界资源。于是这种机制——互斥锁的工作原理就是对临界区进行加锁,保证处于临界区的线程不被其他线程打断,确保其临界区运行完整

1.使用之前需要先初始化一个互斥锁

2.如果线程加锁成功,则可以访问共享资源,期间不会被打断,在访问结束之后解锁

3.线程在进行上锁时,其锁资源被其他线程持有,那么该线程则会执行阻塞等待,等待锁资源被解除之后,才可以进行加锁

4.互斥锁并不能保证线程的执行先后,但却可以保证对共享资源操作的完整性
 

典型场景是临界区被多个线程同时即读又写操作。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define NUM_THREADS 5
pthread_mutex_t lock1,lock2; 
int k;
void *PrintHello(void *tt)/* 线程函数 */
{
    long tid;
    int i,j;
    tid = (long)tt;
    while(1)
    {   sleep(2);
        pthread_mutex_lock(&lock1);//上锁1
        printf("It's thread #%ld begining!\n", tid);/* 打印线程对应的参数 */

        pthread_mutex_lock(&lock2);//上锁2
        for (k = 0; k < 5; k++) 
        { 
            printf("thread #%ld Job %d printing \n", tid,k); 
            for(i=0;i<100000;i++)
            for(j=0;j<6666;j++);
        }   
        printf(" thread #%ld is over!\n", tid); 
        pthread_mutex_unlock(&lock2);//一个线程中操作多个互斥锁时,加锁与解锁的顺序一定是相反的
        pthread_mutex_unlock(&lock1);//解锁1
        /* 线程先加锁1,后加锁2,之后一定要先解锁2,再解锁1 */
        
    }
}
 
int main (int argc, char *argv[])
{
    pthread_t threads[NUM_THREADS];
    long t;
    for(t=0; t<NUM_THREADS; t++)
    {   
        /* 循环创建 5 个线程 */
        printf("In main: creating thread %ld\n", t);
        pthread_create(&threads[t], NULL, PrintHello, (void *)t); /* 创建线程,系统自动分配资源 */
    } 
    pthread_mutex_init(&lock1,NULL);//初始化互斥锁
    pthread_mutex_init(&lock2,NULL);//初始化互斥锁
    while(1);
}


注意事项
这类锁应用范围最广,容易成为系统瓶颈
优化方法一般有:明确要包含的临界资源;不同资源采用不同锁;避免递归锁场景等
多线程编程时,采用锁的方式进行线程同步,可能导致部分线程同步开销(同PFM.LOCK.01原
理),如果可以采用免锁机制,如无锁队列,原子操作等,可以从实现层面彻底避免线程因锁冲突带来的挂起与等待,提高多线程效能

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值