看前须知:如果对线程不了解的,可以先去看Linux---多线程(上),(下)这两篇文章
那里主要讲了线程的一些基础概念和底层相关理解,对我们阅读这篇文章会有所帮助
一、thread --- 线程
1、thread相关接口介绍
函数接口 | 功能说明 |
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn, args1, args2, ...) | 构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数 |
get_id() | 获取线程id |
joinable() | 查看线程是否是连接状态,与detch后的线程的分离状态相对应 |
join() | 该函数调用后会阻塞等待线程结束 如果线程是默认构造的线程对象 / 已经被detach / 已经被 join,再调用join会出错 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
下面是一个简单的创建线程的代码
#include<thread>
#include<iostream>using namespace std;void Print(size_t n)
{for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;
}int main()
{thread t(Print, 10);// t.detach();if (t.joinable())t.join();cout << t.joinable() << endl;return 0;
}
注意:
- 线程是操作系统中的概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
- 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
- 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针、lambda表达式、仿函数、包装器,如下
#include<iostream> void Print(size_t n) {cout << this_thread::get_id() << " : "; // 获取当前线程的线程idfor (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl; }struct print {void operator()(size_t n){cout << this_thread::get_id() << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;} };int main() {thread t1(Print, 10);Sleep(1);thread t2(print(), 20);Sleep(1);thread t3([](size_t n) {cout << this_thread::get_id() << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;}, 30);Sleep(1);function<void(size_t)>f = [](size_t n) {cout << this_thread::get_id() << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;};thread t4(f, 40);t1.join();t2.join();t3.join();t4.join();return 0; }
- thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。
void Print(size_t n, string s) {cout << s << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl; }int main() {int n = 10;vector<thread> vthd(n);size_t j = 0;for (auto& thd : vthd){// 移动赋值 --- 临时变量是将亡值thd = thread(Print, 10, "线程" + to_string(j++));Sleep(1);// 休眠的目的是为了让打印出来的数据看起来不乱}for (auto& thd : vthd){thd.join();}thread t1(Print, 10, "zwxs");// thread t2(t1); // 错,thread不支持拷贝构造thread t2(move(t1)); //thread支持移动构造t2.join();return 0; }
- 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
2、线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参,如果不理解,可以直接记住结论,即创建线程时传引用得加std::ref()
void Print(size_t n, int & x)
{for (size_t i = 0; i < n; i++){x++;}
}void Print1(size_t n, int* x)
{for (size_t i = 0; i < n; i++){(*x)++;}
}int main()
{int x = 0;// thread t(Print, 10, x); // 会报错thread t(Print, 10, ref(x)); // std::ref() 帮助我们传递引用t.join();cout << x << endl;thread t1(Print1, 10, &x); // 可以直接传指针,通过指针来修改值t1.join();cout << x << endl;return 0;
}
3、this_thread命名空间
this_thread
是 C++11 引入的一个命名空间,它位于 std
下,并提供了一组函数,用于操作当前执行的线程。这个命名空间的目的是为开发者提供一种便捷的方式来获取和管理当前线程的信息和行为。
get_id
:这个函数用于获取当前线程的线程 ID。线程 ID 是一个唯一标识线程的整数值,通过它可以在程序中区分和追踪不同的线程。yield
:这个函数用于让当前线程主动放弃处理器的使用权,使得其他线程有机会执行。这是一种线程间的协作机制,有助于实现更高效的线程调度。sleep_for
:这个函数使当前线程进入休眠状态,直到指定的时间段过去,这可以用于控制线程的执行节奏,或者在某些情况下,等待某些条件成立。sleep_until
:这个函数使当前线程进入休眠状态,直到达到指定的时间点。它允许线程在特定的时间唤醒并执行。
与时间相关的函数可以结合<chrono>头文件下的相关函数使用,如hours,minutes,seconds等。
void Print(size_t n)
{for (size_t i = 0; i < n; i++){cout << i << " ";this_thread::sleep_for(chrono::seconds(1));}
}
二、mutex --- 锁
1、std::mutex
函数接口 | 功能说明 |
lock() | 阻塞加锁 |
try_lock() | 非阻塞加锁 |
unlock() | 解锁 |
int main()
{int x = 0;int n = 1000;thread t1([&]() {for (int i = 0; i < n; i++){x++;}});thread t2([&]() {for (int i = 0; i < n; i++){x++;}});t1.join();t2.join();cout << x << endl;return 0;
}
很明显,上面的代码是线程不安全的,我们需要给线程加锁,代码如下
int main()
{int x = 0;int n = 1000;mutex mtx;thread t1([&]() {for (int i = 0; i < n; i++){mtx.lock();x++;mtx.unlock();}});thread t2([&]() {for (int i = 0; i < n; i++){mtx.lock();x++;mtx.unlock();}});t1.join();t2.join();cout << x << endl;return 0;
}
这里细心的读者可能已经发现了一个问题:为什么这里能"传"引用?注意:这里不是传参,而是lambda表达式的捕获列表,可以理解为两者底层走的不是一个逻辑,所以这里可以,至于具体底层是如何走的,有兴趣的可以自己去查查看。
2、std::recursive_mutex
3、std::timed_mutex
4、std::recursive_timed_mutex
具有recursive_mutex和timed_mutex的特性
出现死锁的情况,如下
void func(int x)
{if (x%2)throw exception("异常");elsecout << "func()" << endl;
}int main()
{mutex mtx;int y;thread t1([&]() {try {for (int i = 0; i < 10; i++) {mtx.lock();y++;func(i);mtx.unlock();}}catch (const exception& e) {cout << e.what() << endl;}});thread t2([&]() {try {for (int i = 0; i < 10; i++) {mtx.lock();y++;func(i);mtx.unlock();}}catch (const exception& e) {cout << e.what() << endl;}});t1.join();t2.join();return 0;
}
针对上面的情况,我们需要有一个能自动释放的锁,类似智能指针,库给我们提供了lock_guard和unique_lock用来封装锁,不需要我们去手动释放锁。
thread t1([&]() {try {for (int i = 0; i < 10; i++) {lock_guard<mutex> lock(mtx);y++;func(i);}}catch (const exception& e) {cout << e.what() << endl;}
});
lock_guard和unique_lock的区别:
lock_guard只支持构造和析构,没有其他功能
unique_lock能支持手动的加锁和解锁,并且能用时间进行控制
三、condition_variable --- 条件变量
不了解的可以先去看Linux---多线程(下)
常用的三个函数接口:
函数接口 | 功能说明 |
void wait(unique_lock<mutex>& lck) | 等待条件就绪,再往下执行,会先释放申请到的锁,故只能传unique_lock,lock_guard不支持手动加锁和解锁 |
void notify_one() | 唤醒在条件变量的等待队列中的一个线程,需要重新加锁 |
void notify_all() | 唤醒在条件变量的等待队列中的所有线程,需要重新加锁 |
int main()
{int x = 1;condition_variable cv;mutex mtx;thread t1([&]() {for (int i = 0; i < 10; i++) {unique_lock<mutex> lock(mtx);while (x%2==0)cv.wait(lock);cout << this_thread::get_id() << " : " << x++ << endl;cv.notify_one();}});thread t2([&]() {for (int i = 0; i < 10; i++) {unique_lock<mutex> lock(mtx);while (x%2)cv.wait(lock);cout << this_thread::get_id() << " : " << x++ << endl;cv.notify_one();}});t1.join();t2.join();return 0;
}
四、atomic
C++11中的<atomic>
库提供了对原子操作的支持,这些操作在多线程环境中是线程安全的。原子操作是不可中断的操作,即在执行完毕之前不会被其他线程打断。通过使用原子操作,我们可以避免使用互斥量(mutexes)和条件变量(condition variables)等同步原语,从而在某些情况下提高性能并简化代码
比如上面用锁保证x++的线程安全,其实比较浪费资源,因为申请锁一旦失败,线程就会阻塞,需要让出cpu,切换上下文数据等,而我们加锁只是为了执行x++这一条语句,显然很不值得,这里我们就可以用atomic中的函数,进行原子操作,不需要进入阻塞,代码如下
int main()
{atomic<int> x = 0;int n = 1000;//mutex mtx;thread t1([&]() {for (int i = 0; i < n; i++){x++;// 这里的++用的是运算符重载}});thread t2([&]() {for (int i = 0; i < n; i++){x++;// 这里的++用的是运算符重载}});t1.join();t2.join();std::cout << x << std::endl;return 0;
}
可以简单说明一下,它用的无锁编程---利用原子操作实现锁的功能。上面的++操作符就是用CAS (compare and swap)这个原子操作实现的(CAS在不同的语言中都会有对应的函数)
/*伪代码
int x = 0;
int old,newval;
do
{old = x;newval = old + 1;
}while(!CAS(&x,&old,newval))
//看x在内存中的值是否和old相同,如果相同*x=newval,返回true,否则返回false
*/
如果对无锁编程感兴趣可以去查查文档,这里就不多做介绍了,这个对编程能力要求有点高,建议最好不要轻易去写。