C++11 thread

文章目录

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)

相关推荐
ceffans7 分钟前
PDF文档中文本解析
c++·windows·pdf
SummerGao.14 分钟前
Windows 快速搭建C++开发环境,安装C++、CMake、QT、Visual Studio、Setup Factory
c++·windows·qt·cmake·visual studio·setup factory
仟濹18 分钟前
【二分搜索 C/C++】洛谷 P1873 EKO / 砍树
c语言·c++·算法
服务端相声演员1 小时前
Oracle JDK、Open JDK zulu下载地址
java·开发语言
YH_DevJourney1 小时前
Linux-C/C++《C/8、系统信息与系统资源》
linux·c语言·c++
19岁开始学习1 小时前
Go学习-入门
开发语言·学习·golang
青铜念诗1 小时前
python脚本文件设置进程优先级(在.py文件中实现)
开发语言·python
一念春风2 小时前
C# 背景 透明 抗锯齿 (效果完美)
开发语言·c#
Igallta_8136222 小时前
【小游戏】C++控制台版本俄罗斯轮盘赌
c语言·开发语言·c++·windows·游戏·游戏程序
Dyan_csdn2 小时前
【Python项目】文本相似度计算系统
开发语言·python