从源码深度探究“线程控制“

1.创建线程

在linux内核中没有真正的线程,所以线程的创建函数如pthread_creat不是系统调用函数,Linux只有创建轻量级进程LWP的系统调用接口vfork;但是为了让用户可以使用线程,所以就把vfork进行封装成了一个库,所以我们的pthread_create就是我们这个库(库名为phread,是原生线程库)中的一个函数;

1.1 C++11创建多线程

C++11中创建多线程的函数为thread;头文件为thread,本质上还是封装了pthread库;

线程的tid是什么?是LWP吗?显然不是;

新线程和主线程谁先跑是不确定的;进程的时间片是被线程瓜分的,也就是说线程的时间片不是系统分配的,因为如果是系统分配的,呢么进程就可以通过创建更多的线程来获得更多的时间片,这样是不好的;

1.1.1 获得线程ID的函数

1.2让多个线程执行一个函数(被重入了!!!)

复制代码
void * routine(void * args)
{
    std::string name = static_cast<const char*> (args);
    while(true)
    {
        std::cout <<"我是新线程,我的名字是 " << name << "my tid is :"<< pthread_self() <<std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1 =0;
    pthread_create(&tid1,nullptr,routine,(void*)"thread-1");
  
    pthread_t tid2 =0;
    pthread_create(&tid2,nullptr,routine,(void*)"thread-2");
   
    pthread_t tid3 =0;
    pthread_create(&tid3,nullptr,routine,(void*)"thread-3");
   
    pthread_t tid4 =0;
    pthread_create(&tid4,nullptr,routine,(void*)"thread-4");

    while(true)
    {
        std::cout << "我是主线程"<<std::endl;
        sleep(1);
    }
    return 0;
}

我们发现每次循环线程的执行顺序并不是一样的,这就说明在不加保护的情况下,显示器文件就是共享资源;并且每一个线程都可以看到整个进程中的其他的函数,这说明线程对进程的函数和代码是共享的;

为什么 return buffer; 还能打印出内容?

buffer 是栈上局部数组,函数 toHex 结束后,这块内存会被释放(标记为可复用),但数据不会立刻被清空:你在 cout 里调用 toHex,函数返回后,std::string 构造函数会立刻拷贝 buffer 里的内容到堆上。

只要在拷贝完成前,这块栈内存没被其他操作覆盖,就能成功构造出合法的 std::string。

这是未定义行为的侥幸,不是代码正确!如果中间发生线程切换、函数调用,buffer 内容被覆盖,就会打印乱码或崩溃。

如果多个线程同时调用 toHex:**每个线程都有自己的栈,所以 buffer 是线程独立的,不会互相覆盖栈内存。但 std::cout 本身是共享资源,不加锁的话,多个线程的输出会乱序、交织。**更危险的是:如果 buffer 改成 static,多线程下会直接数据竞争,导致输出完全错误。

我们定义一个全局变量然后让它在route函数中++,我们发现gval依次递增,这说明每个线程都能看到更新后的gval;也就是说全局变量线程也是共享的;这就说明在多线程环境中我们想让线程之间看到同一份空间是非常容易的,只用定义全局变量就可以了;

这也就是为什么一个线程一旦出现异常,其他线程都会崩溃,这是因为一个线程一旦出问题了,OS就会将它的虚拟地址空间回收,而线程之间共享进程的地址空间,呢一个线程的虚拟地址空间被回收,其他的线程也就只能崩溃了;

而一旦一个线程出现异常信号,操作系统就会给每一个线程都拥有异常信号,所以每一个进程都会收到异常信号,所以都会退出;但是操作系统怎么知道你们这几个线程是一起的呢?其实因为线程之间是有组关系的;线程创建之后也是要被等待和回收的;线程一旦被创建,如果不去等待,会有类似僵尸进程的问题,而且还想回收线程的退出问题;而进行等待的线程是我们的主线程

1.3线程等待

如果新线程不退出,主线程在等待队列阻塞等待;

1.4关于线程创建的传参问题和线程退出的返回值问题

而arg参数的类型是void*,这说明想要传什么都可以对于arg;所以我们让args这个参数可以是一个变量,数字,类,传给新线程,这样就可以给新线程传递很多类内参数,以及类内函数方法;

cpp 复制代码
class ThreadData
{
public:
     ThreadData(const std::string & name ,int a,int b)
     :_name(name)
     ,_a(a)
     ,_b(b)
     {}
     int Excute()
     {
        return _a+_b;
     }
     std::string Name()
     {
        return _name;
     }
     ~ThreadData()
     {}
private:
      std::string _name;
      int _a;
      int _b;
};

void * routine1(void * args)
{
    //std::string name = static_cast<const char *> (args);
    ThreadData * td = static_cast<ThreadData *>(args);
    while(true)
    {
        std::cout << "我是新线程,我的名字是:"<<td->Name() <<std::endl;
        sleep(1);
        std::cout << "task is :" <<td-> Excute() <<std::endl;
        break;
    }
    return 0;
}

int main()
{
    pthread_t tid1;
    ThreadData * td =new ThreadData("thread-1",10,20);
    pthread_create (&tid1,nullptr,routine1,td);
    int n =pthread_join(tid1,nullptr);
    if(n!=0)
    {
        std::cerr <<"join error" << n <<","<<strerror(n) <<std::endl;
        return 1;
    }
    std::cout << "join ssuccess!"<<std::endl;
}

线程退出方式:

pthread_join拿到的返回值是pthread_create创建的新线程的执行函数返回值;

① return

注意:C/C++中整数占4个字节,而void*占8个字节;

1.5 多线程的创建方式

cpp 复制代码
class ThreadData
{
public:
     ThreadData()
     {}
     void Init(const std::string &name,int a,int b)
     {
        _name=name;
        _a=a;
        _b=b;
     }
     void Excute()
     {
        _result = _a+_b;
     }
     int Result()
     {
        return _result;
     }
     std::string Name()
     {
        return _name;
     }
     void SetId(pthread_t tid)
     {
        _tid=tid;
     }
     pthread_t Id()
     {
         return _tid;
     }
     ~ThreadData()
     {}
     int A()
     {
        return _a;
     }
     int B()
     {
        return _b;
     }
private:
      std::string _name;
      int _a;
      int _b;
      int _result;
      pthread_t _tid;
};

void * routine1(void * args)
{
    //std::string name = static_cast<const char *> (args);
    ThreadData * td = static_cast<ThreadData *>(args);
    while(true)
    {
        std::cout << "我是新线程,我的名字是:"<<td->Name() <<std::endl;
        sleep(1);
        td->Excute();
        break;
    }
    return td;
}

#define NUM 10

//创建多线程

int main()
{
    ThreadData td[NUM];
    for(int i=0;i<NUM;i++)
    {
        char id[64];
        snprintf(id,sizeof(id),"thread-%d",i);
        td[i].Init(id,i*10,i*20);
    }
    for(int i=0;i<NUM;i++)
    {
        pthread_t id;
        pthread_create(&id,nullptr,routine1,&td[i]);
        td[i].SetId(id);
    }
    
     for(int i=0;i<NUM;i++)
     {
        pthread_join(td[i].Id(),nullptr);
     }

     for(int i=0;i<NUM;i++)
     {
        printf("td[%d]:%d + %d =%d[%ld\n]",i,td[i].A(),td[i].B(),td[i].Result(),td[i].Id());
     }
}

1.6 线程退出

主线程return表示进程退出,新线程退出,表示该线程退出;但是在任何线程的任何地方中调用exit,表示进程退出;

取消一个线程的前提是这个线程已经存在了;但是不推荐使用取消退出线程;

cpp 复制代码
void *start(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << "im a new thread:" << name << std::endl;
        sleep(1);
    }
    return 0;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start, (void *)"thread-1");

    sleep(5);
    pthread_cancel(tid);
    std::cout << "取消线程" << tid << std::endl;

    void *ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << "new thread exit code " << (long long int)ret << std::endl;

    return 0;
}

