C++ 11 thread类多线程笔记

C++11std命名空间将Boost库中的Thread加入到标准库<thread>中,Boost的多线程从准标准变为标准。

创建多线程任务

thread可以将一个函数封装成为是一个线程对象,函数task()运行于该线程中,该函数可以接收任意数量的参数。thread对象创建后会立刻运行,join()为主线程等待子线程的阻塞模式,detach()为主线程不管子线程的非阻塞模式,被 detach的线程将不受控制,无法再join

普通函数多线程

#include <iostream>
#include <string>
#include <vector>
#include <thread>

using namespace std;


void task(int id) {
    cout << "task id: " << to_string(id) << endl;
}

void run_function() {
    vector<thread> tasks;

    for (int i = 0; i < 10; i++)
        tasks.push_back(thread(task, i));

    for (thread& t: tasks)
        t.join();
}

int main() {
    run_function();

    return 0;
}

成员函数多线程

与普通函数不同的是,使用成员函数需要绑定this指针作为第一个参数。

#include <iostream>
#include <string>
#include <vector>
#include <thread>

using namespace std;


class ThreadTest1 {
public:
    ThreadTest1() {}

    void task(int index) {
        cout << "class task id: " << to_string(index) << endl;
    }

    void run() {
        for (int i = 0; i < 10; i++)
            this->tasks.push_back(thread(&ThreadTest1::task, this, i));
        
        for (thread& t: this->tasks)
            t.join();
    }

    ~ThreadTest1() {}
private:
    vector<thread> tasks;
};

int main() {
    ThreadTest1* test = new ThreadTest1();
    test->run();

    delete test;

    return 0;
}
res1

引用和指针作为参数

thread的构造函数中,线程函数的参数被拷贝(浅拷贝)到线程独立内存中,这样可以被线程对象访问。即使函数形参是引用,线程构造函数传递给函数参数的是变量拷贝的引用,而非数据本身的引用。若用ref封装变量,则函数就会接收到变量的引用,而非变量拷贝的引用。当我们的函数参数是指针时,可以直接传递指针;当我们的函数参数是引用时,就需要ref进行封装了。

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>

using namespace std;

mutex count_mutex;

void task_ref(int id, string& c) {
    {
        unique_lock<mutex> lock(count_mutex);
        c = to_string(id);
        cout << "task id: " << to_string(id) << ", context: " << c << endl;
    }
}

void run_function() {
    vector<thread> tasks;
    string context = "glob";

    for (int i = 0; i < 10; i++)
        tasks.push_back(thread(task_ref, i, ref(context)));

    for (thread& t: tasks)
        t.join();

    cout << "task end context: " << context << endl;
}

int main() {
    run_function();
    return 0;
}
ref

线程锁

当多个线程访问同一资源时(比如同时读写同一个变量),为了保证数据的一致性,最简单的方式就是使用mutex提供的互斥锁。

互斥锁

通过mutex可以声明一个互斥锁变量来锁住一个全局变量,我们需要手动来进行lock/unlock的操作,如果前面代码如有异常,unlock就调不到了导致变量被锁死。

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>

using namespace std;

int global_count = 0;
mutex count_mutex;

void task_mutex(int id) {
    count_mutex.lock();

    global_count++;
    cout << "task id: " << to_string(id) << ", count: " << to_string(global_count) << endl;

    count_mutex.unlock();
}

void run_function() {
    vector<thread> tasks;

    for (int i = 0; i < 10; i++) {
        tasks.push_back(thread(task_mutex, i));
    }

    for (thread& t: tasks)
        t.join();
}

int main() {
    run_function();

    return 0;
}
mutex

自动锁

显式的加锁和解锁会导致一些问题,比如忘记解锁或者请求加锁的顺序不正确,进而产生死锁。C++ 11标准提供了一些类和函数帮助解决此类问题。这些封装类保证了在RAII风格上互斥量使用的一致性,可以在给定的代码范围内自动加锁和解锁。封装类包括:

lock_guard: 在构造对象时,它试图去获取互斥量的所有权(通过调用lock()),在析构对象时,自动释放互斥量(通过调用unlock())。这是一个不可复制的类。

unique_lock: 这个一通用的互斥量封装类,不同于lock_guard,它还支持延迟加锁,时间加锁和递归加锁以及锁所有权的转移和条件变量的使用。这也是一个不可复制的类,但它是可移动类。

互斥锁unique

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>

using namespace std;

int global_count = 0;
mutex count_mutex;

void task_mutex(int id) {
    {
        unique_lock<mutex> lock(count_mutex);
        global_count++;
        cout << "task id: " << to_string(id) << ", count: " << to_string(global_count) << endl;
    }
}

void run_function() {
    vector<thread> tasks;

    for (int i = 0; i < 10; i++) {
        tasks.push_back(thread(task_mutex, i));
    }

    for (thread& t: tasks)
        t.join();
}

int main() {
    run_function();

    return 0;
}

读写锁shared

