C++多线程入门

前言

在我们之前的教程中,我们写的程序在同一时间段内都只能执行单个任务,执行完了才能执行下一个任务,但是在实际软件开发中,我们的软件在同一时间段内往往需要进行多个任务。

比如在游戏开发中,我们的游戏场景里面往往有多个角色,如果我们的游戏程序在每一帧内都是一个一个地执行每一个角色的任务的话,那么这个游戏就会很卡,帧数很低。

所以我们希望每一帧内能同时进行多个角色的任务,等到每个角色都执行完后就执行下一帧。

多线程就是解决像这样的问题的技术。

什么是多线程?什么是进程?

线程的定义

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。——百度百科

进程的定义

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。——百度百科

线程和进程的区别

  1. 一个程序有且只有一个进程,但可以拥有至少一个的线程。
  2. 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。

线程和进程的关系

在这里插入图片描述
一般情况下,我们的软件只需要占用一个进程,但这个进程内至少会有一个线程。

C++中的线程

在C语言的标准库中,已经有pthread头文件pthread提供了多线程编程的功能,但如今的C++软件开发中已经很少有人用它了,因为它提供的功能相对于C++11新增的头文件thread中新增的多线程的功能要少,而且不是很好用,所以我们就不再介绍pthread了,直接讲C++11的 thread。

std::thread

thread的构造函数和析构函数:

thread() noexcept//thread的默认构造函数,创建一个线程,线程里面什么也不做

thread(Fn&& fn, Args&&… args)//thread的初始化构造函数,创建一个线程,以args为参数执行fn函数
//注意这里是&&不是&,表示这是右值引用!而&是左值引用!

//在thread头文件的定义中,以下两个函数的后面被加上了=delete,表示这两个函数被删除了
//也就是说,thread无法以复制别的thread的方式来构造
thread(const thread&) = delete;
thread& operator=(const thread&) = delete;

//thread的移动构造函数,会构造一个与x相同的thread,但会消耗x,也就是说x这个thread被移动到了新创建的thread里面
thread(thread&& x) noexcept

~thread()//析构函数

thread类的常用成员函数

void join();//等待线程结束并清理资源(会阻塞线程)
bool joinable();//返回线程是否可以执行join函数
void detach();//将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join
std::thread::id get_id();//这是一个静态方法,可以获取线程的id
thread& operator=(thread &&rhs);//移动构造函数如果,rhs线程是joinable的,那么会调用std::terminate()结果程序)

std::thread的使用样例

thread执行无参数的函数
#include<iostream>
#include<thread>
using namespace std;
void fun()
{
	cout << 1 ;
}
int main()
{
	//线程一旦被创建 就会立即执行
	thread t1(fun), t2([] {cout << 2 ; });//这里使用了C++11的lambda表达式,不知道请百度
	//完后要等待线程结束,然后main函数结束
	t1.join();
	t2.join();
}

输出结果可能会有两种情况:

12

或者:

21

因为在这里,打印1和打印2的任务是同时进行的,所以两个任务谁先执行完都有可能,所以会有两种输出结果。
他们是异步完成任务的,并不是同步完成任务,没有谁需要等谁的次序。

thread执行有参数的函数
#include<iostream>
#include<thread>
using namespace std;
void fun(int a,int b)
{
	cout << a<<b;
}
int main()
{
	int a = 1, b = 2;
	//线程一旦被创建 就会立即执行
	thread t1(fun,a,b);//函数的参数像这样写在函数名后面,而不是写成fun(a,b)
	//完后要等待线程结束
	t1.join();
}

输出结果:

12

上面传递给fun函数的两个参数a和b都是右值(也就是在函数执行完后就会被销毁的值),如果我们要传递个左值,让函数引用它(也就是函数执行完后不会被销毁的左值)该怎么办呢?

std::ref和std::cref就解决了这个问题

