进程是资源调度的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
线程是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。
多个线程同时执行,由于处理器CPU个数有限,不同任务之间需要分时切换,这种切换是有开销的,操作系统需要保持切换时的各种状态。线程一多大量的时间用于切换,导致程序运行效率低下。
多进程实现并发
进程之间可以通过管道,文件,消息队列,共享内存进程通信
不同电脑之间通过socket通信
单进程中多线程
一个进程中的所有线程共享地址空间(共享内存),例如:全局变量,指针、引用都可以在线程之间传递,因此使用多线程的开销比较小。
共享内存引入数据不一致问题,需要使用锁进行解决
主线程执行完毕,整个进程也执行完毕,一般情况下,如果其他子线程没有执行完毕,那么这些子线程也会被操作系统强行终止。
子线程执行一个函数,函数运行完毕,子线程随之运行完毕。
一般情况下,如果想保持子线程的运行状态,需要保持主线程持续运行。
#include
#include
using namespace std;
void myprint(){
cout<< "子线程开始" <
#include
#include
using namespace std;
//使用类对象(可调用对象)创建线程
class TA{
public:
int &m_i; //不建议在线程中使用引用和指针,这样detach时,主线程结束,相关变量被释放,导致子线程中使用不可预知的值
TA(int &i): m_i(i){
cout<< "构造函数开始执行" <
ta在主线程结束之后被立即释放,为什么线程中的类对象还能正常调用类相关函数?
原因在于ta被拷贝了一份到子线程中,因此主线程退出清理ta并不影响子线程中的ta,通过拷贝构造函数可以验证这一过程。程序运行的结果如下:
#include
#include
using namespace std;
void myprint(const int & i, char *pmybuf){
cout << i <
多运行几次结果如下图,这就是一个典型的问题,原因在于子线程使用的detach,主线程已经退出了但是子线程还在执行,主线程退出了就会释放指针指向的内容,因此不会将字符串打印出来。
上述代码中,传入引用没有问题,但是不推荐,因为会创建一个新的引用地址,并不是真正意义上的引用,第二个参数为指针一定会有问题,因为该指针所指向的内容可能在主线程结束时被释放。
#include
#include
#include
using namespace std;
void myprint(const int & i, const string &pmybuf){
cout << i <
总结:
每个线程都有唯一的id。代码中使用std::this_thread::get_id()获取线程id
在线程中传入类对象时,不论函数形参是不是引用,都会得到一个新的拷贝对象,此时修改该对象并不影响实参。如何解决这个问题?
使用std::ref()函数,同时使用join()可以使传入的参数始终为同一个对象。
智能指针unique_ptr在传入线程时,需要使用std::move()函数将参数传入,此时线程形参接收到的地址和原地址一致,但是主线程中的智能指针为空,需要注意的是必须使用join(),使用detach()可能导致主线程将智能指针所指对象释放而在线程中使用的情况,造成未知错误。
#include
#include
#include
using namespace std;
void myprint(int i){
cout << "Thread start ..." < myThreads; //使用容器存储线程
//创建10个线程
for(int i = 0; i<10; i++){
myThreads.push_back(thread(myprint, i));
}
//让主线程等待10个线程运行完成
for(auto iter = myThreads.begin();iter!=myThreads.end(); ++iter){
iter->join();
}
cout << "Main Thread end..." << endl;
return 0;
}
下图是程序运行的结果,产生这种结果的原因是多线程的并发问题,多个线程同时执行,此时cpu的时间片会不停的轮转,所以说线程执行和结束的顺序会产生一些不同,但是可以清楚的看到不管顺序有多乱,但是因为使用的是join(),因此主进程是在子进程执行完之后才结束的。
只读数据是安全稳定的,直接读就行。
需要特殊的处理,避免程序崩溃。写的时候不能读,任意两个线程不能同时写,其他线程不能同时读。
#include
#include
#include
#include
using namespace std;
class A{
public:
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
Msg.push_back(i); //语句1
}
}
//读取消息
void readMsg(){
int curMsg;
for(int i = 0; i < 10000; i++){
if(!Msg.empty()){
//读取消息,读完删除
curMsg = Msg.front(); //语句2
Msg.pop_front();
cout << "消息已读出" << curMsg << endl;
}else{
//消息暂时为空
}
}
}
private:
std::list Msg; //消息变量
};
int main(){
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
多运行几次就会出现这样的报错,原因在于写入线程还没有将数据写入,读取线程就开始都线程,并且将读到的内容删除,这样就会导致崩溃。
上述代码在执行的过程中有问题,原因在于语句1在执行插入操作的时候,语句2可能进行读和删除的操作,导致线程运行不稳定。解决方法:引入**互斥量(Mutex)**的概念。
多个线程同时操作一个数据的时候,需要对数据进行保护,可以使用锁,让其中一个线程进行操作,其他线程处于等待状态。
互斥量可以理解成一把锁,多个线程尝试使用lock()函数对数据进行加锁,只有一个线程能锁定成功,其他线程会不断的尝试去锁数据,直到锁定成功。
互斥量使用需要小心,保护太多影响效率,保护不够会造成错误。
#include
#include
#include
#include
#include //引入互斥量头文件
using namespace std;
class A{
public:
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
my_mutex.lock();
Msg.push_back(i);
my_mutex.unlock();
}
}
//读取消息
void readMsg() {
int MsgCom;
for (int i = 0; i < 10000; i++) {
if (MsgLULProc(MsgCom)) {
//读出消息了
cout << "消息已读出" << MsgCom << endl;
}
else {
//消息暂时为空
cout << "消息为空" << endl;
}
}
}
//加解锁代码
bool MsgLULProc(int &command) {
my_mutex.lock(); //语句1
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
my_mutex.unlock(); //语句2
return true;
}
my_mutex.unlock();//语句3
return false;
}
private:
std::list Msg; //消息变量
std::mutex my_mutex; //互斥量对象
};
int main(){
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
这里将上述代码中的后面一句unlock注释掉,会产生下面的错误,因此lock和unlock必须是一一对应的,绝对不能单独使用。
互斥量是一个对象。
lock_guard的提出是为了防止程序员在使用lock()的时候忘记unlock()的情况,可以直接取代这两个函数,使用lock_guard之后不能再使用这两个函数。使用方式如下:
bool MsgLULProc(int &command) {
//my_mutex.lock(); //语句1
std::lock_guard lgmutex(my_mutex); //使用lock_guard代替lock
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
//my_mutex.unlock(); //语句2 *使用lock_guard之后不需要自己手动释放锁
return true;
}
//my_mutex.unlock();//语句3 *使用lock_guard之后不需要自己手动释放锁
return false;
}
代码中语句1在lock_guard的构造函数中执行,mutex::lock(),在其析构的时候执行mutex::unlock(),由此保证了互斥量的正常使用。
lock_guard缺点是没有lock和unlock使用灵活,需要手动析构。可以使用{}包裹,达到提前析构的目的。见如下代码。
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
//在{}包裹内,lock_guard在{}结束时会自动析构,相当于unlock
{
std::lock_guard lgmutex(my_mutex);
Msg.push_back(i);
}
}
return;
}
这里有一点需要额外注意:一旦使用了lock_guard之后,不能再使用lock和unlock。
死锁是指两个(多个)线程相互等待对方数据的过程,死锁的产生会导致程序卡死,不解锁程序将永远无法进行下去。
举个例子:两个线程A和B,两个数据1和2。线程A在执行过程中,首先对资源1加锁,然后再去给资源2加锁,但是由于线程的切换,导致线程A没能给资源2加锁。线程切换到B后,线程B先对资源2加锁,然后再去给资源1加锁,由于资源1已经被线程A加锁,因此线程B无法加锁成功,当线程切换为A时,A也无法成功对资源2加锁,由此就造成了线程AB双方相互对一个已加锁资源的等待,死锁产生。
理论上认为死锁产生有以下四个必要条件,缺一不可:
通过代码的形式进行演示,需要两个线程和两个互斥量。
#include
#include
#include
#include
#include //引入互斥量头文件
using namespace std;
class A {
public:
//插入消息,模拟消息不断产生
void insertMsg() {
for (int i = 0; i < 10000; i++) {
cout << "插入一条消息:" << i << endl;
my_mutex1.lock(); //语句1
my_mutex2.lock(); //语句2
Msg.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
//读取消息
void readMsg() {
int MsgCom;
for (int i = 0; i < 10000; i++) {
MsgCom = MsgLULProc(i);
if (MsgLULProc(MsgCom)) {
//读出消息了
cout << "消息已读出" << MsgCom << endl;
}
else {
//消息暂时为空
cout << "消息为空" << endl;
}
}
}
//加解锁代码
bool MsgLULProc(int &command) {
int curMsg;
my_mutex2.lock(); //语句3
my_mutex1.lock(); //语句4
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
private:
std::list Msg; //消息变量
std::mutex my_mutex1; //互斥量对象1
std::mutex my_mutex2; //互斥量对象2
};
int main() {
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
语句1和语句2表示线程A先锁资源1,再锁资源2,语句3和语句4表示线程B线索资源2再锁资源1,具备死锁产生的条件。
保证上锁的顺序一致。
功能:锁住两个或两个以上的互斥量,解决因lock()顺序问题导致的死锁问题。
在时间使用过程中,只要有一个互斥量没锁住,就会进行等待,等所有互斥量都做锁住时,程序才继续进行。
要么多个互斥量都锁住,要么都没锁住,只要有一个没锁成功,会立即释放所有已经加锁的互斥量。代码如下:
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
std::lock(my_mutex1, my_mutex2);//顺序无所谓
Msg.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
该函数一次能锁定多个互斥量,小心使用,多个互斥量的时候建议逐个lock()和unlock()。
std::adopt_lock是一个结构体对象,起一个标记作用就是表示这个互斥量已经lock(),不需要在std::lock_guard
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
std::lock(my_mutex1, my_mutex2);//顺序无所谓
//加上adopt_lock参数可以使互斥量不再次进行lock()
std::lock_guard lgmutex1(my_mutex1, std::adopt_lock);
std::lock_guard lgmutex2(my_mutex2, std::adopt_lock);
Msg.push_back(i);
}
}
std::unique_lock可以完全取代std::lock_guard,在使用上更加灵活。
#include
#include
#include
#include
#include //引入互斥量头文件
using namespace std;
class A {
public:
//插入消息,模拟消息不断产生
void insertMsg()
{
for (int i = 0; i < 10000; i++)
{
< lock_guard()给代码加锁演示代码
//std::unique_lock ul(my_mutex);
//Msg.push_back(i);
//cout << "插入一条消息:" << i << endl;
< adopt和try_to_lock两个函数的解释
//my_mutex.lock();
//std::unique_lock ul(my_mutex, std::adopt_lock); //std::adopt_lock标记已经加锁,前面需要lock
//std::unique_lock ul(my_mutex, std::try_to_lock); //std::try_to_lock尝试加锁,前面不能先lock
< defer_lock()初始化未加锁的mutex
//std::unique_lock ul(my_mutex, std::defer_lock); //std::defer_lock初始化一个未加锁的mutex,其之前也不能先lock,否则会报异常
//if (ul.try_lock()) { //判断是否拿到锁
// //拿到锁
// Msg.push_back(i);
//}
//else
//{
// //没有拿到锁
// cout << "写数据线程没有拿到锁" << endl;
//}
< release释放锁的所有权代码演示
//std::unique_lock ul(my_mutex); //演示所有权释放
//Msg.push_back(i);
//std::mutex * p_m = ul.release(); //接管的互斥量指针需要手动释放已加锁的互斥量
//p_m->unlock();
< 转移锁代码演示
std::unique_lock<std::mutex> ul(my_mutex); //演示所有权转移,需要使用移动语义
std::unique_lock<std::mutex> ul2 = std::move(ul);
Msg.push_back(i);
ul2.unlock();
}
}
//读取消息
void readMsg() {
int MsgCom;
for (int i = 0; i < 10000; i++) {
if (MsgLULProc(MsgCom)) {
//读出消息了
cout << "消息已读出" << MsgCom << endl;
}
else {
//消息暂时为空
cout << "消息为空" << endl;
}
}
}
//加解锁代码
bool MsgLULProc(int &command) {
std::unique_lock<std::mutex> ul(my_mutex);
//延迟1s
std::chrono::milliseconds dura(1000);
std::this_thread::sleep_for(dura);
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
return true;
}
return false;
}
private:
std::list<int> Msg; //消息变量
std::mutex my_mutex; //互斥量对象
};
int main() {
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
单例类:指的是在程序中该类的实例只存在一个,其实现通常需要满足以下三个条件:
实例代码7-1:
#include
#include
#include
#include
#include //引入互斥量头文件
using namespace std;
class MyCAS{
private:
MyCAS(){}; //私有化构造函数,保证该类无法被new或者以MyCAS m方式生成实例
private:
static MyCAS * m_instance; //类实例
public:
static MyCAS * GetInstance(){ //返回类实例
if(m_instance == NULL){
m_instance = new MyCAS();
static GCclass gc;//用于回收上一句new产生的内存,在程序结束时会调用其析构函数
}
return m_instance;
}
//类中嵌套回收类,用于回收单例类实例,防止出现内存泄露
class GCclass{
~GCclass(){
if(MyCAS::m_instance){
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};
};
//初始化单例类实例
MyCAS* MyCAS::m_instance = NULL;
int main(){
MyCAS *p_a = MyCAS::GetInstance(); //获取单例类对象,最好在使用多线程之前加载单例类实例
return 0;
}
在多线程中,如果多个类同时创建单例类对象,需要进行互斥,因此引入互斥量进行加锁。在代码7-1中,加入互斥量并修改函数GetInstance():
std::mutex my_mutex; //引入互斥量
static MyCAS * GetInstance(){ //返回类实例
if(m_instance == NULL){//双重锁定,保证有一次实例化之后,不会再进行资源锁定
std::unique_lock myul(my_mutex);
if(m_instance == NULL){
m_instance = new MyCAS();
static GCclass gc;//用于回收上一句new产生的内存,在程序结束时会调用其析构函数
}
}
return m_instance;
}
该函数的功能是保证在多线程中,某一个函数只能被执行一次,可以解决7.2中GetInstance()函数被多次调用的问题。需要与标记std::once_flag配合使用。
在代码7-1中,进行如下修改:
std::once g_flag; //引入once_flag标记
//增加函数
static void CreateInstance(){
m_instance = new MyCAS();
static GCclass gc;
}
//返回类实例
static MyCAS * GetInstance(){
std::call_once(g_flag,CreateInstance);
return m_instance;
}
运行结果如下图所示:使用call_once保证只产生一个单例类
条件变量可以使用通知的方式实现线程同步,其履行发送者或者接受者的角色。
实例代码8-1:
#include //需要引入头文件
#include
#include
#include
using namespace std;
class A{
public:
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
std::unique_lock myul; //加锁
cout<< "插入一条消息:" << i << endl;
Msg.push_back(i);
myul.notify_one(); //语句1
}
}
//读取消息
void readMsg(){
while(true){
std::unique_lock myul; //加锁
my_cond.wait(myul,[this]{ //语句2
if(!Msg.empty())
return true;
return false;
});
}
}
//加解锁代码
bool MsgLULProc(int &command){
int curMsg;
my_mutex.lock(); //语句1
if(!Msg.empty()){
//读取消息,读完删除
curMsg = Msg.front();
Msg.pop_front();
cout << "消息已读出" << curMsg << endl;
my_mutex.unlock(); //语句2
return true;
}
my_mutex.unlock();//语句3
return false;
}
private:
std::list Msg; //消息变量
std::mutex my_mutex; //互斥量对象
std::condition_variable my_cond; //条件变量
}
int main(){
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
语句2中使用wait()函数进行等待,第一个参数为unique_lock()类对象,第二个参数为lambda表达式,如果是true则直接返回,程序继续往下执行,如果为false,则将互斥量解锁,并阻塞到本行,直到有线程调用notify_one()成员函数将其唤醒(如语句2)。如果没有第二个参数,则默认为false。
**注意:**当wait()被唤醒后,会尝试重新拿锁,拿到则程序继续往下执行,notify_one()是唤醒一个处于wait状态的线程,如果有多个线程,则不确定会唤醒哪一个,而notify_all()是唤醒所有处于wait状态的线程。另外一点是,如果在notify的过程中,没有线程处于wait状态,则这个通知会丢失。
本文转载自GitHub的一位大佬,本人做了一些增减。全文代码地址:https://github.com/wlonging/ThreadLearning,包含线程相关知识和一个线程池的项目。
代码都经过实际运行测验,如有问题,欢迎大家留言交流。