C++ 多线程


简介

由于现在的CPU基本都是多核,因此如果对性能要求比较高的程序通常会考虑进行并行化处理。但是除了极少数程序,绝大多数的程序在并行化的时候都存在数据依赖的各种限制,需要线程之间的同步。同步的方法主要有互斥量(Mutex)、信号量(Semaphore)、条件变量(Condition Variable)以及屏障(Barrier)。本来以前草草学过一点有关并行的知识,现在基本上都忘记了。这里回顾一下Posix Thread的知识以及简单的介绍一下C++中对于多线程的封装。

Posix Thread

Posix Thread给类Unix提供了统一多线程API,包括Linux系统和MacOS等,简称PThread, 使用C语言的头文件<pthread.h>,并且在编译时需要增加条件-lpthread或者-pthread链接PThread库。

互斥量(Mutex)

互斥量是最经常用来营造同步区域的工具,在多数情况下可以把它当作锁来使用。当一个线程处于互斥量的临界区时,另一个线程会被阻塞在进入临界区的位置,保证了临界区的独占性。
PThtread中提供了数据类型pthread_mutex_t以及下列API

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);//mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);

pthread_mutex_init用于初始化mutex,也可以使用注释中的赋值来进行mutex的初始化。而pthread_mutex_lock用于进入临界区的时候获取锁,如果当前有其它线程在临界区中,当前线程就会进入休眠,这个过程由一系列的系统调用来实现。当没有其它线程在临界区时就能获取锁并进入临界区。注意,同一个线程不能连续使用两次pthread_mutex_lock,否则会出现未定义行为,通常会卡死。当执行完临界区的操作后,可以使用pthread_mutex_unlock来释放锁,这样可以让别的线程进入临界区。在编程时应当要注意pthread_mutex_lockpthread_mutex_unlock一定要成对使用,否则会有难以预料的结果。

C++给互斥量进行了封装,使用起来更加简单,使用头文件mutex,与大多数标准库中的类一样,mutex类位于namespace std中。

mutex mtx;
mtx.lock();
mtx.unlock();

条件变量(Conditional Variable)

许多时候需要等待某些条件完成以后才能进入临界区,而通过死循环去判断条件会浪费大量的CPU资源,同时还存在race condtion的风险,也就是对条件的判断和修改的顺序出了问题(当然你用原子指令对条件进行修改当我没说,但这样效率还是很低)。所以条件变量就能应用在这样的场景中,可以保证在测试条件时不被干扰,同时如果条件测试失败了自动地释放互斥量。

在PThread中提供了条件变量的API

pthread_
pthread_cond_t cond = PTHRAD_COND_INIALIZER; //pthread_cond_init(&cond, NULL);
pthread_mutex_t mutex = PTHREAD_MUTEX_INIALIZER;
bool ready = false;

//等待
pthread_mutex_lock(&mutex);
while(!ready)
{
    pthread_cond_wait(&cond, &mutex);
}
//执行
//......
pthread_mutex_unlock(&mutex);

//唤醒
pthread_mutex_lock(&mutex);
ready = true;
pthread_cond_broadcast(&cond);//唤醒全部
pthread_cond_signal(&cond); //唤醒一个
pthread_mutex_unlock(&mutex);

头文件 condition_variable 提供了对条件变量的封装。

mutex mtx;
condition_variable cv;
bool ready = false;

//必须配合锁使用
//等待
void wait(int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
}

//唤醒
void go() {
  std::unique_lock<std::mutex> lck(mtx);
  ready = true;
  cv.notify_all();
}

信号量(Semaphore)

信号量其实就是P,V操作,比较适合生产者消费者问题。比如有一个资源,有人生产有人使用,当这个资源用完时还要使用就要等待别人生产出来,这种问题就比较适合信号量。很遗憾,C++中似乎还没有对信号量的封装,我们只能使用C语言为我们提供的API,位于semaphore.h中。

sem_t resources;
sem_init(&resources, NULL, 0); //初始化资源为0个

sem_wait(&resources); //消耗一个资源

sem_signal(&resources); //释放一个资源

屏障(Barrier)

比如我们想要所有线程都计算完一个中间结果,再继续往下计算,这就可以用到barrier。barrier的作用是所有的线程到达后才放行,提前到达的线程会被挡在此处。

pthread_barrier_t barrier;
pthread_barrier_init(&barrier, NULL, thread_num);

pthread_barrier_wait(&barrier);

C++20中似乎有了barrier的封装,在头文件barrier中,但是好像支持的地方还不是特别多。

其他

其实同步的方法还有很多,比如原子系列指令,自旋锁(本菜鸡还不会呜呜~),这里面的底层支持也不太一样,用到了不同体系结构提供的原子操作还有一些系统调用,还需要继续学习呀。


Author: 奖章
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 奖章 !
  TOC