在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。
一、创建线程
头文件:#include <thread>
线程类:std::thread
构造函数:
1)thread() noexcept;
默认构造函,构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
2)template< class Function, class... Args > (常用)
explicit thread(Function&& fx, Args&&... args );
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数。
3)thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
4)thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数:
thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。
注意:
- 先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。
- 线程的任务函数返回后,子线程将终止。
- 如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。
示例:
#include<iostream>
#include<thread>
using namespace std;
//普通函数
void func(int num,const string &str)
{
for(int i=0;i<10;i++)
{
cout<<num<<str<<endl;
this_thread::sleep_for(chrono::seconds(1));//休眠一秒
}
}
//仿函数
class MyThread1
{
public:
void operator()(int num,const string &str)
{
for(int i=0;i<10;i++)
{
cout<<num<<str<<endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
};
//静态成员函数
class MyThread2
{
public:
static void func(int num,const string &str);
};
void MyThread2::func(int num,const string &str)
{
for(int i=0;i<10;i++)
{
cout<<num<<str<<endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
//非静态成员函数
class MyThread3
{
public:
void func(int num,const string &str)
{
for(int i=0;i<10;i++)
{
cout<<num<<str<<endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
};
int main()
{
//lambda函数
auto lam_func = [](int num,const string&str)
{
for(int i=0;i<10;i++)
{
cout<<num<<str<<endl;
this_thread::sleep_for(chrono::seconds(1));
}
};
//普通函数创建线程
//thread t1(func,1,"普通函数");
//仿函数创建线程
//thread t2(MyThread1(),2,"仿函数");
//静态成员函数创建线程
//thread t3(MyThread2::func,3,"静态成员函数");
//非静态成员函数创建线程
//MyThread3 myth;//必须先创建对象,且对象生命周期比线程长
//thread t4(&MyThread3::func,&myth,4,"非静态成员函数");//第三个参数必须填对象的this指针,否则会拷贝对象
//lambda函数创建线程
thread t5(lam_func,5,"lambda函数");
//主线程
for(int i=0;i<10;i++)
{
cout<<"主线程\n";
this_thread::sleep_for(chrono::seconds(1));
}
//回收线程
//t1.join();
//t2.join();
//t3.join();
//t4.join();
t5.join();
return 0;
}
二、线程资源的回收
虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。
回收子线程的资源有两种方法:
1)在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
2)在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。
示例:
#include <iostream>
#include <thread> // 线程类头文件。
#include <windows.h> // Sleep()函数需要这个头文件。
using namespace std;
// 普通函数。
void func(int num, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << num << str << endl;
Sleep(1000); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "线程1");
thread t2(func, 8, "线程2");
t1.detach(); t2.detach(); // 分离子线程。
//cout << "任务开始。\n";
//for (int ii = 0; ii < 12; ii++) {
// cout << "执行任务中......\n";
// Sleep(1000); // 假设执行任务需要时间。
//}
//cout << "任务完成。\n";
//t1.join(); // 回收线程t1的资源。
//t2.join(); // 回收线程t2的资源。
Sleep(12000); //子线程分离出去后主线程不能退出,等待子线程运行完再退出
return 0;
}
三、this_thread的全局函数
C++11提供了命名空间this_thread来表示当前线程,该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。
1)get_id()
thread::id get_id() noexcept;
该函数用于获取线程ID,thread类也有同名的成员函数。
2)sleep_for() VS Sleep(1000) Linux sleep(1)
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
该函数让线程休眠一段时间。
3)sleep_until() 2022-01-01 12:30:35
template <class Clock, class Duration>
void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
该函数让线程休眠至指定时间点。(可实现定时任务)
4)yield()
void yield() noexcept;
该函数让线程主动让出自己已经抢到的CPU时间片。
5)thread类其它的成员函数
void swap(std::thread& other); // 交换两个线程对象。
static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。
The interpretation of this value is system- andimplementation- specific, and may not be exact, but just an approximation.
Note that this does not need to match the actualnumber of processors or cores available in the system: A system can supportmultiple threads per processing unit, or restrict the access to its resourcesto the program.
If this value is not computable or well defined,the function returns 0.
示例:
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;
// 普通函数。
void func(int num, const string& str) {
cout << "子线程:" << this_thread::get_id() << endl;
for (int ii = 1; ii <= 3; ii++)
{
cout << num << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 1, "子线程。");
thread t2(func, 2, "子线程。");
cout << "主线程:" << this_thread::get_id() << endl;
cout << "线程t1:" << t1.get_id() << endl;
cout << "线程t2:" << t2.get_id() << endl;
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
return 0;
}
四、call_once函数
在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。
在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次。
头文件:#include <mutex>
template< class callable, class... Args >
void call_once( std::once_flag& flag, Function&& fx, Args&&... args );
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。
示例:
#include <iostream>
#include <thread> // 线程类头文件。
#include <mutex> // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;
once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
cout << "once_func() bh= " << bh << ", str=" << str << endl;
}
// 普通函数。
void func(int bh, const string& str) {
call_once(onceflag,once_func,0, "开始。");
for (int ii = 1; ii <= 3; ii++)
{
cout << bh << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "普通函数");
thread t2(func, 8, "普通函数");
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
return 0;
}
五、native_handle函数
C++11定义了线程标准,不同的平台和编译器在实现的时候,本质上都是对操作系统的线程库进行封装,会损失一部分功能。
为了弥补C++11线程库的不足,thread类提供了native_handle()成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。
示例:
#include <iostream>
#include <thread>
#include <pthread.h> // Linux的pthread线程库头文件。
using namespace std;
void func() // 线程任务函数。
{
for (int ii=1;ii<=10;ii++)
{
cout << "ii=" << ii << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
thread tt(func); // 创建线程。
this_thread::sleep_for(chrono::seconds(5)); // 休眠5秒。
pthread_t thid= tt.native_handle(); // 获取Linux操作系统原生的线程句柄。
pthread_cancel(thid); // 取消线程。
tt.join(); // 等待线程退出。
return 0;
}
六、线程安全
同一进程中的多个线程共享该进程中的全部系统资源,当多个线程同时调用一个资源时会产生冲突。
关于线程安全需知道的几个概念:
1)顺序性:程序按照代码的先后顺序执行,但CPU为了提高效率,可能会进行优化,按更高效的顺序执行.
2)可见性:线程操作共享变量时,会将该变量从内存加载到CPU缓存中,修改该变量后,CPU会立即更新缓存,但不一定会立即将它写回内存.这时其他线程访问该变量时还是旧值. 当多个线程并发访问共享变量时,一个线程对其修改,其他线程能立马看见.
3)原子性:CPU执行指令的顺序为:读取指令、读取内存、执行指令、写回内存。 原子操作的意思就是一个操作(可能含多个步骤)要么全部执行,要么都不执行.
如何保证线程安全:
1.volatile关键字: 1)保证内存变量的可见性. 2)禁止代码优化(保证顺序性).
2.原子操作(原子类型) 详情见该文章:(6条消息) C++11新特性——互斥锁、条件变量、原子类型_SatoshiGogo的博客-CSDN博客
3.线程同步(锁) 详情见该文章:(6条消息) C++11新特性——互斥锁、条件变量、原子类型_SatoshiGogo的博客-CSDN博客
示例:
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;
int aa = 0; // 定义全局变量。
// 普通函数,把全局变量aa加1000000次。
void func() {
for (int ii = 1; ii <= 1000000; ii++)
aa++;
}
int main()
{
// 用普通函数创建线程。
thread t1(func); // 创建线程t1,把全局变量aa加1000000次。
thread t2(func); // 创建线程t2,把全局变量aa加1000000次。
//两个线程操作不安全,加不到2000000
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
cout << "aa=" << aa << endl; // 显示全局变量aa的值。
return 0;
}