C++11多线程编程笔记

线程创建、启动、结束

  • 线程类的参数是一个可调用对象,如函数(可以是类中的成员函数)、函数指针、仿函数、lambda表达式、bind创建的对象;
  • jion() 是阻塞主线程并等待子线程执行完,当子线程执行完毕,join()就执行完毕,主线程继续往下执行,join意为汇合,子线程和主线程回合;
  • detach() 是线程分离,主线程不再与子线程汇合,不再等待子线程,detach后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管;
  • joinable()判断是否可以成功使用join()或者detach(),返回true就可以调用,如果返回false,证明调用过join()或者detach();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <thread>
using namespace std;

void childFunc() {
//子线程代码逻辑
return;
}

int main() {
//创建子线程,线程执行起点是childFunc,然后执行线程
thread childThread(childFunc);

//阻塞主线程并等待子线程执行完毕
childThread.join();

//线程分离,由C++运行时库回收子线程
//childThread.detach();

if (childThread.joinable()) {
//可以调用可以调用join()或者detach()
} else {
//不能调用可以调用join()或者detach()
}

return 0;
}

TODO: 补充如何给子线程传参:类中的成员函数、函数指针、仿函数、lambda表达式、bind创建的对象

mutex互斥量

  • 互斥量是个类对象,我也称为互斥锁;
  • 多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功;
  • 如果没有锁成功,那么流程将卡在lock()这里不断尝试去加锁;
  • 成员函数lock()和unlock()要成对使用;
  • 使用互斥量少了达不到效果,多了影响效率;
1
2
3
4
5
6
7
8
9
10
#include <mutex>
using namespace std;

// 定义
mutex mux;

// 一般使用步骤
1.加锁 mux.lock();
2.操作共享数据
3.解锁 mux.unlock();

std::lock()函数模板

  • std::lock(mutex1,mutex2...); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量;
  • 如果互斥量中某一个没锁住,它就等待所有互斥量都锁住,才能继续执行;
  • 如果有一个没锁住,就会把已经锁住的释放掉;
  • 要么互斥量都锁住,要么都没锁住,防止死锁;

lock_guard类模板

  • 可以取代使用mux互斥量的lock()和unlock()成员函数;
  • lock_guard构造函数执行了mutex::lock(),在作用域结束时,调用析构函数,执行mutex::unlock();
  • 所有使用时注意作用域范围;
  • 若使用第二个参数std::adopt_lock,则表示这个互斥量已经lock(),构造时不需要再lock();
1
2
3
lock_guard<mutex> guard(mux);
// 或
lock_guard<mutex> guard(mux,adopt_lock);

unique_lock类模板

  • 类似lock_guard,但有更多用法;
  • 无需自己unlock();
  • 其第二个参数有:
    1. adopt_lock; 同lock_guard中的,前提是已经lock();
    2. try_to_lock; 尝试用去锁定,如果没有锁定成功,会立即返回,不会阻塞在那里,前提是没有lock();
    3. defer_lock; 初始化一个没有加锁的互斥量mutex,以便后序调用其它方法,前提是没有lock();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unique_lock<mutex> uniLock(mux); // 相当于把mux和uniLock绑定在了一起,uniLock拥有mux的所有权
// 或
unique_lock<mutex> uniLock(myMutex, defer_lock);

uniLock.lock(); // 加锁
uniLock.unlock(); // 解锁

// 尝试给互斥量加锁,如果拿不到锁,返回false,否则返回true
uniLock.try_lock();

// 解除绑定,返回它所管理的mutex对象的指针,并释放所有权,所有权由ptx接管
mutex* ptx = uniLock.release();

// uniLock可以把自己对mux的所有权转移,但是不能复制
unique_lock<mutex> uniLock2(std::move(uniLock));

单例类与共享数据

单例设计模式:

定义:单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

懒汉模式

用到的时候再创建,需要锁;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <mutex>
using namespace std;
mutex myMutex;
class Singleton {
public:
static Singleton* getInstance() {
//双重判断 提高效率
if (instance == NULL) {
lock_guard<mutex> myLockGua(myMutex);
if (instance == NULL) {
instance = new Singleton;
}
}
return instance;
}

private:
Singleton() {}
static Singleton* instance;
};
Singleton* Singleton::instance = NULL;

饿汉模式

一开始就创建,所以不需要锁;

饿汉模式的问题在于非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

1
2
3
4
5
6
7
8
9
10
11
12
#include <mutex>
using namespace std;
mutex myMutex;
class Singleton2 {
public:
static Singleton2* getInstance() { return instance; }

private:
Singleton2() {}
static Singleton2* instance;
};
Singleton2* Singleton2::instance = new Singleton2;

使用call_once函数模板

