文章目录
- [C++11 线程库](#C++11 线程库)
- 线程对象的构造方式
- this_thread命名空间
- [join && detach](#join && detach)
- mutex
C++11 线程库
线程对象的构造方式
无参的构造函数
1、调用无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程
thread t1;
2、thread提供了移动赋值函数,当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象
void Print(size_t n , const std::string& s)
{
for (size_t i = 0; i < n; i++)
{
std::cout << std::this_thread::get_id() << ":" << i << std::endl;
}
}
int main()
{
int n = 10;
// 创建n个线程执行Print
std::vector<std::thread> vthd(n);
size_t j = 0;
for (auto& thd : vthd)
{
// 移动赋值
thd = std::thread(Print, 10, "线程" + std::to_string(j++));
}
for (auto& thd : vthd)
{
thd.join();
}
return 0;
}
场景:
实现线程池的时候, 需要先创建一批线程,但,一开始这些线程什么也不做,当有任务到来时再让这些线程来处理这些任务。
#include <iostream>
#include <vector>
#include <thread>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
class ThreadPool {
public:
ThreadPool(size_t threads = 4); // 构造函数,可以指定线程池的大小
~ThreadPool(); // 析构解函数
void enqueue(std::function<void()> task); // 添加任务到线程池
private:
std::vector<std::thread> workers; // 线程向量
std::queue<std::function<void()>> tasks; // 任务队列队
std::mutex queue<std::function<void()>> completedTasks;
std::condition_variable cond; // 条件变量
bool stop; // 停止标志
void work(); // 线程工作函数
};
// 构造函数初始化线程池
ThreadPool::ThreadPool(size_t threads) : stop(false) {
for(size_t i = 0; i < threads; ++i)
workers.emplace_back(std::thread(&ThreadPool::work, this));
}
// 析构解函数等待所有线程完成
ThreadPool::~ThreadPool() {
{
std::unique_lock<std::mutex> lock(this->mtx);
this->stop = true;
}
cond.notify_all();
for(std::thread &worker : workers) {
if(worker.joinable()) {
worker.join(); // 等待线程结束
}
}
}
// 添加任务到线程池
void ThreadPool::enqueue(std::function<void()> task) {
{
std::unique_lock<std::mutex> lock(mtx);
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace(task);
}
cond.notify_one();
}
void ThreadPool::work() {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->mtx);
while(tasks.empty()) {
cond.wait(lock); // 如果没有任务,等待
}
task = std::move(tasks.front());
tasks.pop();
}();
task(); // 执行任务
}
}
在这个示例中,ThreadPool
类管理一组工作线程。构造函数创建指定数量的线程,每个线程都调用work
方法等待并执行任务。enqueue
方法用于添加新任务到队列,如果线程池已经停止,则抛出异常。每个工作线程在接收到任务后会出队列并执行它
调用带参的构造函数
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
fn
:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。args...
:调用可调用对象fn时所需要的若干参数
调用移动构造函数
用一个右值线程对象来构造一个线程对象
void func(int n)
{
for (int i = 0; i <= n; i++)
{
cout << i << endl;
}
}
int main()
{
thread t3 = thread(func, 10);
t3.join();
return 0;
}
线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
如果创建线程对象时没有提供线程函数,那么该线程对象实际没有对应任何线程。
如果创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行。
thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行
thread常用成员函数
join ,对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞
joinable , 判断该线程是否已经执行完毕,如果是则返回true,否则返回false
detach ,将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待
get_id , 获取该线程的id
swap , 将两个线程对象关联线程的状态进行交换
joinable函数还可以用于判定线程是否是有效的,
如果是以下任意情况,则线程无效:
采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程)
线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理)
线程已经调用join或detach结束。(线程已经结束)
thread的成员函数get_id
可以获取线程的id,但该方法必须通过线程对象来调用get_id
函数
如果要在线程对象关联的线程函数中获取线程id,调用this_thread
命名空间下的get_id
函数
void func()
{
cout << this_thread::get_id() << endl; //获取线程id
}
int main()
{
thread t(func);
t.join();
return 0;
}
this_thread命名空间
yield | 当前线程"放弃"执行,让操作系统调度另一线程继续执行 |
---|---|
sleep_until | 让当前线程休眠到一个具体时间点 |
sleep_for | 让当前线程休眠一个时间段 |
线程传参
1、使用lambda
#include<thread>
#include<iostream>
#include<vector>
#include<string>
int main()
{
size_t n1 = 5;
size_t n2 = 5;
/*std::cin >> n1 >> n2;*/
std::thread t1( [n1](){
for (size_t i = 0; i < n1; i++)
{
//拿到该线程的线程id
std::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;
}
} );
std::thread t2([n2](){
for (size_t i = 0; i < n2; i++)
{
//拿到该线程的线程id
std::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;
}
} );
t2.join();
t1.join();
return 0;
}
join && detach
启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。thread库提供了两种回收线程资源的方式:
1、join
主线程创建新线程后,调用join函数等待新线程终止,当新线程终止时join
函数就会自动清理线程相关的资源
join
函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join
,如果一个线程对象使用多次join , 程序会崩溃
void func(int n)
{
for (int i = 0; i <= n; i++)
{
cout << i << endl;
}
}
int main()
{
thread t(func, 20);
t.join();
t.join(); //程序崩溃
return 0;
}
2、detach
主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。
使用detach的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach函数。
否则线程对象可能会因为某些原因,在后续调用detach函数分离线程之前被销毁掉,这时就会导致程序崩溃。
因为当线程对象被销毁时会调用thread的析构函数,而在thread的析构函数中会通过joinable判断这个线程是否需要被join,如果需要那么就会调用terminate终止当前程序(程序崩溃)
#include <iostream>
#include <thread>
#include <vector>
void worker() {
std::cout << "Working..." << std::endl;
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Finished" << std::endl;
}
int main() {
std::vector<std::thread> threads;
// 创建并分离多个线程
for (int i = 0; i < 5; ++i) {
threads.emplace_back(std::thread(worker));
threads.back().detach(); // 分离线程
}
std::cout << "Main thread continues to run..." << std::endl;
// 主线程可以继续执行其他任务,而不需要等待分离的线程
// 等待所有线程完成(可选)
for (auto& th : threads) {
if (th.joinable()) {
th.join();
}
}
std::cout << "All threads finished" << std::endl;
return 0;
}
过度使用分离线程可能会导致资源泄露,因为分离的线程将继续运行,即使主线程已经结束。因此,通常建议在可能的情况下使用join
来管理线程的生命周期。
mutex
C++11中,mutex中总共包了四种互斥量
1、std::mute
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动
mutex常用的成员函数:
lock | 对互斥量进行加锁 |
---|---|
try_lock | 尝试对互斥量进行加锁 |
unlock | 对互斥量进行解锁,释放互斥量的所有权 |
线程函数调用lock时,可能会发生以下三种情况:
1、如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一致拥有该锁。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
void printBlock(int n) {
mtx.lock(); // 获取锁,如果锁已经被其他线程占用,则等待
for (int i = 0; i < n; ++i) {
std::cout << "Thread " << std::this_thread::get_id() << " says " << i << std::endl;
}
mtx.unlock(); // 释放锁,其他线程可以获取该锁
}
int main() {
std::thread t1(printBlock, 1);
std::thread t2(printBlock, 2);
t1.join();
t2.join();
return 0;
}
mtx
是一个全局互斥锁,所以一次只能有一个线程执行printBlock
函数。这意味着即使两个线程几乎同时运行,输出也将是交错的,而不是同时打印,因为一个线程在打印时会锁定互斥锁,另一个线程必须等待
2、如果该互斥量已经被其他线程锁住,则当前的调用线程会被阻塞。
互斥锁(mutex)在多线程环境中的基本行为。互斥锁是一种同步机制,用于防止多个线程同时访问共享资源,从而避免数据竞争条件和不一致的状态。当一个线程尝试获取已经被其他线程持有的互斥锁时,该线程将被阻塞,直到互斥锁被释放
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx; // 全局互斥锁
void worker(int id) {
// 尝试获取锁
mtx.lock();
std::cout << "Thread " << id << " acquired the lock" << std::endl;
// 模拟耗时的工作
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Thread " << id << " released the lock" << std::endl;
// 释放锁
mtx.unlock();
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
// 主线程稍作等待,以便 t1 有机会获取锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 尝试在 t1 持有锁时获取锁
mtx.lock();
std::cout << "Main thread acquired the lock" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟主线程持有锁
mtx.unlock();
t1.join();
t2.join();
return 0;
}
3、如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
如何使用局部变量加锁?
例1:用lambda
int main()
{
size_t n1 = 10000;
size_t n2 = 10000;
/*std::cin >> n1 >> n2;*/
int x = 0;
std::mutex mtx;
std::thread t1([&n1, &x ,&mtx]() {
for (size_t i = 0; i < n1; i++)
{
mtx.lock();
x++;
//拿到该线程的线程id
//std::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;
mtx.unlock();
}
});
std::thread t2([&n2, &x,&mtx]() {
for (size_t i = 0; i < n2; i++)
{
mtx.lock();
//拿到该线程的线程id
x++;
//std::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;
mtx.unlock();
}
});
t2.join();
t1.join();
std::cout << x;
return 0;
}
例2:
void Print1(size_t n,size_t j , const std::string& s ,std::mutex & mtx) //锁必须传引用 ,锁不支持拷贝
{
for (size_t i = 0; i < j+n; i++)
{
mtx.lock();
std::cout << std::this_thread::get_id() << ":" << i << std::endl;
mtx.unlock();
}
}
int main()
{
int n = 10;
// 创建n个线程执行Print
std::vector<std::thread> vthd(n);
std::mutex mtx;
std::thread t1(Print1, 100, 1, "hello", ref(mtx )); //必须加ref函数 ,否则锁不能以传引用的方式传参传过去
std::thread t2(Print1, 100, 100000, "world", ref(mtx));
t1.join();
t2.join();
return 0;
}
例3
void Print1(size_t n, const std::string& s, std::mutex& mtx ,int & rx) //锁必须传引用 ,锁不支持拷贝
{
for (size_t i = 0; i < n; i++)
{
mtx.lock();
std::cout << std::this_thread::get_id() << ":" << i << std::endl;
++rx;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::microseconds(1000));
}
}
int main()
{
std::mutex mtx;
int x = 10;
std::thread t1(Print1, 5, "hello", std::ref(mtx), std::ref(x));
std::thread t2(Print1,5, "world", std::ref(mtx), std::ref(x));
std::cout << "线程1: " << t1.get_id() << std::endl;
std::cout << "线程2: " << t2.get_id() << std::endl;
t1.join();
t2.join();
std::cout << x << std::endl;
return 0;
}
线程调用try_lock时,类似也可能会发生以下三种情况:
1、如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一致拥有该锁。
2、如果该互斥量已经被其他线程锁住,则try_lock调用返回false,当前的调用线程不会被阻塞。
3、如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)