取消一个线程必须得join;

1.7 线程分离

默认情况下,新创建的线程是 joinable 的,线程退出后,必须对其执行 pthread_join 操作,否则无法释放线程资源,从而造成系统资源泄漏;如果不关心线程的返回值,join 会成为一种负担,此时我们可以将线程设置为分离状态,告诉系统当线程退出时,自动释放线程资源。

主线程没有非阻塞等待,只有线程分离,一个线程设置出来默认是join ,但是给要分离的线程使用pthread_detach(类似于分家,打个分离的标签)的时候,主线程就可以做自己的事情,不用一直阻塞等待;joinable和分离是冲突的,⼀个线程不能既是joinable又是分离的。joinable(默认):需要手动 pthread_join 回收;

detached:线程退出自动释放资源,不用管

注意:在多执行流情况下,最好让主执行流是最后退出的;

如何进行线程分离?

给要分离的线程加上pthread_detach函数即可

cpp 复制代码
int pthread_detach(pthread_t thread);

我们看到在线程分离之后线程的退出码是0,这说明了退出成功了;值得注意的是线程中可以创建进程;

1.8 线程ID及进程地址空间布局

pthread_create 函数会产生一个线程 ID,存放在第一个参数指向的地址中,该线程 ID 和内核调度层面的线程 ID 不是一回事;内核调度层面的线程 ID 属于进程调度范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,需要一个数值来唯一标识该线程;pthread_create 函数第一个参数指向的内存单元中存储的,是 NPTL 线程库层面的线程 ID,线程库的后续操作都是根据该线程 ID 来操作线程的;线程库 NPTL 提供了 pthread_self 函数,可以让线程获得自身在库层面的 ID。