unique_lock互斥锁不管是对读操作还是写操作,都会独占这个变量,这对只有多线程读操作时会出现性能损耗。STLBoost都提供了 shared_mutex来解决这个问题,可以将其理解为是一种读写锁。读写锁就是同时可以被多个读者拥有,但是只能被一个写者拥有的锁。而所谓「多个读者、单个写者」,并非指程序中只有一个写者(线程),而是说不能有多个写者同时去写。

C++11下只能用Boost来实现;C++14提供了具有超时机制的可共享互斥量shared_timed_mutex; C++17提供了共享的互斥量shared_mutex;

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <boost\thread\thread.hpp>


using namespace std;

int global_count = 0;
shared_mutex count_mutex;

void task_write() {
    {
        unique_lock<boost::shared_mutex> lock(count_mutex);
        global_count++;
    }
}

void task_read(int id) {
    {
         boost::shared_lock<boost::shared_mutex> lock(count_mutex);
         cout << "task id: " << to_string(id) << ", count: " << to_string(global_count) << endl;
    }

}

int main() {
   thread t1 = thread(task_write);
   thread t2 = thread(task_write);
   thread t3 = thread(task_read, 1);
   thread t4 = thread(task_read, 2);

   t1.join();
   t2.join();
   t3.join();
   t4.join();
}

原子类型

C++11引入了atomicintcharbool等基础数据结构进行了原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。

所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。

在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intelpthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如atomic_boolatomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>

using namespace std;


atomic<int> global_count(0);

void task_atomic(int id) {
    global_count += 2;
}

void run_function() {
    vector<thread> tasks;

    for (int i = 0; i < 10; i++)
        tasks.push_back(thread(task_atomic, i));

    for (thread& t: tasks)
        t.join();

    cout << "count result: " << to_string(global_count) << endl;
}


int main() {
    run_function();
    return 0;
}
atomic

条件变量

条件变量(Condition Variable)是线程的另外一种同步机制,这些同步对象为线程提供了会合的场所,理解起来就是两个(或者多个)线程需要碰头(或者说进行交互-一个线程给另外的一个或者多个线程发送消息),我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则接收条件已经发生改变的信号。条件变量同锁一起使用使得线程可以以一种无竞争的方式等待任意条件的发生。所谓无竞争就是,条件改变这个信号会发送到所有等待这个信号的线程。而不是说一个线程接受到这个消息而其它线程就接收不到了。

条件变量的一般用法是:线程 A等待某个条件并挂起,直到线程 B 设置了这个条件,并通知条件变量,然后线程 A 被唤醒。经典的「生产者-消费者」问题就可以用条件变量来解决。这里等待的线程可以是多个,通知线程可以选择一次通知一个(notify_one)或一次通知所有(notify_all)。

线程进入等待状态前先加锁。等待时,如果条件不满足,wait会原子性地解锁并把线程挂起。与条件变量搭配使用的锁,必须是unique_lock。当条件变量被通知后,挂起的线程就被唤醒,但是唤醒也有可能是假唤醒,或者是因为超时等异常情况,所以被唤醒的线程仍要检查条件是否满足,所以wait是放在条件循环里面。cv.wait(lock, [] { return status; }); 相当于:while (!status) { cv.wait(lock); }

notify_one():因为只唤醒等待队列中的第一个线程;不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,需要等待再次调用notify_one()或者notify_all()

notify_all():会唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁。其余未获取锁的线程会继续尝试获得锁(类似于轮询),而不会再次阻塞。当持有锁的线程释放锁时,这些线程中的一个会获得锁。而其余的会接着尝试获得锁。

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace std;

class CP {
public:
    CP() {};

    void produce() {
        for (int i = 0; i < 1000; i++) {
            {
                unique_lock<mutex> lock(this->buffer_mutex);
                this->cv.wait(lock, [=] {return (this->buffer.size() < this->bufferSize);});

                this->buffer.push_back(i);
                this->produceCount++;

                cout << "total: " << to_string(this->buffer.size()) << ", produce: " << to_string(this->produceCount) << ", consume: " << to_string(this->consumeCount) << endl;
            }

            this->cv.notify_one();
        }
    }

    void consume() {
        while (true) {
            {
                unique_lock<mutex> lock(this->buffer_mutex);
                this->cv.wait(lock, [=] {return (this->buffer.size() > 0);});
 
                this->buffer.pop_back();
                this->consumeCount++;
                cout << "total: " << to_string(this->buffer.size()) << ", produce: " << to_string(this->produceCount) << ", consume: " << to_string(this->consumeCount) << endl;
            }

            this->cv.notify_one();
        }
        
    }

    void run() {
        vector<thread> tasks;

        tasks.push_back(thread(&CP::produce, this));
        tasks.push_back(thread(&CP::produce, this));
        tasks.push_back(thread(&CP::consume, this));
        tasks.push_back(thread(&CP::consume, this));
        tasks.push_back(thread(&CP::consume, this));

        for (thread& t: tasks)
            t.join();
    }

    ~CP() {};
private:
    vector<int> buffer;
    int bufferSize = 100;
    int produceCount = 0;
    int consumeCount = 0;
    mutex buffer_mutex;
    condition_variable cv;
};

int main() {
    CP* cp = new CP();
    cp->run();
    delete cp;

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

推荐阅读更多精彩内容