参数:第一个参数为标记,第二个参数是一个函数名;
功能:能够保证函数只被调用一次,具备互斥量的能力,而且比互斥量消耗的资源更少,更高效。

标记为std::once_flag,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <mutex>
using namespace std;

once_flag g_flag;
class Singleton {
public:
//call_once保证其只被调用一次
static void CreateInstance() { instance = new Singleton; }

//两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
static Singleton *getInstance() {
call_once(g_flag, CreateInstance);
return instance;
}

private:
Singleton() {}
static Singleton *instance;
};
Singleton *Singleton::instance = NULL;

借助局部静态对象实现

《Effective C++》(Item 04)中提出一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法在C++11及之后不用加锁和解锁操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
class single {
private:
single() {}
~single() {}

public:
static single* getinstance();
};

single* single::getinstance() {
static single obj;
return &obj;
}

条件变量

  • std::condition_variable实际上是一个和条件相关的类,就是等待一个条件达成;
  • 条件变量的作用是阻塞线程,然后等待通知将其唤醒;
  • 可以通过判断某个函数是否符合某种条件来决定是阻塞线程等待通知还是唤醒线程,由此实现线程间的同步;
  • condition_variable的等待函数有三个,分别是wait()——等待、wait_for()——等待一段时间、wait_until()——等待至某一时刻;
  • 另外针对每个函数condition_variable还提供了有条件等待和无条件等待两种方式;
  • 如果阻塞,则阻塞到其他某个线程调用notify_one()notify_all成员函数为止,就被唤醒;
  • 只要执行到wait后面就一定加锁成功;

无条件等待

  1. void wait(unique_lock& lck)会无条件的阻塞当前线程然后等待通知,前提是此时对象lck已经成功获取了锁;
  2. 等待(休眠)时会调用lck.unlock()释放锁,使其它线程可以获取锁。一旦得到通知(由其他线程显式地通知),函数就会释放阻塞并调用lck.lock(),使lck保持与调用函数时相同的状态,然后函数返回;
  3. wati()函数因为没有条件判断,因此有时候会产生虚假唤醒,而有条件的等待可以很好的解决这一问题;
  4. 效果跟有条件等待的第二个参数返回false一样;
  5. 无条件等待被唤醒后wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待加锁,当获取到了,wait()就继续执行;
1
2
3
4
5
6
7
8
9
#include <condition_variable>
#include <mutex>
using namespace std;

std::mutex mux;
std::unique_lock<std::mutex> uniLock(mux);
std::condition_variable condition;

condition.wait(uniLock);

有条件等待

  1. void wait (unique_lock& lck, Predicate pred)为有条件的等待;
  2. pred是一个可调用的对象或函数,它不接受任何参数,并返回一个可以作为bool的值;
  3. 当pred为false时wait()函数才会使线程等待(休眠),在收到其他线程通知时只有当pred返回true时才会被唤醒;
  4. 有条件等待被唤醒后,就判断第二个参数(例如lambda表达式)的值:
    • 如果为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify唤醒;
    • 如果为true,则wait返回,流程可以继续执行(此时互斥量已被锁住);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex mux;
std::unique_lock<std::mutex> uniLock(mux);
std::condition_variable condition;

condition.wait(uniLock, [this] {
// 等待消息队列不为空
if (!msgRecvQueue.empty()) return true;
return false;
});
// 如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞;
// 如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行;

future类模板

  • std::futureget()成员函数是转移数据,不能多次get;
  • std::future_status status = result.wait_for(std::chrono::seconds(几秒)) 可以卡住当前流程,等待std::async()的异步任务运行一段时间,然后返回其状态std::future_status,如果std::async()的参数是std::launch::deferred(延迟执行),则不会卡住主流程;
  • std::future_status是枚举类型,表示异步任务的执行状态,取值有std::future_status::timeout、std::future_status::ready、std::future_status::deferred;

async函数模板

  • std::async是一个函数模板,用来启动一个异步任务(启动一个异步任务,就是自动创建一个线程,并开始执行对应的线程入口函数);
  • 启动一个异步任务之后,async返回一个std::future对象,这个对象是个类模板,这个std::future对象中就含有线程入口函数所返回的结果,可以调用future对象的成员函数get()来获取结果;
  • std::future提供了一种访问异步操作结果的机制,可以理解为理解future中保存着一个值,这个值是在将来的某个时刻能够拿到;
  • std::future对象的get()成员函数会等待线程执行结束并返回结果,拿不到结果它就会一直等待;
  • std::future对象的wait()成员函数,用于等待线程返回,本身并不返回结果,这个效果和 std::thread 的join()更像;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <future>
#include <iostream>
using namespace std;
class A {
public:
int mythread(int mypar) {
cout << mypar << endl;
return mypar;
}
};

