目录
一、thread类
1.thread类函数介绍
包含在头文件<thread>中
注意:thread类不支持拷贝构造和赋值重载,但是支持移动构造和移动重载
1.无参构造函数
构造一个线程对象,该对象不关联任何线程函数,即没有启动线程。noexcept用于指示函数是否可能抛出异常。
thread() noexcept;
2.有参构造函数
构造一个线程对象,并关联线程函数fn,args1、args2......是线程函数的参数
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
3.获取线程id
获取线程id,id是thread类中的公有成员变量,本质是一个结构体
id get_id() const noexcept;
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
4.判断线程是否在执行
返回true表示线程还在执行,返回false表示线程已经停止。
当线程出现以下三种情况,则线程无效,返回false:
- 默认构造函数构造的对象,即没有线程函数
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用join或detach
bool joinable() const noexcept;
5.阻塞线程
一个线程对象调用join函数后,调用线程(通常为主线程或其他工作线程)将被阻塞,直到被join的线程执行完毕并退出。
void join();
6.线程分离
一个线程调用detach函数后就会与创建其的线程分离,该线程独立在后台运行
void detach();
2.线程函数
线程关联函数也叫线程函数,可以是函数对象、函数指针、lambda表达式。如果没有提供线程函数,则该线程对象实际上没有对应任何线程。
void func1(int x)
{
std::cout << "thread1: " << x << std::endl;
}
struct F {
void operator()(int x)
{
std::cout << "thread2: " << x << std::endl;
}
};
int main()
{
std::thread t1(func1, 1);//线程函数为函数指针(函数名会隐式类型转换为函数指针)
t1.join();
std::thread t2(F(), 2);//线程函数为函数对象
t2.join();
std::thread t3([](int x) {std::cout << "thread3: " << x << std::endl; }, 3);//线程函数为lambda表达式
t3.join();
return 0;
}
3.线程函数的参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间的,即使线程函数的参数为引用类型,在线程函数内部修改参数也不影响外部的实参,因为引用的是线程栈空间的拷贝,并非外部实参。
实际上如果线程函数的参数是引用类型,不能使用传值的方式传递参数,因为线程空间的拷贝是临时变量,无法直接引用。
解决方法:
- 将线程参数类型改为const引用类型,可以引用临时变量
- 借助std::ref()函数,用于生成一个对给定对象的引用封装器,这个封装器再传递给期望接收引用的函数,但实际上传递的是一个对象,而不是直接的引用
void func(int& x, int& y)
{
x++;
y++;
std::cout << x << ":" << y << std::endl;
}
int main()
{
int x = 10;
int y = 20;
std::thread t1(func, std::ref(x), std::ref(y));
t1.join();
//std::thread t2(func, x, y);
//t2.join();
std::cout << x << ":" << y << std::endl;
return 0;
}
二、锁
1.mutex锁
mutex本质上是一个类,包含lock、unlock、try_lock函数
- lock:给互斥量加锁
- unlock:释放互斥量的锁
- try_lock:尝试给互斥量加锁,如果互斥量已经被其他线程加锁,则该线程也不会阻塞,并返回bool值表示是否给互斥量加锁成功
lock和unlock代码演示:
int x = 0;
void func(int& x, std::mutex& m)
{
int i = 0;
m.lock();
while (i++ < 1000)
{
x++;
std::cout << "x=" << x << std::endl;
}
m.unlock();
}
int main()
{
std::mutex m;
std::thread t1(func, std::ref(x),std::ref(m));
std::thread t2(func, std::ref(x), std::ref(m));
std::thread t3(func, std::ref(x), std::ref(m));
t1.join();
t2.join();
t3.join();
return 0;
}
try_lock代码演示:
void func(int& x, std::mutex& m)
{
int i = 0;
if (m.try_lock())
{
while (i++ < 1000000) ++x;
m.unlock();
}
else
{
std::cout << "无法获取到锁" << std::endl;
}
}
int main()
{
int x = 0;
std::mutex m;
std::thread t1(func, std::ref(x), std::ref(m));
std::thread t2(func, std::ref(x), std::ref(m));
std::thread t3(func, std::ref(x), std::ref(m));
t1.join();
t2.join();
t3.join();
return 0;
}
2.lock_guard
lock和unlock存在致命缺陷,如果lock之后程序发生了异常,就无法释放锁,导致锁资源泄露和死锁等问题,因此设计了lock_guard对mutex进行封装。
lock_guard是一个类模板,其在构造时加锁,在析构时释放锁。通过lock_guard创建一个锁对象,当程序异常退出时,锁对象要被销毁,调用析构函数从而释放锁。
lock_guard类源代码:
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx已经被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
lock_guard使用示例:
void func(int& x, int& y, std::mutex& m)
{
//使用{}可以指定加锁区域
{
std::lock_guard<std::mutex> lck(m);
int i = 0;
while (i++ < 10000) std::cout << "x=" << x++ << std::endl;
}
int j = 0;
while (j++ < 10000) std::cout << "y=" << y++ << std::endl;
}
int main()
{
int x = 0;
int y = 0;
std::mutex m;
std::thread t1(func, std::ref(x), std::ref(y), std::ref(m));
std::thread t2(func, std::ref(x), std::ref(y), std::ref(m));
t1.join();
t2.join();
return 0;
}
3.unique_lock
unique_lock和lock_guard基本相同,但是unique_lock比lock_auard更加灵活。除了构造函数和析构函数以外,拥有更多的成员函数:
(1)lock、unlock
unique_lock在构造时就会为互斥量加锁,但是也可以选择构造时先不加锁,后续再调用lock方法加锁,调用unlock方法解锁。
std::mutex m;
std::unique_lock<std::mutex> lck(m, std::defer_lock);
lck.lock();
std::cout << "thread #" << '\n';
lck.unlock();
(2)owns_lock
检查unique_lock是否已经对互斥量加锁,已经加锁返回true,未加锁返回false
(3)try_lock
如果构造时没有加锁,则后续就可以使用try_lock,用法和mutex锁中的try_lock相同。
三、原子操作
多线程中最主要的问题就是线程安全问题,需要对共享数据进行加锁、解锁,这样的方式存在缺陷:当一个线程访问共享数据时,其他线程就要阻塞,程序运行效率低,且容易导致死锁问题。
因此C++11中引入了原子操作类型,对这些数据类型进行的操作是不可被中断的,例如对内置类型int sum进行++操作,++操作不是原子性的可能会被中断导致程序异常,而对应的原子类型atomic_int sum的++操作是一个原子性操作,不会被中断。这使得线程间数据的同步非常高效。
C++11中为每个内置类型都设置了对应的原子类型,如下
std::atomic_int x = 0;
//int x = 0;
void func(int num)
{
for (int i = 0; i < num; ++i)
std::cout << "x=" << x++ << " ";
}
int main()
{
std::thread t1(func, 10000);
std::thread t2(func, 10000);
std::thread t3(func, 10000);
std::thread t4(func, 10000);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
当然也可以使用atomic类模板定义原子类型,原子类型属于资源型数据,仅支持构造函数,不支持拷贝构造、移动构造以及赋值重载。使用其load成员函数获取该原子类型对象的值。
atmoic<T> t; // 声明一个类型为T的原子类型变量t
std::atomic<int> x = 10;
std::cout << x.load() << std::endl;
四、条件变量
条件变量是一种同步机制,使线程能够等待某个条件成立再执行。条件变量本质是一个类,它不支持拷贝构造,包括成员函数wait、wait_for、wait_until、notify_one、notify_all。
1.成员函数介绍
(1)wait
void wait(std::unique_lock<std::mutex>& lock);
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
wiat函数有两个版本,第一个版本的wait函数用于使当前线程释放掉当前持有锁并进入阻塞状态,直到等到条件变量的通知。第二个版本的wait函数添加了一个谓词函数,用于检查某个条件是否满足,条件满足返回true,线程结束阻塞状态继续执行并重新获取锁。谓词函数可以避免虚假唤醒,线程可能因为各种情况被唤醒,而其他线程并没有通知他们,这种情况就是虚假唤醒。谓词函数在线程被唤醒后会重新检查一下条件是否满足,只有当条件真正满足时才会继续执行。
线程首次执行wait语句时,会先判断谓词函数,如果谓词函数返回值为true,则线程不阻塞继续执行;如果谓词函数返回值为false,则线程阻塞,直到线程被其他线程唤醒且谓词函数返回值为true时,才会继续执行。
版本1:
// 共享资源:起跑信号和互斥锁
std::mutex mtx;
std::condition_variable cv;
bool startSignal = false; // 起跑信号,初始为false(未收到起跑命令)
// 运动员线程函数
void athlete_start(int athleteId) {
std::unique_lock<std::mutex> lck(mtx); // 运动员锁定起跑区域(互斥锁)
// 等待起跑信号
while (!startSignal) cv.wait(lck);
// 收到起跑信号后,运动员开始行动(打印出自己的ID)
std::cout << "Athlete " << athleteId << " is running!\n";
}
// 发令员函数,用于发出起跑信号
void start_race() {
std::unique_lock<std::mutex> lck(mtx); // 发令员锁定起跑区域(互斥锁)
startSignal = true; // 发出起跑信号
cv.notify_all(); // 通知所有等待的运动员起跑
}
int main() {
// 创建10名运动员线程
std::thread athletes[10];
for (int i = 0; i < 10; ++i)
athletes[i] = std::thread(athlete_start, i);
// 宣布运动员准备就绪
std::cout << "10 athletes are ready at the starting line...\n";
// 发令员发出起跑信号
start_race();
// 等待所有运动员完成比赛(即线程结束)
for (auto& athlete : athletes) athlete.join();
return 0;
}
版本2(谓词函数):
// 共享资源:起跑信号和互斥锁
std::mutex mtx;
std::condition_variable cv;
bool startSignal = false; // 起跑信号,初始为false(未收到起跑命令)
// 运动员线程函数
void athlete_start(int athleteId) {
std::unique_lock<std::mutex> lck(mtx);
// 使用wait的第二个版本,接受一个谓词函数
cv.wait(lck, [] { return startSignal; });
// 收到起跑信号后(谓词返回true),运动员开始行动(打印出自己的ID)
std::cout << "Athlete " << athleteId << " is running!\n";
}
// 发令员函数,用于发出起跑信号
void start_race() {
std::unique_lock<std::mutex> lck(mtx);
startSignal = true; // 发出起跑信号
cv.notify_all(); // 通知所有等待的运动员起跑
}
int main() {
// 创建10名运动员线程
std::thread athletes[10];
for (int i = 0; i < 10; ++i)
athletes[i] = std::thread(athlete_start, i);
// 宣布运动员准备就绪
std::cout << "10 athletes are ready at the starting line...\n";
// 发令员发出起跑信号
start_race();
// 等待所有运动员完成比赛(即线程结束)
for (auto& athlete : athletes) athlete.join();
return 0;
}
(2)wait_for
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);
std::chrono是C++11中引入的一个库,std::chrono::duration是一个类模板,与时间单位一起使用,例如:秒std::chrono::seconds、毫秒std::chrono::milliseconds、微秒std::chrono::microseconds、纳秒std::chrono::nanoseconds。创建一个duration对象,指明时间单位。
std::chrono::seconds dur_sec(5); // 表示5秒
std::chrono::milliseconds dur_ms(100); // 表示100毫秒
wait_for函数允许线程等待一个指定的时间段,如果在这个时间段内线程被通知,线程会继续执行;如果在这个时间段结束后线程没有被通知,线程也会继续执行。
wait_for函数有两个版本,带谓词函数和不带谓词函数:
不带谓词函数的返回值类型是cv_status,cv_status是枚举类型,包括std::cv_status::no_timeout表示线程被条件变量通知唤醒并非超时、std::cv_status::timeout表示线程因为超时被唤醒、std::cv_status::spurious表示线程被虚假唤醒(既没有被条件变量通知也没有超时,要重新检查条件)
带谓词函数的返回值类型是bool,线程等待期间谓词函数检查条件,条件满足线程继续执行返回true,如果超时后条件还未满足,线程也继续执行返回false。
(3)wait_until
template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time);
template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time,
Predicate pred);
std::chrono是C++11中引入的一个库,std::chrono::time_point是一个类模板,可以获取当前系哦太敏感时间点,也可以对时间点进行算术运算,加减时间段。
std::chrono::time_point<std::chrono::system_clock> tp = std::chrono::system_clock::now();
auto duration = std::chrono::hours(1); // 1小时
auto future_tp = tp + duration; // 1小时后的时间点
auto past_tp = tp - duration; // 1小时前的时间点
wait_until函数和wait_for函数类似,唯一不同点在于wait_until函数允许线程等待的是一个指定的时间点,并非时间段。wait_until也有两个重载版本,不带谓词函数和带谓词函数、
代码示例:
谓词函数版本
// 互斥锁,用于保护共享资源(起跑准备就绪状态)
std::mutex mtx;
// 条件变量,用于通知运动员起跑准备就绪
std::condition_variable cv;
// 共享资源:起跑是否准备就绪
bool ready = false;
// 运动员线程:等待起跑信号,如果超时则放弃比赛
void athlete_wait_for_start(int athlete_id) {
std::unique_lock<std::mutex> lock(mtx);
// 运动员等待起跑信号,最多等待5秒
if (!cv.wait_for(lock, std::chrono::seconds(5), [] { return ready; })) {
std::cout << "运动员 " << athlete_id << " 超时未收到起跑信号,放弃比赛。\n";
}
else {
std::cout << "运动员 " << athlete_id << " 收到起跑信号,开始比赛。\n";
}
}
// 发令员线程:模拟一些准备工作,然后发出起跑信号
void starter_prepare_and_signal() {
std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟发令员准备工作(如检查设备、召集运动员等)
std::unique_lock<std::mutex> lock(mtx);
ready = true; // 设置起跑准备就绪
cv.notify_all(); // 通知所有等待的运动员起跑
}
int main() {
// 创建运动员线程,等待起跑信号
std::thread athlete1(athlete_wait_for_start, 1);
// 创建发令员线程,模拟准备并发出起跑信号
std::thread starter(starter_prepare_and_signal);
// 等待运动员和发令员线程完成
athlete1.join();
starter.join();
return 0;
}
不带谓词函数版本
// 运动员线程:等待起跑信号,如果超时则放弃比赛
void athlete_wait_for_start(int athlete_id) {
std::unique_lock<std::mutex> lock(mtx);
// 运动员等待起跑信号,最多等待5秒
if (cv.wait_for(lock, std::chrono::seconds(5)) == std::cv_status::timeout) {
std::cout << "运动员 " << athlete_id << " 超时未收到起跑信号,放弃比赛。\n";
}
else {
std::cout << "运动员 " << athlete_id << " 收到起跑信号,开始比赛。\n";
}
}
2.线程交替打印奇偶数代码
//两个线程交替打印1~99的奇数偶数
void alternate_print()
{
std::mutex mtx;//互斥锁
std::condition_variable cv;//条件变量
int n = 100;
bool flag = true;
auto even_print = [&]() {
int i = 0;
while (i < n)
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [&]() {return !flag; });
if (i % 2 == 0) std::cout << i << std::endl;
i++;
flag = true;
cv.notify_one();
}
};
auto odd_print = [&]() {
int i = 0;
while (i < n)
{
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [&]() {return flag; });
if (i % 2 == 1) std::cout << i << std::endl;
i++;
flag = false;
cv.notify_one();
}
};
std::thread t1(even_print);
std::thread t2(odd_print);
t1.join();
t2.join();
}
int main()
{
alternate_print();
return 0;
}