发起一个线程
c++11提供了c++风格的线程标准库,线程通过构建std::thread对象而启动,使用多线程我们需要包含头文件,该对象指明线程要运行的任务。最简单的任务就是运行一个普通函数,返回空,也不接收参数。函数在自己的线程上运行,等它一返回,线程即随之终止。
#include <iostream>
#include <thread>
void hello() {
std::cout << "hello concurrent world";
}
int main() {
std::thread t(hello);
t.join();
}
线程也可以接收其他可调用对象,比如函数对象、lamda表达式。
#include <iostream>
#include <thread>
int main() {
std::thread t([]{
std::cout << "hello concurrent world";
});
t.join();
}
等待线程完成或守护线程
若需等待线程完成,那么可以在与之关联的std::thread实例上,通过调用成员函数join()实现。只要调用了join(),隶属于该线程的任何存储空间即会因此清除,std::thread对象遂不再关联到已结束的线程。事实上,它与任何线程均无关联。其中的意义是,对于某个给定的线程,join()仅能调用一次;只要std::thread对象曾经调用过join(),线程就不再可汇合(joinable),成员函数joinable()将返回false。
用std::thread对象的成员函数detach(),会令线程在后台运行,无法与之直接通信。假若线程被分离,就无法等待它完结,也不可能获得与它关联的std::thread对象。
int main() {
std::thread t([]{
std::cout << "hello concurrent world";
});
t.detach();
}
向线程传递参数
int main() {
int a = 1;
std::thread t([]{
std::cout << a;
});
t.detach();
}
线程不能直接像如图所示的形式直接使用外部的变量,若需向新线程上的函数或可调用对象传递参数,需要向std::thread的构造函数增添更多参数。线程具有内部存储空间,参数会按照默认方式先复制到该处,新创建的执行线程才能直接访问它们。以下线程传入了两个外部变量a和b到线程空间中。
#include <iostream>
#include <thread>
void hello(const int a, const std::string& b) {
std::cout << a << b;
}
int main() {
int a = 1;
std::string b = "hello thread";
std::thread t(hello, a, b);
t.join();
}
还有一种传入外部变量的方式是使用lamda表达式的捕获,以下代码捕获了两个外部变量a和b到线程空间中使用。
#include <iostream>
#include <thread>
int main() {
int a = 1;
std::string b = "hello thread";
std::thread t([&]{
a = 2;
std::cout << a << b;
});
t.join();
}
防止悬空引用问题
当向线程中传入一个外部临时变量的引用并且以分离的方式运行时,可能外部方法已经执行完成并释放了该临时变量,此时在创建的线程中使用这个变量就会造成悬空引用的问题。
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024]; // ⇽--- ①
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer); // ⇽--- ②
t.detach();
}
在本例中,向新线程②传递的是指针buffer,指向一个局部数组变量①。我们原本设想,buffer会在新线程内转换成std::string对象,但在此完成之前,oops()函数极有可能已经退出,导致局部数组被销毁而引发未定义行为。这一问题的根源在于:我们本来希望将指针buffer隐式转换成std::string对象,再将其用作函数参数,可惜转换未能及时发生,原因是std::thread的构造函数原样复制所提供的值,并未令其转换为预期的参数类型,而提供的值是个数组,是按照指针类型传入的。
数据同步问题
在多个线程同时访问并更新同一份数据的时候,一个线程读取并修改内存里面的值,覆盖了另一个线程的更新,导致更新丢失,或者读取到了另一个线程的中间状态并修改导致恶性条件竞争。比如两个线程同时修改变量a=1的值让他递增,线程1读取a的值,然后递增把他改为2,同时线程2也要改变他的值,读取到1(线程1还未更新)然后递增改为2,此时就覆盖了线程1的修改结果。
防止恶性条件竞争有两个方向,最通用的方法是使用悲观锁,也就是互斥(mutex),一次只允许一个线程进入共享内存中修改数据,一个是乐观锁,检测多个线程的修改冲突并采取措施解决不一致问题。
在C++中,我们通过构造std::mutex的实例来创建互斥,调用成员函数lock()对其加锁,调用unlock()解锁。
#include <thread>
#include <mutex>
#include <iostream>
int main() {
int a = 1;
std::mutex mutex;
std::thread t1([&]{
mutex.lock();
for (int i = 0; i < 10; ++i) {
a = a + 1;
}
mutex.unlock();
});
std::thread t2([&]{
mutex.lock();
for (int i = 0; i < 10; ++i) {
a = a + 1;
}
mutex.unlock();
});
t1.join();
t2.join();
std::cout << a;
}
我不推荐直接调用成员函数的做法。原因是我们就必须记住,在函数以外的每条代码路径上都要调用unlock(),包括由于异常导致退出的路径。C++标准库提供了类模板std::lock_guard<>,针对互斥类融合实现了RAII手法:在构造时给互斥加锁,在析构时解锁,从而保证互斥总被正确解锁:
#include <thread>
#include <mutex>
#include <iostream>
int main() {
int a = 1;
std::mutex mutex;
std::thread t1([&]{
std::lock_guard<std::mutex> gard(mutex);
for (int i = 0; i < 10; ++i) {
a = a + 1;
}
});
std::thread t2([&]{
std::lock_guard<std::mutex> gard(mutex);
for (int i = 0; i < 10; ++i) {
a = a + 1;
}
});
t1.join();
t2.join();
std::cout << a;
}
防范死锁
有两个线程,都需要同时锁住两个互斥,才可以进行某项操作,但它们分别都只锁住了一个互斥,都等着再给另一个互斥加锁。于是,双方毫无进展,因为它们等待对方解锁互斥。上述情形称为死锁(deadlock)。为了进行某项操作而对多个互斥加锁,由此诱发的最大的问题之一正是死锁。
#include <thread>
#include <mutex>
#include <iostream>
int main() {
std::mutex mutex1;
std::mutex mutex2;
std::thread t1([&]{
std::lock_guard<std::mutex> gard(mutex1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> gard2(mutex2);
std::cout << "t1 finished";
});
std::thread t2([&]{
std::lock_guard<std::mutex> gard(mutex2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> gard2(mutex1);
std::cout << "t1 finished";
});
t1.join();
t2.join();
}
以上代码就形成了一个死锁,线程t1获得了锁mutex1,然后等待100ms,此时线程t2已获得锁mutex2,同样等待线程t1获得了锁mutex1,接下来t1等待t2释放mutex2,t2等待t1释放mutex1,两个线程一直处于等待状态形成死锁。死锁多是由于持有多个互斥导致的,防范死锁可采用如下方法:
- 按同样的顺序加锁和解锁
如果以上两个线程都是按照相同的顺序给mutex1和mutex2加锁,则可以避免死锁。更通用的,我们可以设计一种按层次加锁的数据结构,为每个互斥设计优先级,在运行期检查各互斥的优先级以保证顺序。
class hierarchical_mutex
{
std::mutex internal_mutex;
unsigned long const hierarchy_value;
unsigned long previous_hierarchy_value;
static thread_local unsigned long this_thread_hierarchy_value; ⇽--- ①
void check_for_hierarchy_violation()
{
if(this_thread_hierarchy_value <= hierarchy_value) ⇽--- ②
{
throw std::logic_error("mutex hierarchy violated");
}
}
void update_hierarchy_value()
{
previous_hierarchy_value=this_thread_hierarchy_value; ⇽--- ③
this_thread_hierarchy_value=hierarchy_value;
}
public:
explicit hierarchical_mutex(unsigned long value):
hierarchy_value(value),
previous_hierarchy_value(0)
{}
void lock()
{
check_for_hierarchy_violation();
internal_mutex.lock(); ⇽--- ④
update_hierarchy_value(); ⇽--- ⑤
}
void unlock()
{
if(this_thread_hierarchy_value!=hierarchy_value)
throw std::logic_error("mutex hierarchy violated"); ⇽--- ⑨
this_thread_hierarchy_value=previous_hierarchy_value; ⇽--- ⑥
internal_mutex.unlock();
}
bool try_lock()
{
check_for_hierarchy_violation();
if(!internal_mutex.try_lock()) ⇽--- ⑦
return false;
update_hierarchy_value();
return true;
}
};
thread_local unsigned long
hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX); ⇽--- ⑧
以上代码封装了一个具有优先级的mutex叫做hierarchical_mutex,创建时需要传入一个unsigned long value类型的优先级,每次试图加锁都会拿hierarchical_mutex的优先级和当前线程的this_thread_hierarchy_value做比较②,如果比当前线程的优先级数字小则直接抛出异常,加锁失败,否则加锁成功并更新当先线程的his_thread_hierarchy_value为锁的优先级,因此在运行期保证了加锁的顺序③。
- 使用std::lock()函数和std::scoped_lock<>模板可帮助防范死锁
死锁产生的原因多是对多个互斥加锁导致的,如果我们可以在一个线程中一次获取本次操作所需要的全部锁,则可以防范死锁。
#include <thread>
#include <mutex>
#include <iostream>
int main() {
std::mutex mutex1;
std::mutex mutex2;
std::thread t1([&]{
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
std::lock(lock1, lock2);
std::cout << "t1 finished";
});
std::thread t2([&]{
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::lock(lock1, lock2);
std::cout << "t1 finished";
});
t1.join();
t2.join();
}
以上代码使用std::lock()配合std::unique_lock实现死锁避免,std::unique_lock比std::lock_guard更加灵活,std::lock_guard对象创建后会一直持有锁,而std::unique_lock可以创建时持有锁(传入std::adopt_lock,检测到死锁会抛出异常Resource deadlock avoided)或者延迟之后再加锁(传入std::defer_lock),直到std::lock(lock1, lock2)才加锁。std::scoped_lock原理与std::lock()一致。
参考文献:
[1] 《C++并发编程实战 安东尼·威廉姆斯 第二版 》 链接: https://pan.baidu.com/s/1-KzTyw-Sp61-bXFbVL2bJA 提取码: hack 《c++并发编程实战第二版pdf》互联网中的资源,如有侵权请联系删除。