C++11---多线程

看前须知:如果对线程不了解的,可以先去看Linux---多线程(上)(下)这两篇文章

那里主要讲了线程的一些基础概念和底层相关理解,对我们阅读这篇文章会有所帮助

一、thread --- 线程

1、thread相关接口介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接 口,这使得代码的可移植性比较差C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件
函数接口功能说明
thread()
构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)
构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数
get_id()
获取线程id
joinable()
查看线程是否是连接状态,与detch后的线程的分离状态相对应
join()
该函数调用后会阻塞等待线程结束
如果线程是默认构造的线程对象 / 已经被detach / 已经被 join,再调用join会出错
detach()
在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

下面是一个简单的创建线程的代码

#include<thread>
#include<iostream>

using namespace std;

void Print(size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}


int main()
{
	thread t(Print, 10);
	// t.detach();
	if (t.joinable())
		t.join();
	cout << t.joinable() << endl;
	return 0;
}

注意:

  • 线程是操作系统中的概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
  • 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针lambda表达式、仿函数、包装器,如下
#include<iostream>
void Print(size_t n)
{
	cout << this_thread::get_id() << " : "; // 获取当前线程的线程id
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}

struct print
{
	void operator()(size_t n)
	{
		cout << this_thread::get_id() << " : ";
		for (size_t i = 0; i < n; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	}
};

int main()
{
	thread t1(Print, 10);
	Sleep(1);
	thread t2(print(), 20);
	Sleep(1);
	thread t3([](size_t n) {
		cout << this_thread::get_id() << " : ";
		for (size_t i = 0; i < n; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	}, 30);
	Sleep(1);
	function<void(size_t)>f = [](size_t n) {
		cout << this_thread::get_id() << " : ";
		for (size_t i = 0; i < n; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	};

	thread t4(f, 40);
	t1.join();
	t2.join();
	t3.join();
	t4.join();
	return 0;
}
  • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

void Print(size_t n, string s)
{
	cout << s << " : ";
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}

int main()
{
	int n = 10;
	vector<thread> vthd(n);
	size_t j = 0;
	for (auto& thd : vthd)
	{
        // 移动赋值 --- 临时变量是将亡值
		thd = thread(Print, 10, "线程" + to_string(j++));
		Sleep(1);// 休眠的目的是为了让打印出来的数据看起来不乱
	}
	for (auto& thd : vthd)
	{
		thd.join();
	}
    thread t1(Print, 10, "zwxs");
	// thread t2(t1); // 错,thread不支持拷贝构造
	thread t2(move(t1)); //thread支持移动构造

	t2.join();
	return 0;
}

  • 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用jion或者detach结束

2、线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参,如果不理解,可以直接记住结论,即创建线程时传引用得加std::ref()

void Print(size_t n, int & x)
{
	for (size_t i = 0; i < n; i++)
	{
		x++;
	}
}

void Print1(size_t n, int* x)
{
	for (size_t i = 0; i < n; i++)
	{
		(*x)++;
	}
}

int main()
{
	int x = 0;
	// thread t(Print, 10, x); // 会报错
	thread t(Print, 10, ref(x)); // std::ref() 帮助我们传递引用
	t.join();
	cout << x << endl;

	thread t1(Print1, 10, &x); // 可以直接传指针,通过指针来修改值
	t1.join();
	cout << x << endl;
	return 0;
}

3、this_thread命名空间

this_thread 是 C++11 引入的一个命名空间,它位于 std 下,并提供了一组函数,用于操作当前执行的线程。这个命名空间的目的是为开发者提供一种便捷的方式来获取和管理当前线程的信息和行为。

  1. get_id:这个函数用于获取当前线程的线程 ID。线程 ID 是一个唯一标识线程的整数值,通过它可以在程序中区分和追踪不同的线程。
  2. yield:这个函数用于让当前线程主动放弃处理器的使用权,使得其他线程有机会执行。这是一种线程间的协作机制,有助于实现更高效的线程调度。
  3. sleep_for:这个函数使当前线程进入休眠状态,直到指定的时间段过去,这可以用于控制线程的执行节奏,或者在某些情况下,等待某些条件成立。
  4. sleep_until:这个函数使当前线程进入休眠状态,直到达到指定的时间点。它允许线程在特定的时间唤醒并执行。

与时间相关的函数可以结合<chrono>头文件下的相关函数使用,如hours,minutes,seconds等

void Print(size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cout << i << " ";
		this_thread::sleep_for(chrono::seconds(1));
	}
}

二、mutex --- 锁

在C++11中,Mutex总共包了四个互斥量的种类,都不支持拷贝构造/移动拷贝/赋值拷贝/移动赋值

1、std::mutex

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用
的三个函数:
函数接口功能说明
lock()阻塞加锁
try_lock()非阻塞加锁
unlock()解锁

int main()
{
	int x = 0;
	int n = 1000;
	thread t1([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;
		}
	});

	thread t2([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;
		}
	});

	t1.join();
	t2.join();

	cout << x << endl;
	return 0;
}

很明显,上面的代码是线程不安全的,我们需要给线程加锁,代码如下

