C++ 多线程入门

主要参考:Advanced Operating Systems-Multi-threading in C++ from Giuseppe Massari and Federico Terraneo

介绍

多任务处理器允许我们同时运行多个任务。操作系统会为不同的进程分配独立的地址空间。
多线程允许一个进程在共享的地址空间里执行多个任务。

线程

一个线程是一个轻量的任务。
每个线程拥有独立的栈和context。


多线程

取决于具体的实现,线程至核心的安排由OS或者language runtime来负责。

C++对线程的支持

新建线程

void myThread() {
    for (;;) {
        std::cout << "world" << std::endl; 
    }
}
int main() {
    std::thread t(myThread);
    for(;;) {
        std::cout << "hello " << std::endl;
    }
}

std::thread的构造函数可以以一个可调用对象和一系列参数为参数来启动一个线程执行这个可调用对象。
除了上面例子里的函数(myThread)外,仿函数(functor)也是线程常用的可调用对象。
仿函数是一个定义和实现了operator()成员函数的类。与普通的函数相比,可以赋予其一些类的性质,如继承、多态等。
std::thread::join()等待线程结束,调用后thread变为unjoinable。
std::thread::detach()将线程与thread对象脱离,调用后thread变为unjoinalbe。
bool std::thread::joinable()返回线程是否可加入。

同步

static int sharedVariable = 0;
void myThread() {
    for (int i=0; i<1000000; i++) sharedVariable++;
}
int main() {
    std::thread t(myThread);
    for (int i=0; i<1000000; i++) sharedVariable--;
    t.join();
    std::cout<<"sharedVariable="<<sharedVariable<<std::endl;
}

上面的程序会遇到数据竞争的问题,因为++--都不是元操作(atomic operation),实际上我们需要拿到数据、递增/递减、放回数据三步,而两个线程可能会在对方没有完成三步的时候就插入,导致结果不可预测。

image.png

为了避免竞争,我们需要在线程进入关键段(critical section)的时候阻止并行。为此,我们引入互斥锁。

互斥锁

在我们进入一个关键段的时候,线程检查互斥锁是否是锁住的:

  • 如果锁住,线程阻塞
  • 如果没有,则进入关键段

std::mutex有两个成员函数lockunlock
然而,对互斥锁使用不当可能导致死锁(deadlock):

  • 原因1:忘记unlock一个mutex
    解决方案:使用scoped lock locak_guard<mutex>,会在析构的时候自动释放互斥锁。
    std::mutex myMutex;
    void muFunctions(int value) {
        {
            std::lock_guard<std::mutex> lck(myMutex);
            //...
        }
    } 
    
  • 原因2:同一个互斥锁被嵌套的函数使用
    解决方案:使用recursive_mutex,允许同一个线程多次使用同一个互斥锁。
    std::recursive_mutex myMutex;
    void func2() {
        std::lock_guard<recursive_mutex> lck(myMutex);
        //do some thing
    }
    void func1() {
        std::lock_guard<recursive_mutex> lck(myMutex);
        //do some thing
        func2();
    }
    
  • 原因3:多个线程用不同的顺序调用互斥锁
    解决方案:使用lock(..)函数取代mutex::lock()成员函数,该函数会自动判断上锁的顺序。
    mutex myMutex1, myMutex2;
    void func2() {
        lock(myMutex1, myMutex2);
        //do something
        myMutex1.unlock();
        myMutex2.unlock();
    }
    void func1() {
        lock(myMutex2, myMutex1);
        //do something
        myMutex1.unlock();
        myMutex2.unlock();
    }
    

条件变量

有的时候,线程之间有依赖关系,这种时候需要一些线程等待其他线程完成特定的操作。
std::condition_variable条件变量,有三个成员函数:

  • wait(unique_lock<mutex> &):阻塞当前线程,直到另一个线程将其唤醒。在wait(...)的过程中,互斥锁是解锁的状态。
  • notify_one():唤醒一个等待线程。
  • notify_all():唤醒所有等待线程。
using namespace std;
string shared;
mutex myMutex;
condition_variable myCv;

void myThread() {
    unique_lock<mutex> lck(myMutex);
    while (shared.empty()) myCv.wait(lck);
    cout << shared << endl;
}

int main() {
    thread t(myThread);
    string s;
    cin >> s;
    {
        unique_lock<mutex> lck(myMutex);
        shared = s;
        myCv.notify_one();
    }
    t.join();
}

另外有一个比较小的点:为什么wait()通常放在循环中调用,是为了保证condition_variable被唤醒的时候条件仍然会被判断一次。

设计模式

Producer/Consumer

一个消费者线程需要生产者线程提供数据。
为了让两个线程的操作解耦,我们设计一个队列用来缓存数据。


