C++11
中std
命名空间将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;
}
引用和指针作为参数
在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;
}
线程锁
当多个线程访问同一资源时(比如同时读写同一个变量),为了保证数据的一致性,最简单的方式就是使用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;
}
自动锁
显式的加锁和解锁会导致一些问题,比如忘记解锁或者请求加锁的顺序不正确,进而产生死锁。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
互斥锁不管是对读操作还是写操作,都会独占这个变量,这对只有多线程读操作时会出现性能损耗。STL
和Boost
都提供了 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
引入了atomic
对int
、char
、bool
等基础数据结构进行了原子性封装,在多线程环境中,对std::atomic
对象的访问不会造成竞争-冒险。利用std::atomic
可实现数据结构的无锁设计。
所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
在以往的C++
标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel
的pthread
来实现。在新标准C++11
,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如atomic_bool
,atomic_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;
}
条件变量
条件变量(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;
}