【Linux】从零开始熟悉多线程 --- 线程控制

瑞星  金牌会员 | 2024-7-19 17:57:42 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 646|帖子 646|积分 1938


   在这个浮躁的时代      只有自律的人才能脱颖而出        -- 《觉醒年代》       

  
1 知识回首

上一篇文章中,我们通过对地址空间的再次学习来熟悉了线程:

  • 物理空间不是一连的,是4kb的内存块(页框)组成的。
  • 页表映射是通过虚拟地址来索引物理地址:

    • 虚拟地址共32位:前10位用来索引页目次中的元素(页表),中间10位用来索引页表中的对应的元素(页框),后12位用来索引页框中的每一个字节

  • 虚拟地址本质是一种资源,可以举行分配!对一个历程的数据举行分配实行,就是多线程的本质!
  • Linux中的线程是通过历程模仿的(并没有单独计划出一个单独的线程模块)
  • 历程中可以有多个历程(之前学习的是历程的特殊环境),他们共用一个地址空间。历程从内核来看,是负担分配系统资源的基本实体!
  • Linux中的实行流是线程 ,CPU看到的实行流 <= 历程
历程与线程需要注意:

  • 线程的调度本钱比历程低很多,是由于硬件缘故原由:CPU中存在一个cache会储存热门数据(历程相干数据) ,要访问数据时,会先在cache中探求,假如命中直接访问,反之举行置换。切换历程需要更换热门数据,切换线程不需要切换。
  • 线程的结实性很差!一个线程出错会导致整个线程退出,而不同历程是独立的互不影响!历程和线程各有特长!
  • 线程的本质是代码块!只使用函数的对应代码,即拿页表的一部分来实行!!!
  • 线程的使用场景多为计算密集型和IO密集型,可以充分使用CPU的并行本事!
同一个历程中的线程虽然共享一个地址空间,但是照旧有独属于自己的一些东西:

  • 一组寄存器:在硬件中储存上下文数据,包管线程可以动态并行运行!
  • 栈空间:线程中可以处理自己的临时变量,临时变量储存在自己独立的栈区,可以独立完成使命。
  • 线程ID
  • errno信号屏蔽字
  • 调度优先级
复习的差不多了,我们了解了线程的基本概念,接下来就要开始学习怎样管理线程 — 线程控制。根据我们之前学习的历程控制,大概可以估计一下线程控制的基本接口:线程创建 , 线程等候 , 线程退出…
2 线程控制

2.1 线程创建

万事开头难,我们先来看线程怎么创建:
  1. PTHREAD_CREATE(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CREATE(3)
  2. NAME
  3.        pthread_create - create a new thread
  4. SYNOPSIS
  5.        #include <pthread.h>
  6.        int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
  7.                           void *(*start_routine) (void *), void *arg);
  8.        Compile and link with -pthread.
复制代码
pthread_create是创建线程的接口,内里有4个参数:

  • pthread_t *thread :输出型参数,线程ID。
  • const pthread_attr_t *attr :线程属性(优先级,上下文…),默认传入nullptr
  • void *(*start_routine) (void *) : 函数指针,线程需要实行的函数地址。
  • void arg:想要传入到线程的信息,可以传入int,string地址或者传入一个类对象的地址。
再来看返回值:
  1. RETURN VALUE
  2.        On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
复制代码
pthread系列的函数的返回值是都是一样的:成功返回0,反之返回错误码!
2.2 线程等候

学习历程的时候,假如历程创建出来了,但是不举行等候,就拿不到退出信息,还会造成僵尸历程,进而造成内存泄漏。同样线程也需要举行等候。由主线程来等候新线程
  1. PTHREAD_JOIN(3)                                                     Linux Programmer's Manual                                                    PTHREAD_JOIN(3)
  2. NAME
  3.        pthread_join - join with a terminated thread
  4. SYNOPSIS
  5.        #include <pthread.h>
  6.        int pthread_join(pthread_t thread, void **retval);
  7.        Compile and link with -pthread.