image.png
#include <list>
#include <mutex>
#include <condition_variable>

template<typename T>
class SynchronizedQueue {
public:
    SynchronizedQueue();
    void put(const T&);
    T get();
private:
    SynchronizedQueue(const SynchronizedQueue&);
    SynchronizedQueue &operator=(const SynchronizedQueue&);
    std::list<T> queue;
    std::mutex myMutex;
    std::condition_variable myCv;
};

template<typename T>
void SynchronizedQueue<T>::put (const T& data) {
    std::unique_lock<std::mutex> lck(myMutex);
    queue.push_backdata();
    myCv.notify_one();
}

template<typename T>
T SynchronizedQueue<T>::get() {
    std::unique_lock<std::mutex> lck(myMutex);
    while(queue.empty())
        myCv.wait(lck);
    T result = queue.front();
    queue.pop_front();
    return result;
}

Active Object

目标是实例化一个任务对象。
通常来说,其他线程无法通过显式的方法与一个线程函数通信,数据常常是通过全局变量在线程之间交流。
这种设计模式让我们能够在一个对象里封装一个线程,从而获得一个拥有可调用方法的线程。
设计一个类,拥有一个thread成员变量和一个run()成员函数。

//active_object.hpp
#include <atomic>
#include <thread>

class ActiveObject {
public:
    ActiveObject();
    ~ActiveObject();
private:
    virtual void run();
    ActiveObject(const ActiveObject&);
    ActiveObject& operator=(const ActiveObject&);
protected:
    std::thread t;
    std::atomic<bool> quit;
};

//active_object.cpp
#include "active_object.hpp"
#include <functional>

ActiveObject::ActiveObject() :
    t(std::bind(&ActiveObject::run, this)), quit(false) {}

void ActiveObject::run() {
    while(!quit.load()) {
        // do something
    }
}

ActiveObject::~ActiveObject() {
    if(quit.load()) return;
    quit.store(true);
    t.join();
}

其中std::bind可以用于基于函数和部分/全部参数构建一个新的可调用对象。

Reactor

Reactor的目标在于让任务的产生和执行解耦。会有一个任务队列,同时有一个执行线程负责一次执行队列里的任务(FIFO,当然也可以设计其他的执行顺序)。Reactor本身可以继承自Active object,同时维护一个Synchronized Queue作为成员变量。
这样我们拥有了一个线程,它能够在执行的过程中不断地接受新的任务,同时避免了线程频繁的构建和析构所浪费的资源。

ThreadPool

Reactor的局限在于任务是顺序完成的,而线程池Thread Pool则允许我们让多个线程监听同一个任务队列。
一个比较不错的实现可以参考这里:https://blog.csdn.net/MOU_IT/article/details/88712090
通常来说,一个线程池需要有以下几个元素:

  • 管理器(创建线程、启动/停止/添加任务)
  • 任务队列
  • 任务接口(任务抽象)
  • 工作线程

其他概念

还有一些其他的与多线程息息相关的概念:

atomic原子类型

常见的比如用std::atomic<bool>或者std::atomic_bool取代bool类型变量。
原子类型主要涉及以下几个问题(参考):

tearing: a read or write involves multiple bus cycles, and a thread switch occurs in the middle of the operation; this can produce incorrect values.
cache coherence: a write from one thread updates its processor's cache, but does not update global memory; a read from a different thread reads global memory, and doesn't see the updated value in the other processor's cache.
compiler optimization: the compiler shuffles the order of reads and writes under the assumption that the values are not accessed from another thread, resulting in chaos.
Using std::atomic<bool> ensures that all three of these issues are managed correctly. Not using std::atomic<bool> leaves you guessing, with, at best, non-portable code.

future和promise

在线程池里常常会用到异步读取线程运行的结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 最近是恰好写了一些c++11多线程有关的东西,就写一下笔记留着以后自己忘记回来看吧,也不是专门写给读者看的,我就想...
    编程小世界阅读 2,483评论 1 2
  • 介绍:什么是线程,线程的优点是什么 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是...
    未来已来_1cab阅读 2,151评论 0 3
  • 本文根据众多互联网博客内容整理后形成,引用内容的版权归原始作者所有,仅限于学习研究使用,不得用于任何商业用途。 互...
    深红的眼眸阅读 1,092评论 0 0
  • 互斥量 用于线程同步,保证多线程访问共享数据的正确性 基本类型 std::mutex:独占的互斥量,不能递归使用 ...
    JasonLiThirty阅读 555评论 0 1
  • 接着上节 atomic,本节主要介绍condition_varible的内容,练习代码地址。本文参考http://...
    jorion阅读 8,464评论 0 7