也就是说线程的属性并不是LWP的属性,所谓的属性就是id,优先级,状态等,之所以这样是因为我们的软件要解耦;这些属性在磁盘的库文件中,也就是进程库中,会被映射到内存和虚拟地址空间。而且我们这个库是用户级线程库,所以库对对应的属性有对应的函数接口,而这么多线程都需要维护对应的属性,就需要操作系统对这些属性进行先描述、后组织,而描述的这个结构体的名字不是Pcb,而是tcb;所以join释放的其实就是内存中的库维护的系统线程对应的属性;所以join传的id其实是我们线程属性在库中对应的地址;而且这些TCB在库中是按照数组形式存放的;

注意在进程地址空间中的栈是主线程的栈,而后面的现成的栈是在共享区中动态申请的,这就叫做线程栈;

1.7 线程局部存储

也就是变量名一样但是地址不一样,这就叫做线程的局部存储;在底层理解上其实就是在线程库中给每个线程都拷贝了一份,最后被映射到动态库中;

而errno其实就是线程的局部存储,但是线程局部存储只能修饰内置类型,比如我们高频使用的变量其实就可以在全局定义;

而clone其实是轻量级线程的创建方式;

补充知识

mmap作用:

① 申请空间,然后把空间映射到虚拟地址空间,再把空间返回;是malloc的底层实现;

2.C++实现线程接口

原生 C++ thread 太底层、难用、不安全,封装是为了简化使用、统一管理、避免内存泄漏和线程失控。

原生 thread 需手动 join/detach,忘操作会崩溃;封装自动管理生命周期

封装可统一实现线程池、命名、优先级、异常安全,业务更易用

隔离底层细节,让代码更简洁、稳定、易维护

Main.cc

cpp 复制代码
#include "Thread.hpp"
#include <unordered_map>
#include <memory>

#define NUM  10
using thread_ptr_t = std::shared_ptr<ThreadModule::Thread> ;

