参考文档:https://zh.cppreference.com/w/cpp/thread
C++11把线程相关的系统调用封装成了 std::thread 。我们看下构造函数
thread() noexcept; //构造一个空线程
thread( thread&& other ) noexcept; //支持移动构造
template< class F, class... Args >
explicit thread( F&& f, Args&&... args ); //传参构造
thread( const thread& ) = delete; //禁用拷贝构造
线程对象会在底层关联一个线程,如果构造了一个空线程,那么该对象在底层不会关联任何线程。
传参构造的第一个参数是可调用对象,参数包是可调用对象的参数包。
传参构造是以值拷贝的方式传递给底层的系统调用,所以,如果传引用传参就会变成传值传参,所以我们需要借助 std::ref 来解决该问题,如下代码
#include
void ThreadFunc1(int& x) { x += 10; }
void ThreadFunc2(int* x) { *x += 10; }
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
std::thread 类禁掉了拷贝构造,说明一个线程不允许被另一个线程拷贝。
但是 std::thread 类支持移动构造,这是为什么呢?这是为了方便空线程转移资源。如下代码
int sum(int a, int b) { return a + b; }
thread t1;
t1 = thread(sum, 1, 3);
先创建空线程 t1 。thread(sum, 1, 3) 是匿名对象,具有右值属性,所以该线程的资源会被转移给 t1。支持移动构造是为了我们方便写线程池。
注:线程也支持移动赋值
thread& operator=( thread&& other ) noexcept;
如果我们向先创建一批线程但不执行任何函数,可以这样写
vector<thread> threads(10);
当我们想用线程池中的某个线程时,只需用传参构造匿名对象,然后把匿名对象的资源转移给要执行的线程。
get_id() :获取线程id
jionable() :线程是否还在执行
jion() :该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行
detach():该函数调用后,线程的“死活”就与主线程无关了
https://zh.cppreference.com/w/cpp/atomic/atomic
我们看如下程序,两个线程对同一个整数加加一万次
int main() {
int x = 0, n = 10000;
thread t1([&, n]()mutable { while(n--) x++; });
thread t2([&, n]()mutable { while(n--) x++; });
t1.join();
t2.join();
cout << x << endl;
return 0;
}
结果如下
很明显有线程安全, 如果我们不想加锁,用 std::atomic 可以让类型的属性具有原子性。
atomic<int> x = 0;
这样在加加 x 时,永远是线程安全的
https://zh.cppreference.com/w/cpp/thread/mutex
某一时刻有且只有一个线程可以锁住互斥量,互斥量所保护的临界区的资源就是原子的
成员函数
lock | 锁定互斥体,若互斥体不可用则阻塞 |
---|---|
try_lock | 尝试锁定互斥体,若互斥体不可用则返回 |
unlock | 解锁互斥体 |
void task(int& x, mutex& lock) {
int n = 100000;
lock.lock();
while (n--) x++;
lock.unlock();
}
int main() {
mutex lock;
int x = 0;
thread t1(task, ref(x), ref(lock));
thread t2(task, ref(x), ref(lock));
t1.join();
t2.join();
cout << x << endl;
return 0;
}
这个思想和智能指针类似,用类的构造函数和析构函数管理资源。
我们可以用构造函数加锁,析构函数解锁,这样会很大程度避免死锁问题。
C++11 的线程库提供了 std::lock_guard 和 std::unique_lock,这两个类模板可以用构造和析构管理加锁和解锁
如下代码,可以用 {} 指定局部作用域
void task(int& x, mutex& lock) {
int n = 100000;
{
lock_guard<mutex> lock_guard(lock);
while (n--) x++;
}
}
int main() {
mutex lock;
int x = 0;
thread t1(task, ref(x), ref(lock));
thread t2(task, ref(x), ref(lock));
t1.join();
t2.join();
cout << x << endl;
return 0;
}
https://zh.cppreference.com/w/cpp/thread/condition_variable
条件变量需要被互斥量保护,所以条件变量需要关联一个互斥量,如果某个条件成立,对互斥量解锁并阻塞到条件变量上。
下面是一个很经典的问题:创建两个线程,一个线程打印基数,一个线程打印偶数,并且交替打印
代码如下
int main() {
condition_variable cv;
mutex m;
bool falg = true;
int count = 0, n = 10;
thread t1([&]() {
int i = 1;
while (i < n) {
unique_lock<mutex> lock(m);
cv.wait(lock, [&]() { return falg == true; });
cout << "打印基数的线程" << std::this_thread::get_id() << ": " << i << endl;
i += 2;
falg = false;
cv.notify_one();
}
});
thread t2([&]() {
int i = 0;
while (i < n) {
unique_lock<mutex> lock(m);
cv.wait(lock, [&]() { return falg == false; });
cout << "打印偶数的线程" << std::this_thread::get_id() << ": " << i << endl;
i += 2;
falg = true;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}