前言
作为编程开发人员,“多线程”、“高并发”、“多进程”在项目开发过程中,是十分常见的应用场景,本文会从以下几个方面展开:
- 线程-进程的区别联系;
- 目前c++中线程安全问题的常用解决办法;
线程与进程
先看一段最简单的代码:
#include <stdio.h>
#include <iostream>
#include <thread>
using namespace std;
int main()
{
std::cout << this_thread::get_id << endl;
}
功能很简单,就是打印当前线程的ID,输出如下:
根据上面的测试结果先引出两者的概念:
进程-就是应用程序,系统会为每个应用程序分配对应的内存空间供其使用。
线程-可以说是进程的子集,每个进程最少会拥有一个线程,这个线程就是主线程,其函数入口点就是main()函数。
1.两者的区别和联系
老生常谈-“线程作为系统调度的最小单元,进程是系统分配资源的最小单位”,我们在各种各样的博客都能上面这句话,那么具体它代表什么意思呢?
我的简单理解就是每次运行一个应用程序时,系统会为其分配一段固定大小的内存,也就是地址空间,而线程是系统调度的最小单元可以理解成线程是比进程颗粒度更小的单位,系统在不同任务切换的时候可以识别最小的单位就是线程。
线程安全问题
什么是线程安全问题,先看代码:
#include <stdio.h>
#include <iostream>
#include <thread>
using namespace std;
int number = 0;
void Func1() noexcept
{
for (auto i = 0; i < 1000000; i++)
{
++number;
}
}
int main()
{
thread thread1(Func1);
thread thread2(Func1);
thread1.join();
thread2.join();
cout << ++number << endl;
}
创建两个线程,每个线程都对全局变量进行+1的操作,最后输出结果,按照理想情况,最终结果应该是2000000,但是事实并不是如此:
结果只有1000000多,与我们想象的期望值大相径庭,这就是线程安全问题,其准确定义应该是:在多个线程运作中,其实际结果与期望结果并不相符的情况。
下面给出常见的保证线程安全的解决办法-
1.互斥锁
std::mutex名为互斥锁,其功能就是保证在其运作期间,其他线程想尝试lock这个锁的时候会产生阻塞,直到mutex被unlock之后,其他线程才能够lock锁。
#include <stdio.h>
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>
using namespace std;
int number = 0;
std::mutex mute;
void Func1()
{
for (auto i = 0; i < 1000000; i++)
{
mute.lock();
++number;
mute.unlock();
}
}
int main()
{
thread thread1(Func1);
thread thread2(Func1);
thread1.join();
thread2.join();
cout << ++number << endl;
}
我们在每次自增之前,都进行了lock操作,并在操作结束之后进行了unlock操作。
下面我们看一下结果:
结果正确。下面总结几个使用mutex使用需要注意的点:
**1. mutex在lock之后,一定要记得unlock,否则其他线程在申请lock的时候将永久阻塞;
2. lock和unlock中间包裹的代码区又被称为临界区,表示应受到保护的资源或者数据,其体量应该在满足保护性的前提下尽可能的小;
互斥锁的引申有std::lock_forward(析构函数自动释放锁) std::unique_lock(多功能锁,效率相对偏低) std::time_lock(超时锁) 这些类模板本质都是互斥锁,只不过在其上层进行了一系列的封装,感兴趣的童鞋可以自行搜索~
2.原子操作
2.1什么是原子操作
简单定义:最接近计算机硬件语言的操作,保证同一时刻只有一个线程能够对数据进行规定操作(+=、-+、*=、&=、前置++、后置++等)
2.2如何使用原子操作
更改上述代码如下:
std::atomic<int> number = 0;
运行,并输出结果如下:
需要注意的点:
1. atomic仅支持内置类型,不支持自定义类型,并且不支持float;
2. 原子操作相对于互斥锁效率较高,但仅仅限于特定类型的特定操作,无法对文件、资源进行保护;
小结
在多线程开发中,尤其要注意临界区的线程安全问题,常用的解决办法有:
- 互斥锁
- 原子操作
其中互斥锁通用性更广,但是效率较低,原子操作效率更高,但是只能满足内置数据类型的自增、自减等操作,且不支持自定义类型。
新人初次写blog,请多支持~