• C++11 多线程 (上)


    一.进程与线程简单介绍

    进程是资源分配的基本单位,是程序的一次执行过程,结合实际,比如打开一个QQ,就是启动了一个进程,在vs里面运行一个程序也是一个进程。线程是调度的基本单位,引入线程的目的就是要提高程序运行的效率,一个进程可以包括多个线程,但是一个进程只有一个主线程,打开电脑的任务管理器可以很清楚的看到当前在运行的进程和线程

    二.线程的创建和join()、joinable(),detach()

    说明:创建多线程需要引入头文件#include,由于程序在执行的时候是从主线程开始执行,存在一种情况:主线程执行完毕,而子线程还在执行中,此时就会报错

    下面就让我们来引入join()来解决这个问题,引入后就会完美的解决这个问题,结果如下:

    所以join()的作用是:阻塞主线程,让主线程等待子线程,然后一起执行,如果主线程不等待子线程,就会出现上面的那种错误。

    接下来就来了解一下线程的创建方式吧

    (1)通过函数对象创建子线程

    其实上面那个例子就是通过函数对象创建的子线程

    1. #include<iostream>
    2. using namespace std;
    3. #include<thread>
    4. void myprint()
    5. {
    6. cout << "我创建的线程开始执行" << endl;
    7. cout << "我创建的线程执行完毕" << endl;
    8. }
    9. int main()
    10. {
    11. thread mythread(myprint);
    12. mythread.join();
    13. cout << "I love China!" << endl;
    14. return 0;
    15. }

    这里myprint()就是一个函数对象,这里代表线程的入口,在实际应用中还会用到一个joinable()来判断线程是否可以join(),如果可以就会返回true,否则返回false

    1. int main()
    2. {
    3. thread mythread(myprint);
    4. if (mythread.joinable())
    5. mythread.join();
    6. else
    7. cout << "join false" << endl;
    8. cout << "I love China!" << endl;
    9. return 0;
    10. }

    (2)通过类对象创建线程

    1. class A
    2. {
    3. public:
    4. A()
    5. {
    6. cout << "A的构造函数调用" << endl;
    7. }
    8. A(const A &a)
    9. {
    10. cout << "A的拷贝构造函数调用" << endl;
    11. }
    12. void operator()()//这里不能带参数
    13. {
    14. cout << "我创建的线程开始执行" << endl;
    15. cout << "我创建的线程执行完毕" << endl;
    16. }
    17. ~A()
    18. {
    19. cout << "A的析构函数调用" << endl;
    20. }
    21. };
    22. int main()
    23. {
    24. A a;
    25. thread mythread1(a);
    26. mythread1.join();
    27. cout << "I love China!" << endl;
    28. return 0;
    29. }

    注意类中的operator()那里是没有参数的。首先创建一个类对象,然后把这个类对象作为线程的入口来创建一个线程

    从运行结果可以看出此处会拷贝一份类对象作为入口参数,这显然不是高效的写法,这会浪费内存,我们对其进行简单修改来解决这个问题:

    1. int main()
    2. {
    3. A a;
    4. thread mythread1(ref(a));
    5. mythread1.join();
    6. cout << "I love China!" << endl;
    7. return 0;
    8. }

    加上一个ref就可以解决这个问题,不需要多创建一个变量,可以节省内存

    (3)通过lambda表达式创建线程

    1. auto thread_my = []
    2. {
    3. cout << "我创建的线程开始执行" << endl;
    4. cout << "我创建的线程执行完毕" << endl;
    5. };//注意这里是有;的
    6. int main()
    7. {
    8. thread mythread(thread_my);
    9. mythread.join();
    10. cout << "I love China!" << endl;
    11. return 0;
    12. }

    运行结果如下:

    在实际的操作中,前面的两中创建方式较为常用。

    还剩下一个detach(),下面会用到,此处先简单解释一下,dedach()就是会把子线程挂到后台去执行,不会和主线程一起执行,这里面就会涉及到很多的问题。

    三.传参数问题

    在上面我们讨论的线程都是没有参数的,下面让我们来看看带参数的线程创建

    (1)传递临时对象作为参数

    当使用join()的时候程序不会出现问题,当使用detach()的时候,就会出现主线程已经执行完了,主线程中的临时变量也会销毁,但是此时子线程依旧在执行,而且子线程中 传入了已经销毁的参数。当使用detach()的时候每次运行会得到不同的结果,运行比较混乱

    1. void myprint(string s,const int &i)
    2. {
    3. cout << "我创建的线程开始执行" << endl;
    4. cout << s << endl;
    5. cout << i << endl;
    6. cout << "我创建的线程执行完毕" << endl;
    7. }
    8. int main()
    9. {
    10. int i = 10;
    11. int& val = i;
    12. string s = "This is a thread";
    13. thread mythread(myprint, s, i);
    14. mythread.detach();
    15. cout << "I love China!" << endl;
    16. return 0;
    17. }

    要传入的参数直接放在创建线程入口后边,从此处的两个临时变量分析,i和s在主线程执行完之后机会销毁,那么传入子线程的是什么呢,实际还是用join()最靠谱,不会出现这种问题将detach()换成 join(),线程将会顺利执行

    (2)类临时对象作为参数与get_id()

    将上面的类A进行简单的修改,并加入get_id()来获取线程的ID来观察线程的创建时机,修改后的类A如下:

    1. class A
    2. {
    3. public:
    4. A(int a):m_a(a)
    5. {
    6. cout << "A的构造函数调用,其线程ID "<<this_thread::get_id() << endl;
    7. }
    8. A(const A &a):m_a(a.m_a)
    9. {
    10. cout << "A的拷贝构造函数调用,其线程ID "<< this_thread::get_id() << endl;
    11. }
    12. ~A()
    13. {
    14. cout << "A的析构函数调用,其线程ID "<< this_thread::get_id() << endl;
    15. }
    16. public:
    17. mutable int m_a;//即使a被const修饰也可以去修改其值
    18. };
    19. void myprint(const A &a, const int& i)
    20. {
    21. cout << "我创建的线程开始执行,线程ID "<<this_thread::get_id() << endl;
    22. a.m_a = 100;
    23. cout << i << endl;
    24. cout << "我创建的线程执行完毕" << endl;
    25. }
    26. int main()
    27. {
    28. A a(10);
    29. int i = 5;
    30. int& val = i;
    31. thread mythread(myprint, ref(a), val);
    32. mythread.join();
    33. cout << a.m_a << endl;
    34. cout << "主线程ID " << this_thread::get_id() << endl;
    35. return 0;
    36. }

    我们通过this_thread::get_id()来获取线程的ID。我们在类A中加入了一个mutable的成员变量,注意这里面的这些写法,减少了拷贝构造函数的调用,提高了执行效率

    ref在上面已经说明过了,其实一个函数能够使传过去的类对象省去拷贝构造函数,提高效率。除非在万不得已的情况下再去使用detach(),否则容易出现临时变量已经被销毁又被调用的情况。

    四.创建多个线程

    我们可以通过stl容器来帮助创建多个线程,在通过迭代器让每一个子线程都join(),举例如下:

    1. void myprint(int n)
    2. {
    3. cout << "myprint线程开始执行了 " << n << "myprint 线程ID " << this_thread::get_id() << endl;
    4. cout << "myprint线程执行完毕" << "myprint 线程ID " << this_thread::get_id() << endl;
    5. }
    6. int main()
    7. {
    8. vector<thread>v;
    9. for (int i = 0; i < 10; i++)
    10. {
    11. v.push_back(thread (myprint, 1));
    12. }
    13. for (auto it = v.begin(); it != v.end(); it++)
    14. {
    15. it->join();
    16. }
    17. cout << "主线程ID" << this_thread::get_id() << endl;
    18. cout << "I love China!" << endl;
    19. return 0;
    20. }

    运行结果如下:

    通过观察运行结果我们发现这个运行打印过程是混乱的

    通过创建的多个线程访问只读的数据:定义一个全局的共享容器,使其可以被所有的线程访问,通过修改上面的代码,注意,此处的vector是全局变量,尝试让每个线程都访问共享数据。

    1. vector<int>share_num{1,2,3};//定义一个全局的共享数据
    2. void myprint(int n)
    3. {
    4. cout << "myprint线程开始执行了 " << n << "myprint 线程ID " << this_thread::get_id() << " " << share_num[0] << share_num[1] << share_num[2] << endl;
    5. cout << "myprint线程执行完毕" << "myprint 线程ID " << this_thread::get_id() << endl;
    6. }

     

    通过观察运行结果我们发现,这个只读数据是安全的,不需要什么特殊的手段,可以直接读取

    五.多个进程互斥访问lock(),unlock(),lockguard()

    对于多个进程互斥访问问题,我们需要引入互斥量的概念,使用时需要引用头文件#include,mutex本质上是一个类,加锁lock()和解锁unlock()是它的两个成员函数,对于lock和unlock的使用必须成对使用,尤其是在分支语句中的成对使用一定要理解好。下面我们从一个类的成员函数作为线程入口开始讨论。

    1. class Test
    2. {
    3. private:
    4. list<int>l1;
    5. public:
    6. void in()//往容器里写入数据
    7. {
    8. for (int i = 0; i < 100000; i++)
    9. {
    10. cout << "in执行,插入数据 " << i << endl;
    11. l1.push_back(i);
    12. }
    13. }
    14. void out()//从容器里面读出数据
    15. {
    16. for(int i=0;i<100000;i++)
    17. {
    18. if(!l1.empty())
    19. {
    20. int costant = l1.front();
    21. l1.pop_front();
    22. cout << "取出一个元素 " << costant << endl;
    23. }
    24. else
    25. {
    26. cout << "l1为空" <
    27. }
    28. }
    29. }
    30. };
    31. int main()
    32. {
    33. Test t;
    34. thread mythread2(&Test::out, &(t));//此处使用ref也行
    35. thread mythread1(&Test::in, &(t));
    36. mythread1.join();
    37. mythread2.join();
    38. return 0;

    设置一个list来保存数据,我们都知道对于同一个list容器是不能同时进行读写操作的,我们定义了一个in函数表示写入,out函数表示读出,让他们作为线程的入口,在老版本的编译器会直接报错,在vs2022上面经过我的多次测试发现有时候它会卡在某一个阶段然后直接终止程序运行

    我们设置的两个读写循环都是100000次的,此处才运行到20000多次就结束了,说明程序是存在问题的,所以接下来我们就要引入互斥量来让读写分开,在同一时刻只能读或者只能写

    给Test类加入一个互斥量mutex,通过mutex的加锁、解锁操作来限制读写

    1. private:
    2. list<int>l1;
    3. mutex mymutex;

    我们把out函数中的判断条件单独拿出来作为一个新的函数,方便加锁解锁,最终将类修改成如下

    1. class Test
    2. {
    3. private:
    4. list<int>l1;
    5. mutex mymutex;
    6. public:
    7. void in()//往容器里写入数据
    8. {
    9. for (int i = 0; i < 100000; i++)
    10. {
    11. cout << "in执行,插入数据 " << i << endl;
    12. mymutex.lock();//加锁
    13. l1.push_back(i);
    14. mymutex.unlock();//解锁
    15. }
    16. }
    17. bool isGet(int &command)
    18. {
    19. mymutex.lock();//加锁
    20. if (!l1.empty())
    21. {
    22. command = l1.front();
    23. l1.pop_front();
    24. //cout << "取出一个元素 " << command << endl;
    25. mymutex.unlock();//上面加锁进入这个分支以后需要解锁
    26. return true;
    27. }
    28. mymutex.unlock();//解锁
    29. return false;
    30. }
    31. void out()//从容器里面读出数据
    32. {
    33. int command = 0;
    34. for(int i=0;i<100000;i++)
    35. {
    36. bool result = isGet(command);
    37. if (result)
    38. {
    39. cout << "out执行了,取出一个元素" << command << endl;
    40. }
    41. else
    42. {
    43. cout << "l1为空" <
    44. }
    45. }
    46. }
    47. };

    特别注意理解lock()和unloc()的成对使用,尤其是在分支语句就像上面的if条件判断中。这样加锁后程序就会安全的执行,让读写互不干扰

    读写交替进行,程序正常执行。切记lock和unlock一定要成对使用

    在这里我们注释了一个unlock发现程序直接报错,足以表示lock和unlock成对使用的重要性,可能大家会觉得这样写lock和unlock在日常的使用中,会很容易出现只写了一个lock忘记unlock的问题,这时候就需要引入我们的lock_guard<>(),lock_guard会自帮助我们完成加锁和解锁这两个操作,使用lock_guard之后就不能使用lock和unlock了,lock_guard的执行原理是它会在它的构造函数中调用mutex的lock,在析构函数中调用unlock,就是只能在return的时候才能解锁。下面我们把in函数进行修改

    1. void in()//往容器里写入数据
    2. {
    3. for (int i = 0; i < 100000; i++)
    4. {
    5. cout << "in执行,插入数据 " << i << endl;
    6. lock_guard myguard(mymutex);
    7. l1.push_back(i);
    8. }
    9. }

    因为lock_guard<>()的析构时间问题,会让它使用起来不是那么灵活,不像lock()和unlock()可以随时加锁和解锁

    我们可以通过加大括号的方式来改变lock_guard的生命周期,对in函数在进行修改如下,此时,走出这个大括号,它就会进行unlock();

    1. void in()//往容器里写入数据
    2. {
    3. for (int i = 0; i < 100000; i++)
    4. {
    5. cout << "in执行,插入数据 " << i << endl;
    6. {
    7. lock_guard myguard(mymutex);
    8. l1.push_back(i);
    9. }
    10. }
    11. }

  • 相关阅读:
    Linux系统安装DB2数据库的详细步骤
    深度解析NLP文本摘要技术:定义、应用与PyTorch实战
    记录一次htonl和ntohl的使用方法和差别
    APISpace 成语大全API
    17.适配器模式(Adapter)
    领英工具--领英精灵功能解析分享
    优质笔记软件综合评测和详细盘点 Notion、Obsidian、RemNote、FlowUs
    音视频开发—V4L2介绍,FFmpeg 打开摄像头输出yuv文件
    闭包问题优化
    网络套接字编程(三)
  • 原文地址:https://blog.csdn.net/qq_66157971/article/details/132632220