复制代码
这个函数内里有2个参数:

  • pthread_t thread:需要举行等候的线程ID
  • void **retval: 获取的返回信息
2.3 线程停止

牢记:main线程结束那么历程结束,所以肯定要包管main线程最后退出。

  • 最简朴的线程停止是线程函数返回return !
  • 切记不要使用exit(),我们在历程控制中学习过exit()可以退出历程,但是要注意线程是在一个历程中讨论的,新线程假如使用了exit()那整个历程就退出了!exit()不可以用来停止线程
  • 操纵系统也给我们提供了线程停止的接口:
  1. PTHREAD_CANCEL(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CANCEL(3)
  2. NAME
  3.        pthread_cancel - send a cancellation request to a thread
  4. SYNOPSIS
  5.        #include <pthread.h>
  6.        int pthread_cancel(pthread_t thread);
  7.        Compile and link with -pthread.
复制代码
通过这个参数,可以看出来这是个很简朴的接口,停止对应tid的线程。只要线程存在,并且知道tid , 就可以停止线程(可以自己停止自己)。线程停止的返回值是一个整数!
3 测试运行

3.1 小试牛刀 — 创建线程

我们举行一个简朴的测试,来使用这两个接口:
注意,使用线程库的接口需要动态链接g++ -o $@ $^ -std=c++11 -lpthread
  1. #include <iostream>
  2. #include <pthread.h>
  3. #include <unistd.h>
  4. #include <ctime>
  5. #include <string>
  6. // 测试 1
  7. void *ThreadRun(void *args)
  8. {
  9.     std::cout << "name: " << *(std::string*)args << " is running"<< std::endl;
  10.     sleep(1);
  11.     std::string* ret = new std::string(*(std::string*)args + "finish...") ;
  12.     return (void*)ret;
  13. }
  14. int main()
  15. {
  16.     // 创建一个新线程
  17.     // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
  18.     pthread_t tid;
  19.     std::string name = "thread - 1";
  20.     pthread_create(&tid, nullptr, ThreadRun, &name);
  21.     //进程等待
  22.     //int pthread_join(pthread_t thread, void **retval);
  23.     std::string *ret = nullptr;
  24.     pthread_join(tid, (void**)&ret);
  25.     std::cout << *(std::string*)ret << std::endl;
  26.     return 0;
  27. }
复制代码
编译运行一下,我们可以看到:

新线程完成了使命!
问题 1 : main线程和new线程谁先运行? 不确定,和历程的调度方式同等,由具体环境来定。
问题 2 : 我们期望谁先退出?肯定是main线程,所以就有join来举行等候,壅闭等候线程退出。假如不举行join,就会造成雷同僵尸历程的环境(内存泄漏)!
问题 3 :tid是什么样子的,我们可不可以看一看?固然可以:
  1. std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
复制代码
这样就可以打印出来tid的十六进制:

这数字好像和lwp不同等啊

为什么tid这么大?其实tid是一个虚拟地址!!!
3.2 探幽析微 — 理解线程参数

问题 4 : 全面看待线程函数传参。上面我们的程序传入了name变量的地址,让线程获取了对应的名字。假如想要传入多个变量或方法,可以传入类对象的地址:
  1. class ThreadData{public:    std::string name;    int num;};vvoid *ThreadRun(void *args){    ThreadData* td = static_cast<ThreadData*>(args);    std::cout << "name: " << td->name << " is running" << std::endl;    std::cout << "num: " << td->num << std::endl;     sleep(1);    std::string *ret = new std::string(*(std::string *)args + "finish...");    return (void *)ret;}std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
  7. int main(){    // 创建一个新线程    // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);    pthread_t tid;    // std::string name = "thread - 1";    ThreadData td;    td.name = "thread - 1";    td.num = 100;    pthread_create(&tid, nullptr, ThreadRun, &td);    // 查看tid    sleep(1);    std::cout << "tid: " << ToHex(tid) << std::endl;    // 历程等候    // int pthread_join(pthread_t thread, void **retval);    std::string *ret = nullptr;    pthread_join(tid, (void **)&ret);    std::cout << *(std::string *)ret << std::endl;    return 0;}
复制代码
这样就可以传入多个变量:

所以这个void*的变量是可以传入任何地址的,肯定要想到可以传入类对象。但是刚写的有些问题,我们上面的写法是在主线程的栈区创建变量,让新线程读取主线程的栈,不太合适(粉碎了肯定独立性)!假如多个变量都传入了这个变量,那么修改一个就会造成所以的线程中的数据都发生改变!!!这可不可!保举写:
  1.         ThreadData* td = new ThreadData();
  2.     td->name = "thread - 1";
  3.     td->num = "100";
  4.     pthread_create(&tid, nullptr, ThreadRun, td);
复制代码
这是在堆区举行开辟空间,然后将该空间交给新线程来管理!就不会出现这样的问题了!以后我们都使用这种方式来传递参数!!!
3.3 小有心得 — 探索线程返回

问题 5 :线程的返回值输出型参数void** retval,他需要我们传递一个void*变量,然后返回值就交给了void*变量!这个过程就是对一个指针举行改变其指向的内容的操纵。
下面是一个让新线程举行加法工作的程序
  1. void *ThreadRun(void *args)
  2. {
  3.     ThreadData* td = static_cast<ThreadData*>(args);
  4.     std::cout << "name: " << td->name << " is running" << std::endl;
  5.     std::cout << "num: " << td->num << std::endl;
  6.     sleep(1);
  7.     delete td;
  8.     //返回值
  9.     std::string *ret = new std::string(*(std::string *)args + "finish...");
  10.     return (void *)ret;
  11. }
复制代码
这就将void*变量返回给&(void* ret)变量,让ret指向对应的堆区。这就雷同int a放入int * 中就可以改变a的值
问题 5 :怎样全面的看待线程的返回。我们知道假如一个线程出现问题,整个历程就会退出。所以线程的返回只有正常的返回,没有异常的返回,出现异常整个历程会直接退出,根本没有返回错误信息的机会!和传入参数音参数一样,我们也可以返回一个类对象来传递多个变量。
  1. #include <iostream>#include <pthread.h>#include <unistd.h>#include <ctime>#include <string>// 测试 1class ThreadData{public:    std::string name;    int num1;    int num2;};class ThreadResult{public:    std::string name;    int num1;    int num2;    int ans;};void *ThreadRun(void *args){    ThreadData *td = static_cast<ThreadData *>(args);    std::cout << "name: " << td->name << " is running" << std::endl;    std::cout << "num1: " << td->num1 << " num2: " << td->num2 << std::endl;    sleep(1);    ThreadResult *ret = new ThreadResult();    ret->name = td->name;    ret->num1 = td->num1;    ret->num2 = td->num2;    ret->ans = td->num2 + td->num1;    delete td;    return (void *)ret;}std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
  7. int main(){    // 创建一个新线程    // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);    pthread_t tid;    // std::string name = "thread - 1";    ThreadData *td = new ThreadData();    td->name = "thread - 1";    td->num1 = 100;    td->num2 = 88;    pthread_create(&tid, nullptr, ThreadRun, td);    // 查看tid    sleep(1);    std::cout << "tid: " << ToHex(tid) << std::endl;    // 历程等候    // int pthread_join(pthread_t thread, void **retval);    ThreadResult *ret = nullptr;    pthread_join(tid, (void **)&ret);        std::cout << ret->num1 << " + " << ret->num2 << " = " << ret->ans << std::endl;    return 0;}
复制代码
来看返回值:

我们成功获取了新线程中设置的返回值!非常nice!
3.4 求索无厌 — 实现多线程

问题 6 :上面只是创建了单独的一个线程,那怎样创建多线程呢?
可以通过维护一个vector数组来对tid举行同一管理
  1. void *ThreadRun(void *args){    std::string name = static_cast<const char *>(args);    while (true)    {        std::cout << name << "is running ..." << std::endl;        sleep(1);    }    return (void *)0;}std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
  7. const int num = 10;int main(){    std::vector<pthread_t> tids;    for (int i = 0; i < num; i++)    {        // 1. 线程ID        pthread_t tid;        // 2. 线程名字        char* name = new char[128];        snprintf(name, 128, "thread - %d", i + 1);        pthread_create(&tid, nullptr, ThreadRun, name);        //保存所有线程的ID        tids.push_back(tid) ;    }    //join    sleep(100);    return 0;}
复制代码

这样就创建出了10个新线程,但是我们看这些新线程的的名字好像不太对:

怎么不是1 - 10???完全是乱的!因为线程谁先被调度运行不确定!而我们传入的名字是在主线程的栈区域,大概在新线程还没有调度,name就已经在主线程中被覆盖了!解决办法很简朴,我们创建在堆区就可以了
  1. for (int i = 0; i < num; i++)
  2.     {
  3.         // 1. 线程ID
  4.         pthread_t tid;
  5.         // 2. 线程名字
  6.         //在堆区进行创建。防止被重写覆盖
  7.         char* name = new char[128];
  8.         snprintf(name, 128, "thread - %d", i + 1);
  9.         pthread_create(&tid, nullptr, ThreadRun, name);
  10.         pids.push_back(tid) ;
  11.     }
复制代码

这样就整齐多了!
接下来就要举行等候:
我们已经通过vector容器来维护了创建所有线程的tid,所以只需要对所有的tid举行join就好了!
  1. void *ThreadRun(void *args){    std::string name = static_cast<const char *>(args);    while (true)    {        std::cout << name << "is running ..." << std::endl;        sleep(3);        break;    }    return nullptr;}std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
  7. const int num = 10;int main(){    std::vector<pthread_t> tids;    for (int i = 0; i < num; i++)    {        // 1. 线程ID        pthread_t tid;        // 2. 线程名字        char* name = new char[128];        snprintf(name, 128, "thread - %d", i + 1);        pthread_create(&tid, nullptr, ThreadRun, name);        //保存所有线程的ID        tids.push_back(tid) ;    }    //join    for (auto tid : tids)    {        pthread_join(tid , nullptr);        std::cout << ToHex(tid) << " quit..." << std::endl;    }}
复制代码
来看运行结果:

非常好!!!
我们也可以通过返回值来获取线程的名字:
  1.     for (auto tid : tids)
  2.     {
  3.         void* name = nullptr;
  4.         pthread_join(tid , &name);
  5.         std::cout << (const char*)name<< " quit..." << std::endl;
  6.         delete (const char*)name;
  7.     }
复制代码
非常优雅!

3.5 返璞归真 — 线程停止与线程分离

问题 7 :线程停止的返回值
我们来看看通过线程停止接口停止的线程返回值是什么样的:
  1. void *ThreadRun(void *args){    std::string name = static_cast<const char *>(args);    while (true)    {        std::cout << name << "is running ..." << std::endl;        sleep(3);        //break;    }    return args;}std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
  7. const int num = 10;int main(){    std::vector<pthread_t> tids;    for (int i = 0; i < num; i++)    {        // 1. 线程ID        pthread_t tid;        // 2. 线程名字        char* name = new char[128];        snprintf(name, 128, "thread - %d", i + 1);        pthread_create(&tid, nullptr, ThreadRun, name);        //保存所有线程的ID        tids.push_back(tid) ;    }    //join    sleep(3);        for (auto tid : tids)    {        pthread_cancel(tid);        std::cout <<  " cancel: " << ToHex(tid) << std::endl;        void* ret= nullptr;        pthread_join(tid , &ret);        std::cout << (long long int)ret << " quit..." << std::endl;    }    return 0;}
复制代码

可以看的,被phread_cancel()停止的线程的返回值是 -1!这个 -1其实是宏定义#define PTHREAD_CANCELED ((void *) -1)。线程停止的方式有三种:

  • 线程函数 return
  • pthread_cancel 新线程退出结果为-1
  • pthread_exit
问题 8 :可不可以不通过join线程,让他实行完就退出呢,固然可以!
这里需要线程分离接口:
  1. PTHREAD_DETACH(3)                                                   Linux Programmer's Manual                                                  PTHREAD_DETACH(3)
  2. NAME
  3.        pthread_detach - detach a thread
  4. SYNOPSIS
  5.        #include <pthread.h>
  6.        int pthread_detach(pthread_t thread);
  7.        Compile and link with -pthread.
复制代码
通过这个接口,分离出去的线程依然属于历程内部,但不需要被等候了。举个例子,之前再讲线程与历程的关系时,我们把不同的线程比作家庭成员,做好自己分内的事变,既可以让家庭幸福,即历程成功运行。而历程分离就比如你长大了,自己搬出去住,不受父母管了,但是依旧属于这个家庭。这种状态就是线程分离。
固然,假如想要将自己分离出去,就要知道自己的tid,这里需要接口:
  1. PTHREAD_SELF(3)                                                     Linux Programmer's Manual                                                    PTHREAD_SELF(3)
  2. NAME
  3.        pthread_self - obtain ID of the calling thread
  4. SYNOPSIS
  5.        #include <pthread.h>
  6.        pthread_t pthread_self(void);
  7.        Compile and link with -pthread.
复制代码
这个接口会返回调用它的线程的ID。如同getpid()
  1. void *ThreadRun(void *args){    // 线程分离    pthread_detach(pthread_self());    std::string name = static_cast<const char *>(args);    while (true)    {        std::cout << name << "is running ..." << std::endl;        sleep(3);        break;    }    return args;}std::string ToHex(int x)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer, sizeof(buffer), "0x%x", x);
  5.     return buffer;
  6. }
  7. const int num = 10;int main(){    std::vector<pthread_t> tids;    for (int i = 0; i < num; i++)    {        // 1. 线程ID        pthread_t tid;        // 2. 线程名字        char *name = new char[128];        snprintf(name, 128, "thread - %d", i + 1);        pthread_create(&tid, nullptr, ThreadRun, name);        // 保存所有线程的ID        tids.push_back(tid);    }    sleep(3);    for (auto tid : tids)    {        pthread_cancel(tid);        std::cout << " cancel: " << ToHex(tid) << std::endl;        void *ret = nullptr;        int n = pthread_join(tid, &ret);        std::cout << (long long int)ret << " quit... , n: " << n << std::endl;    }    return 0;}
复制代码
可以看到,假如我们等候一个已经分离出去的线程,会得到22号错误信息!所以不能 join 一个分离的线程!

所以主线程就可以不管新线程,可以继承做自己的事变,不消壅闭在join!
但是注意:线程分离了,依然是同一个历程!一个线程出异常,会导致整个历程退出!
上面是自己分离自己。也可以通过主线程分离新历程:
  1.     for (auto tid : tids)
  2.     {
  3.         pthread_detach(tid);//主线程分离新线程
  4.     }
复制代码
4 语言层的线程封装

上面讲的是Linux系统提供给我们的系统调用,资助我们可以举行线程控制,也叫做原生线程库。我们熟悉了底层的原生线程库,就会方便很多。
我们来看C++11中的线程
  1. #include <iostream>
  2. #include <pthread.h>
  3. #include <unistd.h>
  4. #include <ctime>
  5. #include <vector>
  6. #include <string>
  7. #include <thread>
  8. void threadrun( int num)
  9. {
  10.     while (num)
  11.     {
  12.         std::cout << " num: " << num << std::endl;
  13.     }
  14. }
  15. // C++中线程库
  16. int main()
  17. {
  18.     std::thread mythread(threadrun, 10);
  19.     while (true)
  20.     {
  21.         std::cout << "main thread..." << std::endl;
  22.         sleep(1);
  23.     }
  24.     mythread.join();
  25.     return 0;
  26. }
复制代码
注意,虽然是使用的语言层的线程库,但是依旧要连接thread动态库,因为语言层线程库的本质是对原生线程库接口的封装!!!无论是java照旧python都要与原生线程库产生联系!
Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

瑞星

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表