std::ref
#include <thread>
using namespace std;
void fun(int& a)//注意这里是引用,就是告诉了这个函数a在内存中的地址,这个函数就可以在内存中修改它
{
	a = 5;
}
int main()
{
	int a = 1;
	thread t(fun, ref(a));
	t.join();//一定要在输出a前等待线程结束,a还没有被那个线程修改,就被输出了
	cout << a;
}

输出结果:

5
std::cref

std::cref的使用方式和ref相同,但它传递的变量的引用是const类型的,即定义后就不可被修改。

std::this_thread

std::this_thread命名空间可以很方便地让线程对自己进行控制, 它用于在线程中调用thread类的成员。类似其他类中的this。

使用thread的注意事项:

  1. thread对象一旦被定义就会开始执行,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
  2. 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源
  3. 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
  4. 没有执行join或detach的线程在程序结束时会引发异常。

我们知道了thread怎么使用,但还远远不够。我们在实际游戏开发中,有些数据可能是要被多个线程共同使用的。
比如,游戏场景中有一个箱子,和几个角色,这个箱子在同一时刻,只能让一个角色往里面放东西,不能同时有多个角色往里面放东西,其他角色一定要等这个角色放完了东西才能往里面放东西。
std::mutex和std::atomic。

std::mutex

C++11提供了std::mutex在头文件mutex中,mutex类型的变量,在这个线程操作完它之前,其他线程都不能操作它,它是C++11中最基本的线程互斥量。
那我们我们要在游戏中实现上述的箱子就可以这样写:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class Box
{
public:
	int n = 0;//计数,箱子里有多少东西
};
std::mutex m;
void add(Box &b)
{
	m.lock();//开始操作时锁定m
	b.n++;
	m.unlock();//结束操作时,解锁m
}
int main()
{
	Box b=Box();
	thread t[100];
	for (int i = 0; i < 100; ++i)
	{
		t[i] = thread(add, ref(b));
	}
	for (int i = 0; i < 100; ++i)
	{
		t[i].join();
	}
	cout << b.n << endl;
}

输出结果:

100

std::mutex的常用成员方法

void lock();
//将mutex上锁。
//如果mutex已经被其它线程上锁,
//那么会阻塞,直到解锁;
//如果mutex已经被同一个线程锁住,
//那么会产生死锁。

void unlock();
//解锁mutex,释放其所有权。
//如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;
//如果mutex不是被此线程上锁,那么会引发未定义的异常。

bool try_lock();
//尝试将mutex上锁。
//如果mutex未被上锁,则将其上锁并返回true;
//如果mutex已被锁则返回false。

std::atomic

mutex有一个缺点,就是每次都需要上锁和解锁,非常麻烦,而且可能会拖慢程序的速度。
std::atomic可以让我们自己定义一些多线程需要使用的变量,并且它是线程安全的,也就是说它同时只能被一个线程操作,其他线程要等其操作完后,才能进行操作。

atomic英文本意为原子,在C++中原子操作是最小的且不可并行化的操作。

利用std::atomic,我们可以这样写:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class Box
{
public:
	atomic<int> n = 0;//<>里面放类型名
};
std::mutex m;
void add(Box &b)
{
	//m.lock();
	b.n++;
	//m.unlock();
}
int main()
{
	Box b;
	cout << b.n << endl;
	thread t[10000];
	for (int i = 0; i < 10000; ++i)
	{
		t[i] = thread(add, ref(b));
	}
	for (int i = 0; i < 10000; ++i)
	{
		t[i].join();
	}
	cout << b.n << endl;
}

输出结果:

10000

atomic类没有显式定义析构函数,也删除它自己的复制构造函数,它不能被简单复制。

后记

关于C++中的多线程,除了上面说的这些以外,还有std::async、std::future、std::promise等内容,在C++20的新标准中又新增了std::jthread,这些内容对于初学者来说太难了,所以暂不展开。

前面的路以后再来探索吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值