- [C++ 线程](#C++ 线程)
- 成员函数
- 线程创建
- 锁
- try_lock/unclok
- lock_guard
- lock_guard
- 死锁
- [条件变量(condition variable)](#条件变量(condition variable))
C++ 线程
成员函数
c++
1)构造函数:std::mutex 不允许拷贝构造,也不允许 move 拷贝,最初产生的mutex 对象是处于 unlocked 状态的。
2)lock():调用线程将锁住该互斥量,线程调用该函数会发生以下 3 种情况:
(a)如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock 之前,该线程一直拥有该锁。
(b)如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
(c)如果当前互斥量被当前调用线程锁住,则会产生死锁。
3)unlock():解锁,释放对互斥量的所有权。
4)try_lock():尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会
被阻塞,线程调用该函数会出现下面 3 种情况:
(a)如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
(b)如果当前互斥量被其他线程锁住,false,而并不会被阻塞掉。
(c)如果当前互斥量被当前调用线程锁住,则会产生死锁。
线程创建
C++线程的启动,只需要#include 即可。线程对象的创建,意味着线
程的开始。
c++
//todo 线程
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
// 定义一个函数,该函数将在新线程中运行
void func()
{
// 输出当前线程的ID
cout << "thread id:" << this_thread::get_id() << endl;
cout << "线程运行" << endl;
// 让当前线程休眠10秒
this_thread::sleep_for(chrono::seconds(10));
}
int main()
{
// 输出主线程的ID
cout << "main thread id:" << this_thread::get_id() << endl;
// 创建一个新线程,运行func函数
thread t(func);
// 等待新线程完成(即等待func函数执行完毕)
t.join();
cout << "线程运行结束" << endl;
return 0;
}
c++
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
// 定义一个函数,该函数将在新线程中运行
void func(int & n,string & c)
{
for (int i = 0; i < n; ++i) {
cout<<c<<endl;
}
// 修改传入的参数n和c的值
n = 10;
c = "aa";
}
int main()
{
int n=5;string str="cccc";
// 创建一个新线程t,并传入函数func和参数n、str的引用
//std::ref 是 C++ 标准库中的一个函数模板,位于 <functional> 头文件中。
// 它的作用是创建一个 std::reference_wrapper 类型的对象,
// 该对象可以用来传递引用,而不是传递值。
thread t(func, std::ref(n), std::ref(str));
t.join();
cout<< n<<" "<<str<<endl;//10 aa
return 0;
}
线程分离
c++
#if 1
//todo 线程
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
// 定义一个函数,该函数将在新线程中运行
void func(int n,string c)
{
for (int i = 0; i < n; ++i) {
cout<<c<<endl;
}
}
int main()
{
int n=5;string str="cccc";
// 创建一个新线程t,并传入函数func和参数n、str
thread t(func, n, str);
// 将线程t设置为分离状态,使其在后台运行
t.detach();
// 检查线程t是否仍然可连接(即是否仍在运行)
if (t.joinable()) {
cout << "等待线程执行完毕" << endl;
// 如果线程仍可连接,则等待其执行完毕
t.join();
}
return 0;
}
# endif
批量创建
c++
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
using namespace std;
// 定义一个宏,表示要创建的线程数量
#define N 1000
// 定义一个函数,该函数将在新线程中运行
void foo(int id)
{
cout << "Thread " << id << " started. ID: " << this_thread::get_id() << endl;
// 让当前线程休眠一段时间,模拟工作负载
this_thread::sleep_for(chrono::seconds(1));
cout << "Thread " << id << " finished." << endl;
}
int main()
{
// 创建一个存储线程对象的向量
vector<thread> threads;
cout<<"thread 大小:"<< sizeof(thread)<<endl;//8
// 预分配向量空间,避免多次内存分配
// reserve 函数用于预分配内存空间,使得向量的容量(capacity)至少为指定的值。这样,在向向量中添加元素时,如果元素数量不超过预分配的容量,就不会触发内存重新分配,从而提高性能。
threads.reserve(N);
// 循环创建并启动N个线程
for (int i = 0; i < N; ++i)
{
// 创建一个新线程,并执行foo函数,传递线程编号作为参数
//emplace_back 函数用于在向量的末尾直接构造一个元素。它通过可变参数模板(variadic templates)接受构造函数的参数,并在向量的末尾直接调用元素的构造函数,从而避免复制或移动操作。
threads.emplace_back(foo, i);
}
// 等待所有线程执行完毕
for (auto& th : threads)
{
if (th.joinable())
{
th.join();
}
}
cout << "All threads finished." << endl;
return 0;
}
锁
c++
//todo 线程同步
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
volatile int counter(0); //volatile 关键字在 C++ 中用于告诉编译器,某个变量的值可能会在程序的控制之外被改变,因此编译器不应该对该变量进行优化,到寄存器。
mutex mtx;//锁
void increase10Ktime()
{
for(int i=0; i<10000; i++)
{
mtx.lock();//加锁
counter++;
mtx.unlock();//解锁
}
}
int main()
{
thread ths[10];
for(int i=0; i<10; i++)
{
ths[i] = thread(increase10Ktime);
}
for(auto &th:ths)
th.join();
cout<<"after successful increase :"<<counter<<endl;
return 0;
}
try_lock/unclok
c++
# if 1
//todo 线程同步 try_lock/unclok
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
volatile int counter(0); //volatile 关键字在 C++ 中用于告诉编译器,某个变量的值可能会在程序的控制之外被改变,因此编译器不应该对该变量进行优化,到寄存器。
mutex mtx;//锁
void increase10Ktime()
{
for(int i=0; i<10000; i++)
{
mtx.try_lock();
counter++;
mtx.unlock();//解锁
}
}
int main()
{
thread ths[10];
for(int i=0; i<10; i++)
{
ths[i] = thread(increase10Ktime);
}
for(auto &th:ths)
th.join();
cout<<"after successful increase :"<<counter<<endl;
return 0;
}
# endif
lock_guard
c++
在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象)会被当前线程锁住。
在 lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,
因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的
Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常
处理代码。
与 Mutex RAII 相关,方便线程对互斥量上锁。using a local lock_guard to lock
mtx guarantees unlocking on destruction / exception:
引例:
c++
//todo RAII
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
// 声明一个互斥锁对象
mutex mtx;
// 函数:检查一个整数是否为偶数,并输出结果
void printEven(int i)
{
if( i % 2 == 0) // 如果i是偶数
cout << i << " is even" << endl; // 输出i是偶数
else
throw logic_error("not even"); // 如果i不是偶数,抛出一个逻辑错误异常
}
// 函数:打印线程ID,并处理可能的异常
void printThreadId(int id)
{
try{
mtx.lock(); // 锁定互斥锁,确保线程安全
printEven(id); // 调用printEven函数,检查并输出id是否为偶数
mtx.unlock(); // 解锁互斥锁
}catch(logic_error & ){ // 捕获逻辑错误异常
cout << "exception caught" << endl; // 输出异常信息
}
}
int main()
{
thread ths[10]; // 声明一个包含10个线程对象的数组
// 创建10个线程
for(int i = 0; i < 10; i++)
{
ths[i] = thread(printThreadId, i + 1); // 启动线程,调用printThreadId函数,参数为i+1
}
// 等待所有线程完成
for(auto & th : ths)
th.join(); // 主线程等待每个子线程完成
return 0;
}
获取锁后,抛出异常,就不会解锁
lock_guard
c++
lock_guard 的使用体现了 RAII(Resource Acquisition Is Initialization)思想。RAII 是 C++ 中的一种编程范式,核心思想是通过对象的生命周期来管理资源,确保资源在对象构造时获取,在对象析构时释放。
RAII 的核心概念
资源获取即初始化:在对象构造时获取资源,在对象析构时释放资源。
自动管理资源:通过对象的生命周期自动管理资源,避免资源泄漏和手动管理资源的复杂性。
lock_guard 如何体现 RAII
构造时获取资源:lock_guard 在其构造函数中锁定互斥锁。
析构时释放资源:lock_guard 在其析构函数中解锁互斥锁。
这意味着当 lock_guard 对象被创建时,互斥锁会被自动锁定;当 lock_guard 对象离开其作用域(例如函数返回或异常抛出)时,互斥锁会被自动解锁。
c++
# if 1
//todo RAII
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
// 声明一个互斥锁对象
mutex mtx;
// 函数:检查一个整数是否为偶数,并输出结果
void printEven(int i)
{
if( i % 2 == 0) // 如果i是偶数
cout << i << " is even" << endl; // 输出i是偶数
else
throw logic_error("not even"); // 如果i不是偶数,抛出一个逻辑错误异常
}
// 函数:打印线程ID,并处理可能的异常
void printThreadId(int id)
{
try{
lock_guard<mutex> lck(mtx);//它在其构造函数中锁定互斥锁,并在其析构函数中解锁互斥锁。这样可以确保在离开作用域时自动释放锁,避免忘记解锁导致的死锁问题。
printEven(id); // 调用printEven函数,检查并输出id是否为偶数
}catch(logic_error & ){ // 捕获逻辑错误异常
cout << "exception caught" << endl; // 输出异常信息
}
}
int main()
{
thread ths[10]; // 声明一个包含10个线程对象的数组
// 创建10个线程
for(int i = 0; i < 10; i++)
{
ths[i] = thread(printThreadId, i + 1); // 启动线程,调用printThreadId函数,参数为i+1
}
// 等待所有线程完成
for(auto & th : ths)
th.join(); // 主线程等待每个子线程完成
return 0;
}
# endif
c++
输出
exception caught
2 is even
exception caught
10 is even
exception caught
6 is even
exception caught
8 is even
exception caught
4 is even
死锁
死锁的原因是,container 试图多次去获取锁己获得的锁。std::recursive_mutex 允
许多次获取相同的 mutex。
死锁:
c++
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
using namespace std;
// 模板类:容器,用于存储元素并提供线程安全的添加和访问方法
template <typename T>
class container
{
public:
// 添加单个元素到容器中
void add(T element)
{
_mtx.lock(); // 锁定互斥锁,确保线程安全
_elements.push_back(element); // 将元素添加到向量中
_mtx.unlock(); // 解锁互斥锁
}
// 添加多个元素到容器中
void addrange(int num, ...)
{
va_list arguments; // 声明一个可变参数列表
va_start(arguments, num); // 初始化可变参数列表
for (int i = 0; i < num; i++)
{
_mtx.lock(); // 锁定互斥锁,确保线程安全
add(va_arg(arguments, T)); // 从可变参数列表中获取一个元素并添加到容器中
_mtx.unlock(); // 解锁互斥锁
}
va_end(arguments); // 结束可变参数列表
}
// 输出容器中的所有元素
void dump()
{
_mtx.lock(); // 锁定互斥锁,确保线程安全
for(auto e : _elements)
cout << e << endl; // 输出每个元素
_mtx.unlock(); // 解锁互斥锁
}
private:
mutex _mtx; // 互斥锁,用于保护共享数据
vector<T> _elements; // 存储元素的向量
};
// 线程函数:向容器中添加随机数
void func(container<int>& cont)
{
cont.addrange(3, rand(), rand(), rand()); // 向容器中添加3个随机数
}
int main()
{
srand((unsigned int)time(0)); // 初始化随机数生成器
container<int> cont; // 创建一个存储整数的容器
// 创建3个线程,每个线程调用func函数向容器中添加随机数
thread t1(func, ref(cont));
thread t2(func, ref(cont));
thread t3(func, ref(cont));
// 等待所有线程完成
t1.join();
t2.join();
t3.join();
cont.dump(); // 输出容器中的所有元素
return 0;
}
递归锁 (unique_lock /recursive_mutex)
recursive_mutex _mtx;
recursive_mutex 是 C++ 标准库中的一种互斥锁,允许同一个线程多次锁定同一个互斥锁,而不会导致死锁。这与普通的 mutex 不同,普通的 mutex 在同一个线程中多次锁定会导致死锁。
unique_lock<recursive_mutex> lck(_mtx);unique_lock 是 C++11 引入的一种智能锁,用于简化互斥锁的管理。与 lock_guard 类似,unique_lock 也提供了 RAII 风格的锁管理,但它更加灵活,支持延迟锁定、锁的所有权转移和手动解锁等操作。
改进:
c++
include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
using namespace std;
// 模板类:容器,用于存储元素并提供线程安全的添加和访问方法
template <typename T>
class container
{
public:
// 添加单个元素到容器中
void add(T element)
{
unique_lock<recursive_mutex> lck(_mtx);
_elements.push_back(element); // 将元素添加到向量中
}
// 添加多个元素到容器中
void addrange(int num, ...)
{
va_list arguments; // 声明一个可变参数列表
va_start(arguments, num); // 初始化可变参数列表
for (int i = 0; i < num; i++)
{
unique_lock<recursive_mutex> lck(_mtx);
add(va_arg(arguments, T)); // 从可变参数列表中获取一个元素并添加到容器中
}
va_end(arguments); // 结束可变参数列表
}
// 输出容器中的所有元素
void dump()
{
unique_lock<recursive_mutex> lck(_mtx);//unique_lock 是 C++11 引入的一种智能锁,用于简化互斥锁的管理。与 lock_guard 类似,unique_lock 也提供了 RAII 风格的锁管理,但它更加灵活,支持延迟锁定、锁的所有权转移和手动解锁等操作。
for(auto e : _elements)
cout << e << endl; // 输出每个元素
}
private:
recursive_mutex _mtx; // recursive_mutex 是 C++ 标准库中的一种互斥锁,允许同一个线程多次锁定同一个互斥锁,而不会导致死锁。这与普通的 mutex 不同,普通的 mutex 在同一个线程中多次锁定会导致死锁。
vector<T> _elements; // 存储元素的向量
};
// 线程函数:向容器中添加随机数
void func(container<int>& cont)
{
cont.addrange(3, rand(), rand(), rand()); // 向容器中添加3个随机数
}
int main()
{
srand((unsigned int)time(0)); // 初始化随机数生成器
container<int> cont; // 创建一个存储整数的容器
// 创建3个线程,每个线程调用func函数向容器中添加随机数
thread t1(func, ref(cont));
thread t2(func, ref(cont));
thread t3(func, ref(cont));
// 等待所有线程完成
t1.join();
t2.join();
t3.join();
cont.dump(); // 输出容器中的所有元素
return 0;
}
c++
输出
6334
18467
41
6334
18467
41
6334
18467
41
条件变量(condition variable)
条件变量(condition variable)是利用线程间共享的全局变量进行同步的一种机
制,
主要包括两个动作:
一个线程等待某个条件为真,而将自己挂起;
另一个线程使的条件成立,并通知等待的线程继续。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
C++11 中引入了条件变量,其相关内容均在<condition_variable>中。
这里主要介绍 std::condition_variable 类。
条件变量 std::condition_variable 用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。
std::condition_variable 需 要 与 std::unique_lock 配 合 使 用 。
std::condition_variable 效果上相当于包装了 pthread 库中的 pthread_cond_*()系列的函数。
当 std::condition_variable 对象的某个wait函数被调用的时候 , 它使用std::unique_lock(通过 std::mutex)来锁住当前线程。
当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒
当前线程。
c++
# if 1
//todo 条件变量(condition variable)
/*
10 个线程,争相去打印,传入的 id,第一个线程但是获得锁的,但为条件不满足,
采用条件变量出让锁,等待条件,其它线程亦是如此。延时后的主要线程,获得锁,将
条件置为真,并能过条件变量唤醒所有等待在条件变量上的 10 个线程。此时 10 个线
程再次争相去获取锁,然后,判断条件为真,然后打印,释放锁给其它线程。
*/
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
// 全局变量,用于指示线程是否可以开始工作
bool ready = false;
// 互斥锁,用于保护共享变量
mutex mtx;
// 条件变量,用于线程间的同步
condition_variable cv;
// 线程函数:打印线程ID
void printId(int id) {
unique_lock<mutex> lck(mtx); // 创建unique_lock并锁定互斥锁
while (!ready) { // 检查ready变量是否为true
cv.wait(lck); // 如果ready为false,则线程进入等待状态,并解锁互斥锁
}
cout << "id= " << id << endl; // 打印线程ID
} // unique_lock在离开作用域时自动解锁
// 函数:通知所有线程开始工作
void go() {
unique_lock<mutex> lck(mtx); // 创建unique_lock并锁定互斥锁
ready = true; // 设置ready变量为true,表示线程可以开始工作
cv.notify_all(); // 通知所有等待的线程
} // unique_lock在离开作用域时自动解锁
int main() {
thread ths[10]; // 声明一个包含10个线程对象的数组
// 创建10个线程,每个线程调用printId函数,参数为线程ID
for (int i = 0; i < 10; ++i) {
ths[i] = thread(printId, i);
}
// 主线程休眠5秒
this_thread::sleep_for(chrono::seconds(5));
// 通知所有线程开始工作
go();
// 等待所有线程完成
for (auto &t : ths) {
t.join();
}
return 0;
}
# endif