C++线程库

目录

一、thread类

1.thread类函数介绍

2.线程函数

3.线程函数的参数

二、锁

1.mutex锁

2.lock_guard

3.unique_lock

三、原子操作

四、条件变量

1.成员函数介绍

(1)wait

(2)wait_for

(3)wait_until

2.线程交替打印奇偶数代码


一、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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南林yan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值