int main()
{
    //创建多线程
   std::unordered_map<std::string,thread_ptr_t> threads;
    for(int i=0;i<NUM;i++)
    {
       thread_ptr_t t=std::make_shared<ThreadModule::Thread>([](){
         while(true)
        {
            std::cout << "hello world"<<std::endl;
            sleep(2);
        }
     });
        threads[t->Name()]=t;
    }
    for(auto & thread :threads)
    {
        thread.second->Start();
    }

    for(auto & thread :threads)
    {
        thread.second->Join();
    }

    // ThreadMoudule::Thread t([](){
    //     while(true)
    //     {
    //         std::cout << "hello world"<<std::endl;
    //         sleep(2);
    //     }
    // });
    // t.Start();
    // std::cout << t.Name() <<"is running"<<std::endl;
    // sleep(5);
    // t.Stop();
    // std::cout << "stop thread" <<t.Name()<<std::endl;
    // sleep(1);
    // t.Join();
    // std::cout << "Join thread" << t.Name() <<std::endl;
    return 0;
}

Thread.cpp

cpp 复制代码
#ifndef _THREAD_HPP_
#define _THREAD_HPP_
#include <functional>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
namespace ThreadModule 
{
    using func_t = std::function<void()>;
    static int numble =1;
    enum class TSTATUS
    {
        NEW,    //线程是新建的
        RUNNING,
        STOP
    };
    class Thread
    {
     private:
          //成员方法,要传this指针,或者直接使用static也行,但是不会传this指针
          static void * Routine(void * args)
          {
             Thread * t =static_cast<Thread *> (args);
             t->_status=TSTATUS::RUNNING;
             t->_func();
             return nullptr;
          }
           void EnableDetach()
          {
               _Joinable=false;
          }
     public:
          Thread(func_t func)
          :_func(func)
          ,_status(TSTATUS::NEW)
          ,_Joinable(true)
          {
               _name = "Thread-" + std::to_string(numble++);
               _pid = getpid();
          }
          bool Start()
          {
               if(_status != TSTATUS::RUNNING)
               {
                    int n = ::pthread_create(&_tid,nullptr,Routine,this);
                    if(n!=0)
                    {
                         return false;
                    }
                    _status=TSTATUS::RUNNING;
                    return true;
               }
               return false;

          }
          //线程结束了并不代表Thread这个对象被释放了
          bool Stop()
          {
               if(_status == TSTATUS::RUNNING)
               {
                    int n=::pthread_cancel(_tid);
                    if(n != 0)
                    {
                         return false;
                    }
                    _status = TSTATUS::STOP;
                    return true;

               }
               return false;
          }
          bool Join()
          {
               if(_Joinable)
               {
                  int n = ::pthread_join(_tid,nullptr);
                  if(n!= 0)
                  {
                    return false;
                  }
                  _status =TSTATUS::STOP;
                  return true;

               }
               return false;
          }
         
          void Detach()
          {
               EnableDetach();
               pthread_detach(_tid);
          }
          bool Joinable()
          {
               return _Joinable;
          }
          std::string Name()
          {
               return _name;
          }
          ~Thread()
          {}
     private:
          std::string _name;
          pthread_t _tid;
          pid_t _pid;
          bool _Joinable;
          func_t _func;
          TSTATUS _status;

    };
}

#endif
相关推荐
2301_814590252 小时前
实时音频处理C++实现
开发语言·c++·算法
网安2311石仁杰2 小时前
ZAP 主动扫描模块精读:从代码层面理解安全检测引擎的设计与质量
java·安全
码上生存指南2 小时前
技术栈要不要追新?我为此换过一次工作,结论是……
java·程序人生
gududexiao2 小时前
linux 设置tomcat开机启动
linux·运维·tomcat
chehaoman2 小时前
SpringBoot3.3.0集成Knife4j4.5.0实战
java
Fang fan2 小时前
Netty入门
java·开发语言·redis·分布式·python·哈希算法
第二只羽毛2 小时前
C++ 高并发内存池2
大数据·开发语言·jvm·c++·c#
我真会写代码2 小时前
Java程序员常用设计模式详解(实战版)
java·开发语言·设计模式
2401_878530212 小时前
C++与FPGA协同设计
开发语言·c++·算法