『 C++ 』线程库
文章目录
- 线程库
- 线程的创建与销毁
- 成员函数
- this_thread 命名空间
- 线程的引用传值
- 互斥锁
- 互斥锁的基本操作
- 递归锁(可重入锁)
- 定时互斥锁
- 互斥锁管理器与互斥锁抛异常所引发的死锁问题
- 条件变量
- 条件变量的等待
- 条件变量的唤醒
- 两个线程交替打印奇偶数
线程库
C++
标准库提供了一套完整的线程支持库,从C++11
开始引入,并在后续版本中不断增强;
这些库包括用于创建和管理线程的类,以及多种并发工具,如互斥锁,条件变量,原子操作等;
在C++
中的线程库根据不同的操作系统平台其底层支持不同,以Linux
为例用的线程库为原生pthread
线程库提供,即在Linux
下使用C++
的线程库时必须链接对应的pthread
;
g++ -o a.out main.cc -lpthread -std=c++11
pthread
线程库是一个POSIX
标准的线程库(POSIX
指可以指操作系统接口),可适用于Linux
,Unix
,MacOS X
等操作系统;
在Windows
下其也提供了一个单独的线程库,Windows
下使用C++
的线程库即为Windows
线程库的封装;
Linux
下C++
线程库为对pthread
线程库的封装;
语言层面和系统层面进行解耦合,在使用C++
提供的线程库时只需要包含<thread>
头文件即可(Linux
下还是需要在编译时链接pthread
库,否则无法使用);
将操作系统提供的线程库通过封装为一个类(std::thread
)以面向对象;
其提供了一系列的成员函数用于线程的使用(参考 [ Link - C++ Reference thread ]);
线程的创建与销毁
C++
线程库提供了一系列的构造函数用于创建线程;
-
带参构造
template <class Fn, class... Args> explicit thread (Fn&& fn, Args&&... args);
线程库提供了一个万能引用与带有一个可变参数包的构造函数用于构造一个带参数的线程对象实例;
-
Fn&& fn
该参数是一个万能引用参数,可以根据传递的数据属性自动推导其左值或是右值属性;
这个参数表示需要传入一个可调用对象,这个可调用对象可以是仿函数,函数指针,
bind
绑定后的函数对象,function
包装器包装后的函数对象以及Lambda
表达式等;所传入的可调用对象将称为该线程的入口点;
-
Args&&... args
这个可变参数包同样的是一个万能引用参数,既能够自动推导所传入参数的左右值属性,同时作为可变参数包可自动推导所传入的参数类型;
这个可变参数包表示传入给可调用对象
Fn&& fn
的参数;
void Print(int n) { // 定义一个打印函数 for (int i = 0; i < n; ++i) { cout << n << " "; } cout << endl; } int main() { thread td1(Print, 5); // 线程 td1 调用 Print 作为可调用对象 thread td2( [](int n) { // 线程 td2 调用 lambda 表达式作为可调用对象 for (int i = 0; i < n; ++i) { cout << "td2" << endl; } },3); // join 用于等待线程结束 td1.join(); td2.join(); return 0; }
当一个线程带参构造时对应的可调用对象将成为该线程的入口点,对应的存在可调用对象时线程对象在实例化后将自行启动;
-
-
无参构造
thread() noexcept;
线程库提供了一个无参构造函数用于实例化一个空的线程对象;
空的线程对象可通过移动构造或是移动赋值将一个非空线程实例资源转移至该空线程实例上以进行使用;
thread td; // 实例化一个空的线程对象
实例化的空线程不会启动;
-
拷贝构造
thread (const thread&) = delete;
线程的拷贝是一个危险的动作,
thread
线程不支持任何拷贝操作(无论是拷贝构造还是拷贝赋值); -
移动构造
thread (thread&& x) noexcept;
线程支持移动(移动构造或是移动拷贝)操作,即将一个将亡值线程对象中的属性资源转移给另一个线程;
int main() { thread td1( [](int n) { for (int i = 0; i < n; ++i) { cout << "td2" << endl; } }, 3); thread td2(move(td1)); // 移动构造 thread td3; td3 = move(td2); // 移动赋值 td3.join(); return 0; }
在使用移动时必须保证接收移动资源的线程实例必须是一个空线程,否则会因为目标线程对象已经管理了一个活动线程而导致资源双重管理或丢失产生的未定义行为;
int main() { // 未定义行为 thread td1(Print, 5); thread td2( [](int n) { for (int i = 0; i < n; ++i) { cout << "td2" << endl; } },3); td2 = move(td1); // 线程 td2 为非空线程 td2.join(); return 0; } /* 运行结果为: $ ./thread terminate called without an active exception Aborted # 运行崩溃 */
-
析构函数
~thread();
析构函数用于销毁该线程实例;
在调用析构函数时必须保证该线程实例是
join
后的,否则将存在未定义行为;
成员函数
-
thread::detach()
void detach();
该成员函数用于分离一个线程,被分离的线程不需要显式
join
,将会成为一个不可联结的线程;void Print(int n) { for (int i = 0; i < n; ++i) { cout << i << " "; } cout << endl; } int main() { thread t1(Print, 3); t1.detach(); sleep(1); // 防止主线程过早结束 return 0; } /* 运行结果: $ ./thread 0 1 2 */
-
thread::get_id()
id get_id() const noexcept;
该函数用于返回该线程的线程
ID
;其中类型
id
是C++
线程库中自定义的一个类型,为一个自定义类型;在
Linux - CentOS7
中被一个哈希表所存储,其中结构体中保存着该线程的基本属性,通过重载operator<<
流插入实现打印哈希表对应的key
值,即线程id
;int main() { thread t1(Print, 3); printf("printf id : %llu\n", t1.get_id()); cout << "cout id : " << t1.get_id() << endl; cout << typeid(t1.get_id()).name() << endl; // 打印 id 的类型名 t1.join(); return 0; } /* 运行结果: // Linux $ ./thread printf id : 140061102868224 cout id : 140061102868224 NSt6thread2idE 0 1 2 // Windows printf id : 28396 cout id : 28396 class std::thread::id 0 1 2 */
-
thread::join()
void join();
该函数用于等待线程结束;
已经被
detach()
的线程不能使用join
进行等待,否则会出现未定义行为;int main() { thread t1([] { cout << "t1" << endl; }); thread t2([] { cout << "t2" << endl; }); t1.detach(); // t1 进行 detach // 对两个线程进行 join t1.join(); t2.join(); return 0; } /* 运行结果: $ ./thread t2 // t2 正常 terminate called after throwing an instance of 'std::system_error' what(): Invalid argument Aborted // t1 已经被 detach , 再次 join 时出现未定义行为 */
运行结果中
t1
正常运行,t2
已经被detach
,对t2
进行join
时出现未定义行为,程序异常退出; -
thread::joinable()
bool joinable() const noexcept;
该函数用于判断该线程是否为一个可联结(未
join/detach
且已经启动)线程,是则返回true
,否则返回false
;int main() { thread t1([] { cout << "t1" << endl; }); thread t2([] { cout << "t2" << endl; }); t1.detach(); cout << t1.joinable() << endl; cout << t2.joinable() << endl; t2.join(); return 0; } /* 运行结果: $ ./thread 0 1 t2 t1 */
运行结果
t1
被detach
后不为可联结线程返回false
,t2
未join
且未detach
为一个可联结线程,返回true
; -
thread::swap()
void swap (thread& x) noexcept;
该函数用于交换两个线程;
int main() { thread t1([] { cout << "t1" << endl; }); thread t2([] { cout << "t2" << endl; }); t1.swap(t2); t1.join(); t2.join(); return 0; }
-
operator=()
重载了赋值操作符;
thread& operator= (thread&& rhs) noexcept; thread& operator= (const thread&) = delete;
不支持拷贝赋值,支持移动赋值;
可通过其他容器,无参构造和移动赋值来实现管理多个线程;
void Print(const string& str, int n) { cout << str << n << endl; } int main() { int n = 5; vector<thread> vths(n); for (int i = 0; i < n; ++i) { vths[i] = thread(Print, "线程", i); // 其中 thread(Print, "线程", i) 为将亡值 } for(auto& th:vths){ th.join(); } return 0; } /* 运行结果为: $ ./thread 线程1 线程3 线程4 线程2 线程0 */
this_thread 命名空间
std::this_thread
命名空间是C++
标准库中的一个命名空间,提供了一系列与当前执行线程相关的函数;
-
this_thread::get_id()
thread::id get_id() noexcept;
该函数用于获取当前执行线程中的线程
id
;void Print(const string& str, int n) { cout << str << n << " the id : " << this_thread::get_id() << endl; // 获取当前线程 id } int main() { int n = 5; vector<thread> vths(n); for (int i = 0; i < n; ++i) { vths[i] = thread(Print, "线程", i); } for (auto& th : vths) { th.join(); } return 0; } /* 运行结果为: $ ./thread 线程3 the id : 140560757815040 线程1 the id : 140560774600448 线程4 the id : 140560749422336 线程2 the id : 140560766207744 线程0 the id : 140560782993152 */
-
this_thread::sleep_for()
template <class Rep, class Period> void sleep_for (const chrono::duration<Rep,Period>& rel_time);
该函数用于阻塞当前线程一段时间;
其中
chrono
为一个命名空间,提供一系列用于处理时间和时钟的工具和类型; -
this_thread::sleep_until()
template <class Clock, class Duration> void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
该函数用于阻塞当前执行线程时间至
abs_time
时间点; -
this_thread::yield()
void yield() noexcept;
该函数的作用为在多线程环境中允许当前线程主动让出处理器的执行权使得操作系统调度其他线程执行;
当一个线程调用该函数时将会提示操作系统当前线程愿意让出处理器的时间片,允许调度器切换到另一个线程执行以提升多线程程序的整体性能和响应性;
线程的引用传值
在对线程进行引用传值时需要在传值中使用ref()
以确保所传递的引用被完美转发;
因为在进行传引用传值时所传数据不会直接被线程的入口函数接收,而是首先被线程thread
的构造函数接收;
使用ref
以确保被构造函数接收后还能保持其左值或右值引用属性向下传递给线程的入口函数;
void Func1(mutex &mtx, int &x) {
for (int i = 0; i < 10000; ++i) {
mtx.lock();
++x;
mtx.unlock();
}
cout << this_thread::get_id() << " : Func1" << endl;
}
int main() {
mutex mtx;
int x = 0;
thread t1(Func1, ref(mtx), ref(x));
thread t2(Func1, ref(mtx), ref(x));
t1.join();
t2.join();
cout << x << endl;
return 0;
}
/*
运行结果:
$ ./thread
139676657420032 : Func1
139676665812736 : Func1
20000
*/
互斥锁
C++在引入线程库时也引入了对应的用于同步操作的锁,即mutex
,用于支持多线程情况下的相关操作;
如一些需要保护临界资源避免产生竞态条件的情况;
mutex
需要包含<mutex>
头文件;
互斥锁的基本操作
-
互斥锁的创建
在
C++
中互斥锁也被封装为一个类,在使用锁之前需要实例化一个锁对象;mutex mtx; // 实例化一个互斥锁对象
-
加锁与解锁
调用成员函数
mutex::lock()
用于互斥锁的加锁,若是互斥锁被其他线程占有则阻塞等待;调用成员函数
mutex::unlock()
用于互斥锁的解锁;调用成员函数
mutex::try_lock()
用于互斥锁的加锁,若是互斥锁被其他线程占有则调用失败函数返回;
mutex
互斥锁同样的不支持拷贝操作只支持移动操作(移动构造mutex(mutex&&)
与移动赋值operator=(mutex&&)
);
int main() {
mutex mtx;
size_t a = 0, b = 0, x = 0;
cin >> a >> b;
thread t1([a, &x,&mtx] { // 以引用的方式捕获互斥锁
for (size_t i = 0; i < a; i++) {
mtx.lock(); // 加锁
++x;
mtx.unlock(); // 解锁
}
});
thread t2([b, &x, &mtx] {
for (size_t i = 0; i < b; i++) {
mtx.lock();
++x;
mtx.unlock();
}
});
t1.join();
t2.join();
cout << x << endl;
}
/*
运行结果:
$ ./thread
100000 100000
200000
*/
在这个例子中实例化一个互斥锁对象并且使用lock
与unlock
成员函数用于保护临界资源从而保证线程安全;
递归锁(可重入锁)
递归锁是C++
引入的一个用于避免在递归状态下导致死锁问题的锁;
class recursive_mutex;
其成员函数与mutex
相同;
当一个线程在使用mutex
互斥锁时将会因为重入互斥锁导致死锁问题;
void Func1(mutex &mtx, int &x) {
if (!x) return;
mtx.lock();
cout << this_thread::get_id() << " : Func1" << endl;
Func1(mtx, --x);
mtx.unlock();
}
int main() {
mutex mtx; // 实例化一个互斥锁
int x = 5;
thread t1(Func1, ref(mtx), ref(x));
t1.join();
return 0;
}
/*
运行结果:
$ ./thread
139666051618560 : Func1
^C // 死锁 - ctrl + C 结束程序
*/
递归锁则在递归调用过程中判断lock
时的线程是不是同一个线程,若是一个持有锁的线程再次lock
时会判断是否为同一个线程,为同一个线程则不再次获取锁或是等待,非同一线程则阻塞;
void Func1(recursive_mutex &rmtx, int &x) {
if (!x) return;
rmtx.lock();
cout << this_thread::get_id() << " : Func1" << endl;
Func1(rmtx, --x);
rmtx.unlock();
}
int main() {
recursive_mutex rmtx; // 实例化一个可重入互斥锁
int x = 5;
thread t1(Func1, ref(rmtx), ref(x));
t1.join();
return 0;
}
/*
运行结果:
$ ./thread
140547591325440 : Func1
140547591325440 : Func1
140547591325440 : Func1
140547591325440 : Func1
140547591325440 : Func1
*/
定时互斥锁
class timed_mutex; // 定时互斥锁
class recursive_timed_mutex; // 可重入定时互斥锁
定时互斥锁与可重入定时互斥锁使用方式与互斥锁/可重入互斥锁相同;
定时获取互斥锁的方式提供了两种,分别为try_lock_for
与try_lock_until
,一个用于定时时间段,一个用于定时具体时间点;
这两个获取互斥锁的方式都是以try
的方式,即尝试获取锁(定时互斥锁/可重入定时互斥锁),若是在对应的时间段过后或者具体时间点时该锁被其他线程占有则调用失败返回false
;
通常情况下定时互斥锁用于在多线程环境中,允许线程在有限的时间内尝试获取资源而不是无限期的等待;
互斥锁管理器与互斥锁抛异常所引发的死锁问题
互斥锁在锁定后抛异常将出现死锁问题;
int main() {
mutex mtx;
thread t1([&mtx] {
try {
mtx.lock(); // 占用互斥锁
throw "t1 Throw an exception"; // 抛出一个异常
mtx.unlock(); // 解锁操作 - 抛出异常后该操作无法进行
} catch (const char* str) {
cout << str << endl;
}
});
thread t2([&mtx] {
try {
mtx.lock();
throw "t2 Throw an exception";
mtx.unlock();
} catch (const char* str) {
cout << str << endl;
}
});
t1.join();
t2.join();
return 0;
}
/*
运行结果:
$ ./thread
t1 Throw an exception
^C // 发生死锁
*/
在这个例子中两个线程t1
和t2
在执行过程中必现死锁现象,原因是无论是哪个线程占有互斥锁后都会抛出一个异常跳到对应的catch
位置处理异常从而忽略unlock
解锁;
该问题通常需要使用RAII
来解决问题;
class testGuard { // RAII
public:
testGuard(mutex& mtx) : _mtx(mtx) { mtx.lock(); } // 资源创建即初始化
~testGuard() { _mtx.unlock(); } //
private:
mutex& _mtx;
};
int main() {
mutex mtx;
thread t1([&mtx] {
try {
testGuard(ref(mtx));
throw "t1 Throw an exception";
} catch (const char* str) {
cout << str << endl;
}
});
thread t2([&mtx] {
try {
testGuard(ref(mtx));
throw "t2 Throw an exception";
} catch (const char* str) {
cout << str << endl;
}
});
t1.join();
t2.join();
return 0;
}
这个例子中使用了RAII
的模式来管理互斥锁的生命周期以避免资源泄露和死锁问题;
当资源初始化时则锁定互斥锁,出了该作用域调用析构时则自动释放互斥锁;
在标准库中提供了对应的用于管理互斥锁生命周期的类,即std::lock_guard
与std::unique_lock
;
-
std::lock_guard
该互斥锁管理器是一个轻量级的,非可重入的互斥锁管理器;
当一个
lock_guard
对象被创建时将会自动尝试获取一把给定的锁;当该对象销毁时(通常为作用域结束)时将会自动释放该锁;
int main() { mutex mtx; thread t1([&mtx] { try { lock_guard<mutex>(ref(mtx)); // 使用互斥锁管理器管理互斥锁的生命周期以避免抛异常后产生的死锁问题 throw "t1 Throw an exception"; } catch (const char* str) { cout << str << endl; } }); thread t2([&mtx] { try { lock_guard<mutex>(ref(mtx)); throw "t2 Throw an exception"; } catch (const char* str) { cout << str << endl; } }); t1.join(); t2.join(); return 0; } /* 运行结果: $ ./thread t1 Throw an exception t2 Throw an exception */
-
std::unique_lock
该互斥锁管理器比
lock_guard
更加灵活,提供了延迟锁定,提前解锁或显式重新锁定,并且支持不同种类的同步机制,如条件变量;更加灵活提供了更多功能也表示其对
lock_guard
更高的开销;同样当
unique_lock
生命周期结束时将会解锁与之关联的互斥锁;int main() { mutex mtx; thread t1([&mtx] { try { unique_lock<mutex> m(ref(mtx)); m.unlock(); // 解锁 m.lock(); // 重新锁定 // 使用互斥锁管理器管理互斥锁的生命周期以避免抛异常后产生的死锁问题 throw "t1 Throw an exception"; } catch (const char* str) { cout << str << endl; } }); thread t2([&mtx] { try { unique_lock<mutex>(ref(mtx)); throw "t2 Throw an exception"; } catch (const char* str) { cout << str << endl; } }); t1.join(); t2.join(); return 0; } /* 运行结果: $ ./thread t1 Throw an exception t2 Throw an exception */
条件变量
C++
标准在引入线程库时为了支持线程间的同步操作不仅提供了互斥锁同样的也提供了条件变量std::condition_variable
;
class condition_variable;
同样的条件变量只支持构造不支持拷贝操作;
default (1)
condition_variable();
copy [deleted] (2)
condition_variable (const condition_variable&) = delete;
条件变量的等待
C++的条件变量提供了三种等待(wait
)的方式,分别为condition_variable::wait
,condition_variable::wait_for
,condition_variable::wait_until
;
无论是哪种wait
方式所传入的参数必须是一个使用unique_lock
互斥锁管理器的互斥锁;
原因是通常情况下条件变量的使用是与互斥锁相互配合的,在判断条件变量条件时需要将互斥锁提前进行解锁;
lock_guard
互斥锁管理器没有提供相应的提前解锁操作,因此在使用条件变量等待场景中必须使用unique_lock
;
-
wait
void wait (unique_lock<mutex>& lck); template <class Predicate> void wait (unique_lock<mutex>& lck, Predicate pred);
-
void wait (unique_lock<mutex>& lck)
该条件变量等待为传入一个使用
unique_lock
互斥锁管理器的互斥锁,当一个持有该互斥锁的线程wait
时将会进行一次unlock
解锁操作;若该线程未持有该互斥锁则直接进行
wait
等待,不进行其他操作; -
void wait (unique_lock<mutex>& lck, Predicate pred)
该条件变量等待是一个重载版本,可以根据传入的谓词
pred
来等待特定条件;该谓词再等待和唤醒时会被持续调用,以确保条件满足后才继续执行;
-
-
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);
-
cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time)
该等待操作允许线程等待指定的时间段,同时管理互斥锁和条件变量;
这个函数将会返回一个枚举类型
cv_status
表明它是被唤醒(通过notify_one
或notify_all
)还是超时了; -
bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred)
同样的该等待方式提供了一个添加传入 谓词 来检查是否满足条件的一种等待方式;
这个版本的
wait_for
返回一个bool
类型标识再指定时间内谓词是否变为true
;
-
-
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);
-
cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time)
提供了一个
wait_until
方法,该方法允许线程等待直到某个特定的绝对时间点;这个方法返回一个枚举类型表明该线程是被唤醒还是超时;
-
bool wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred)
提供了一个带有谓词参数的
wait_until
方法;允许线程等待直到某个特定的绝对时间点并在等待过程中检查特定条件(谓词)是否满足;
该方法返回了一个
bool
类型指示再给定时间内谓词是否为true
;
-
条件变量的唤醒
C++
提供的条件变量提供了两种唤醒的方式,分别为notify_one
与notify_all
,分别表示唤醒条件变量等待队列中的一个线程与唤醒条件变量等待队列中的所有线程;
-
notify_one()
void notify_one() noexcept;
用于唤醒条件变量等待队列中的一个线程;
如果等待队列中不存在任何线程则什么都不做;
-
notify_all()
void notify_all() noexcept;
用于唤醒条件变量等待队列中的所有线程;
如果等待队列中不存在任何线程则什么都不做;
通常情况下在使用notify_all
应谨慎以避免产生竞态条件与无效唤醒导致无意义的增加系统开销;
两个线程交替打印奇偶数
假设存在两个线程为t1
与t2
,这两个线程需要交替打印奇偶数;
在使用交替打印的时候必须使用条件变量进行控制;
#include <iostream>
#include <thread>
#include <condition_variable>
using namespace std;
int main() {
int last = 0;
cin >> last; // 获取用户输入的最后一个数
condition_variable cv; // 条件变量,用于线程同步
int x = 1; // 临界资源,表示当前计数值
bool isOdd = true; // 状态变量,用于控制线程应该打印的是奇数还是偶数
mutex mtx; // 互斥锁,保护临界资源
// 线程1:用于打印奇数
thread t1([&, last] {
int tmp = last / 2; // 循环次数为输入的一半
for (int i = 0; i < tmp; ++i) {
unique_lock<mutex> umtx(mtx); // 使用互斥锁保护共享资源
// 使用传统的while循环进行条件等待
while (!isOdd) cv.wait(umtx);
// 提示:你也可以直接使用带谓词的`wait`方法代替上面的while循环
// cv.wait(umtx, [&] { return isOdd; });
// 带谓词的`wait`方法,当lambda表达式返回`false`时线程会被阻塞
cout << "t1 : " << x << endl; // 打印当前奇数
++x; // 增加计数
isOdd = false; // 设置状态为false,下一个应该打印偶数
cv.notify_one(); // 通知等待中的线程t2
}
});
// 线程2:用于打印偶数
thread t2([&, last] {
int tmp = last / 2; // 循环次数为输入的一半
for (int i = 0; i < tmp; ++i) {
unique_lock<mutex> umtx(mtx); // 使用互斥锁保护共享资源
// 使用传统的while循环进行条件等待
while (isOdd) cv.wait(umtx);
// 提示:你也可以直接使用带谓词的`wait`方法代替上面的while循环
cv.wait(umtx, [&] { return !isOdd; });
// 带谓词的 wait 方法,当lambda表达式返回 false 时线程会被阻塞
cout << "t2 : " << x << endl; // 打印当前偶数
++x; // 增加计数
isOdd = true; // 设置状态为true,下一个应该打印奇数
cv.notify_one(); // 通知等待中的线程t1
}
});
t1.join(); // 等待线程t1执行完毕
t2.join(); // 等待线程t2执行完毕
}
/*
运行结果:
$ ./thread
6
t1 : 1
t2 : 2
t1 : 3
t2 : 4
t1 : 5
t2 : 6
*/
在这个例子中的两个线程,t1
用于打印奇数,t2
用于打印偶数;
输入了一个值作为标准值,两个线程打印最终会从1
开始交替打印奇偶数,最终打印至标准值;
-
共享资源和同步机制
-
mutex mtx
两个线程共用一个互斥锁来保护临界资源
x
和状态变量isOdd
; -
condition_variable cv
用于协调两个线程对临界资源的访问,确保只有一个线程对共享资源的访问,确保每次只有一个线程能够访问;
-
-
线程1(打印奇数)
- 使用
unique_lock<mutex> umtx(mtx)
锁定互斥锁; - 使用
cv.wait(umtx, [&]{ return isOdd; })
等待直到isOdd
为true
; - 打印并修改临界资源后将
isOdd
设置为true
并通知另一个线程;
- 使用
-
线程2(打印偶数)
- 使用
unique_lock<mutex> umtx(mtx)
锁定互斥锁; - 使用
cv.wait(umtx, [&]{ return isOdd; })
等待直到isOdd
为false
; - 打印并修改临界资源后将
isOdd
设置为false
并通知等待另一个线程;
- 使用
这个例子中t1
打印奇数t2
打印偶数,如果t2
先比t1
拿到互斥锁,但由于条件变量条件不满足将会wait
进行阻塞;
当t2
进行阻塞时将会释放互斥锁,t1
将会占用互斥锁并通过条件变量进行一轮操作直至下一轮操作时条件变量条件不满足时将会被阻塞,但在此前的一轮操作中已经使用notify_one
唤醒了另一个线程;
如此往复;