int mythread(int mypar) {
cout << mypar << endl;
cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
std::chrono::milliseconds dura(5000);
std::this_thread::sleep_for(dura);
cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
return 5;
}

int main() {
A a;
int tmp = 12;

//普通函数作为异步任务参数
std::future<int> result1 = std::async(mythread);
cout << result1.get() << endl; //卡在这里等待mythread()执行完毕,拿到结果

//类成员函数作为异步任务参数
//第二个参数是对象引用才能保证线程里执行的是同一个对象
std::future<int> result2 = std::async(&A::mythread, &a, tmp);
cout << result2.get() << endl;

return 0;
}
  • 参数std::lunch::deferred表示线程入口函数的调用会被延迟,一直到std::future的wait()或者get()函数被调用时(由主线程调用)才会执行,如果wait()或者get()没有被调用,则根本就不会创建新线程执行;
1
std::future<int> result1 = std::async(std::launch::deferred ,mythread);
  • 参数std::launch::async,在调用async函数的时候就开始创建新线程;
1
std::future<int> result1 = std::async(std::launch::async ,mythread);

packaged_task类模板

  • std::packaged_task的模板参数是各种可调用对象,通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用;
  • packaged_task包装起来的可调用对象还可以直接调用,从这个角度来讲,packaged_task对象也是一个可调用对象;
  • 通过调用get_future()成员函数得到future对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <thread>
#include <iostream>
#include <future>
using namespace std;

int mythread(int mypar) {
// ......
return 5;
}

int main() {
cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
// 我们把函数mythread通过packaged_task包装起来
// 参数是一个int,返回值类型是int
std::packaged_task<int(int)> mypt(mythread);
// t1是线程
std::thread t1(std::ref(mypt), 1);
t1.join();
// 用std::future对象接收线程入口函数的返回结果
std::future<int> result = mypt.get_future();
cout << result.get() << endl;

return 0;
}

// 可调用对象可由函数换成lambda表达式
std::packaged_task<int(int)> mypt([](int mypar) {
cout << mypar << endl;
// ......
return 5;
});

promise类模板

  • 我们能够在某个线程中给它赋值,然后我们可以在其他线程中,把这个值取出来;
  • 即通过std::promise保存一个值,在将来某个时刻把一个future绑定到这个promise上,来得到绑定的值;
  • 通过调用get_future()成员函数得到future对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <thread>
#include <iostream>
#include <future>
using namespace std;

void mythread(std::promise<int> &tmp, int clac) {
cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
std::chrono::milliseconds dura(5000);
std::this_thread::sleep_for(dura);
cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
int result = clac;
tmp.set_value(result); //结果保存到了tmp这个对象中
return;
}

vector<std::packaged_task<int(int)>> task_vec;

int main() {
std::promise<int> myprom;
std::thread t1(mythread, std::ref(myprom), 180);
t1.join(); //在这里线程已经执行完了
std::future<int> fu1 = myprom.get_future(); //promise和future绑定,用于获取线程返回值
auto result = fu1.get();
cout << "result = " << result << endl;
}

使用thread时,必须 join() 或者 detach() 否则程序会报异常

shared_future类模板

std::shared_future 的 get() 成员函数是复制数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::future<int> result = mypt.get_future();

//判断future中的值是不是一个有效值
bool ifcanget = result.valid();

//执行完毕后result_s里有值,而result里空了
std::shared_future<int> result_s(result.share());

//或通过get_future返回值直接构造一个shared_future对象
std::shared_future<int> result_s(mypt.get_future());

// 可以多次get,因为是赋值
auto myresult1 = result_s.get();
auto myresult2 = result_s.get();

atomic类模板

  • 在多线程中,如果想要执行不会被打断的程序执行片段,一般需要使用互斥量;
  • 可以把std::atomic原子操作理解成一种:不需要用到互斥量加锁的多线程并发编程方式;
  • 从效率上来说,原子操作要比互斥量的方式效率要高;
  • 互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量
  • std::atomic是用来封装某个类型的值,原子操作一般用于计数或者统计;
  • 原子操作实质上是不允许在进行原子对象操作时进行CPU的上下文切换;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
std::atomic<int> g_count = 0; //封装了一个类型为int的 对象(值)

void mythread1() {
for (int i = 0; i < 1000000; i++) {
g_count++;
}
}

int main() {
std::thread t1(mythread1);
std::thread t2(mythread1);
t1.join();
t2.join();
cout << g_count << endl; //得到2000000
system("pause");
return 0;
}

C++11多线程编程笔记
http://example.com/2022/07/12/C++11多线程编程笔记/
作者
ZYUE
发布于
2022年7月12日
更新于
2022年7月31日
许可协议