int main()
{
	int x = 0;
	int n = 1000;
	mutex mtx;
	thread t1([&]() {
		for (int i = 0; i < n; i++)
		{
			mtx.lock();
			x++;
			mtx.unlock();
		}
	});

	thread t2([&]() {
		for (int i = 0; i < n; i++)
		{
			mtx.lock();
			x++;
			mtx.unlock();
		}
	});

	t1.join();
	t2.join();

	cout << x << endl;
	return 0;
}

这里细心的读者可能已经发现了一个问题:为什么这里能"传"引用?注意:这里不是传参,而是lambda表达式的捕获列表,可以理解为两者底层走的不是一个逻辑,所以这里可以,至于具体底层是如何走的,有兴趣的可以自己去查查看。

2、std::recursive_mutex

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

3、std::timed_mutex

比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

4、std::recursive_timed_mutex

具有recursive_mutex和timed_mutex的特性


出现死锁的情况,如下

void func(int x)
{
	if (x%2)
		throw exception("异常");
	else
		cout << "func()" << endl;
}

int main()
{
	mutex mtx;
	int y;
	thread t1([&]() {
		try {
			for (int i = 0; i < 10; i++) {
				mtx.lock();
				y++;
				func(i);
				mtx.unlock();
			}
		}
		catch (const exception& e) {
			cout << e.what() << endl;
		}
	});

	thread t2([&]() {
		try {
			for (int i = 0; i < 10; i++) {
				mtx.lock();
				y++;
				func(i);
				mtx.unlock();
			}
		}
		catch (const exception& e) {
			cout << e.what() << endl;
		}
		});
	t1.join();
	t2.join();
	return 0;
}

针对上面的情况,我们需要有一个能自动释放的锁,类似智能指针,库给我们提供了lock_guard和unique_lock用来封装锁,不需要我们去手动释放锁。

thread t1([&]() {
	try {
		for (int i = 0; i < 10; i++) {
			lock_guard<mutex> lock(mtx);
			y++;
			func(i);
		}
	}
	catch (const exception& e) {
		cout << e.what() << endl;
	}
});

lock_guard和unique_lock的区别:

lock_guard只支持构造和析构,没有其他功能

unique_lock能支持手动的加锁和解锁,并且能用时间进行控制

三、condition_variable --- 条件变量

不了解的可以先去看Linux---多线程(下)

常用的三个函数接口:

函数接口功能说明
void wait(unique_lock<mutex>& lck)等待条件就绪,再往下执行,会先释放申请到的锁,故只能传unique_lock,lock_guard不支持手动加锁和解锁
void notify_one()唤醒在条件变量的等待队列中的一个线程,需要重新加锁
void notify_all()唤醒在条件变量的等待队列中的所有线程,需要重新加锁
如何实现两个线程交替打印,一个打印奇数,一个打印偶数?
int main()
{
	int x = 1;
	condition_variable cv;
	mutex mtx;
	thread t1([&]() {
		for (int i = 0; i < 10; i++) {
			unique_lock<mutex> lock(mtx);
			while (x%2==0)
				cv.wait(lock);
			cout << this_thread::get_id() << " : " << x++ << endl;
			cv.notify_one();
		}
	});

	thread t2([&]() {
		for (int i = 0; i < 10; i++) {
			unique_lock<mutex> lock(mtx);
			while (x%2)
				cv.wait(lock);
			cout << this_thread::get_id() << " : " << x++ << endl;
			cv.notify_one();
		}
	});

	t1.join();
	t2.join();
	return 0;
}

四、atomic

C++11中的<atomic>库提供了对原子操作的支持,这些操作在多线程环境中是线程安全的。原子操作是不可中断的操作,即在执行完毕之前不会被其他线程打断。通过使用原子操作,我们可以避免使用互斥量(mutexes)和条件变量(condition variables)等同步原语,从而在某些情况下提高性能并简化代码

比如上面用锁保证x++的线程安全,其实比较浪费资源,因为申请锁一旦失败,线程就会阻塞,需要让出cpu,切换上下文数据等,而我们加锁只是为了执行x++这一条语句,显然很不值得,这里我们就可以用atomic中的函数,进行原子操作,不需要进入阻塞,代码如下

int main()
{
	atomic<int> x = 0;
	int n = 1000;
	//mutex mtx;
	thread t1([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;// 这里的++用的是运算符重载
		}
	});

	thread t2([&]() {
		for (int i = 0; i < n; i++)
		{
			x++;// 这里的++用的是运算符重载
		}
	});

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

可以简单说明一下,它用的无锁编程---利用原子操作实现锁的功能。上面的++操作符就是用CAS (compare and swap)这个原子操作实现的(CAS在不同的语言中都会有对应的函数)

/*伪代码
int x = 0;
int old,newval;
do
{
	old = x;
	newval = old + 1;
}while(!CAS(&x,&old,newval)) 
//看x在内存中的值是否和old相同,如果相同*x=newval,返回true,否则返回false
*/

如果对无锁编程感兴趣可以去查查文档,这里就不多做介绍了,这个对编程能力要求有点高,建议最好不要轻易去写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值