【Linux】多线程的创建、等待、终止


目录

一、线程的概念

1、页表详解

1.1页表的举例

1.2页表的真实表示形式

2、进程、线程的区别

2.1进程

2.2线程(Linux中的线程被称为轻量级进程)

3、使用POSIX标准的pthread原生线程库创建“线程”

4、线程中共享和私有数据及读取返回值判定错误

5、线程的优缺点

5.1线程的优点

5.2线程的缺点

6、clone创建两种子进程(了解)

7、用户级线程ID

二、多线程的创建

1、使用Linux原生线程库创建多线程

2、使用C++多线程接口在Linux环境创建多线程

三、线程等待

1、主线程使用pthread_join()等待新线程

2、分离线程

2.1pthread_self()获取线程id

2.2pthread_detach()分离线程

四、线程的终止

1、线程函数return,线程就算终止

2、使用pthread_exit终止线程

3、使用pthread_cancel取消线程

五、对原生线程库进行二次封装

1、Thread.hpp

2、main.cpp


一、线程的概念

1、页表详解

1.1页表的举例

例如线程正在执行一条修改常量区常量的代码,通过页表找到该句代码所对应的执行权限并没有修改权限,内存管理单元MMU将会终止该访问行为,MMU硬件报错,操作系统发现硬件报错,将向进程发送十一号信号SIGSEGV(段错误)终止进程。

1.2页表的真实表示形式

为了解决一一映射页表体积过大问题,采用了类似哈希的结构。

虚拟地址的前10位是页目录,共计2^10个,即1KB大小;中间10位是页表,每一个页目录指向一张页表,每张页表大小1KB,共有1KB张页表,合计大小1MB;后12位代表所属页表指向物理内存的偏移量,加上这个偏移量,即可找到真实的物理地址。

2、进程、线程的区别

2.1进程

进程是承担分配操作系统资源的基本单位进程=一堆线程PCB+进程地址空间+页表+物理内存的一部分(进程=内核数据结构+进程对应的代码和数据)。

2.2线程(Linux中的线程被称为轻量级进程)

1、线程是CPU调度的基本单位

2、线程(thread)在进程的进程地址空间中运行,拥有进程的一部分资源。进程粒度较粗,线程粒度更细。

3、OS肯定得通过特定的数据结构来管理大量的线程。某些操作系统例如windows,会给线程创建一个个TCB(线程控制块)来管理大量的线程;但是Linux中的线程并不像windows那样,而是直接复用了进程PCB的那套数据结构、管理方法。所以Linux中并没有真正意义上的线程,这些披着进程PCB外壳的“线程”被称为轻量级进程

4、进程用来整体申请资源;线程“伸手”向进程申请资源。Linux没有真正意义上的线程,所以没有办法提供创建线程的系统调用接口,只提供了创建轻量级进程的接口(需要添加对应的原生线程库pthread,它封装了底层的轻量级进程的接口,让程序员在调用该库时感觉像在玩线程)。

3、使用POSIX标准的pthread原生线程库创建“线程”

PTHREAD_CREATE(3)   
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
		void *(*start_routine) (void *), void *arg);
参数:1、thread:输出型参数,指向线程标识符的指针,线程创建成功后将通过此指针返回线程标识符。
      2、attr:线程属性,包括线程的栈大小、调度策略、优先级等信息。如果为NULL,则使用默认属性。
      3、start_routine:线程启动后要执行的函数指针。
      4、arg:线程函数的参数,将传递给线程函数的第一个参数。
返回值:pthread_create()成功返回0。失败时返回错误号,*thread中的内容是未定义的。
编译请使用 -pthread 链接。

主线程和新线程两个执行流分别在各自的作用域死循环打印指定内容:

使用Linux线程库记得在编译时链接pthread:

使用ps -aL查看当前操作系统中的线程:

LWP:轻量级进程ID(light weight process)

同一个进程中的线程PID相同,但是LWP不同。主线程的LWD等于PID

4、线程中共享和私有数据及读取返回值判定错误

共享:全局数据、堆空间、加载的的动态库、文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 、当前工作目录、用户id和组id等进程中的大部分资源时共享的;

私有:1、线程PCB属性私有;2、线程有一定的私有上下文结构;3、每个线程都有自己独立的栈结构;

错误:1、传统的一些函数,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。但是pthreads函数出错时不会设置全局变量errno(大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。

2、pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小

5、线程的优缺点

5.1线程的优点

1、创建一个新线程的代价要比创建一个新进程小得多

2、与进程切换相比,线程切换操作系统需要做的工作要少。进程切换:切换页表、进程地址空间、PCB切换、上下文切换;而线程切换仅需PCB切换、上下文切换。

3、线程占用的资源要比进程少很多

4、能充分利用多处理器(多核)的可并行数量

5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

6、计算密集型应用(加密、解密、算法等),为了能在多处理器系统上运行,将计算分解到多个线程中实现

7、I/O密集型应用(外设、磁盘、网络等),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

5.2线程的缺点

1、性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

2、健壮性(鲁棒性)降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 一个线程异常退出了,操作系统会向该进程对应的所有PCB发送信号,因为该进程中的所有线程的PID均相同,该信号线程人手一份,全部退出,同样的,进程也因为PID及信号的原因,退出。

3、缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

4、编程难度提高

编写与调试一个多线程程序比单线程程序困难得多。

6、clone创建两种子进程(了解)

CLONE(2)  
int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

这个接口是不需要我们使用的,它是fork()和vfork()的底层。区别在于fork创建子进程将不会共享父进程的进程地址空间,而vfork()创建的进程会共享父子进程的进程地址空间(有点轻量级进程那味了)。当然pthread原生线程库的底层,就是使用了clone来为用户创建轻量级进程。

7、用户级线程ID

主线程用进程地址空间中的独立栈结构,新线程使用线程库分配好地址的线程栈。新线程独立的线程栈可以扩容。

线程局部存储:现在有一个全局变量int a=100,存放于已初始化数据段;在a的前面加一个__pthread修饰,每个线程将会各有一份属于自己的变量a,互不干扰,此时a存放于共享区(共享区在pthread库中)。

二、多线程的创建

1、使用Linux原生线程库创建多线程

创建10个新线程,将自定义类对象作为参数传入回调函数中,并将10个对象保存至vector中。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
#include <vector>
#include <string>
using namespace std;
class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
};
void* start_routine(void* args)//新线程
{
    ThreadData* td=static_cast<ThreadData*> (args);//安全的进行强制类型转换
    int cnt=10;
    while(cnt--)
    {
        cout<<td->namebuffer<<endl;
        sleep(1); 
    }
    delete td;
    return nullptr;
}

int main()
{
    //创建一批线程,先定义一个对象数组
    vector<ThreadData*> threads;
    #define NUM 10//定义创建10个线程
    //循环创建NUM个线程
    for(int i=0;i<NUM;++i)
    {
        ThreadData* td=new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s%d","thread",i);
        pthread_create(&td->tid,nullptr,start_routine,td);//传递new出来对象的地址,就不会出现缓冲区冲突的问题
        threads.push_back(td);
        //pthread_create(&tid,nullptr,start_routine,(void*)"newThread");
        //pthread_create(&tid,nullptr,start_routine,namebuffer);//传的是缓冲区的地址,所有线程均可访问这段空间
                                                              //这段堆空间虽然出作用域销毁,但是每个线程的缓冲区都是这个地址
        //sleep(1);//不睡眠的话,线程缓冲区会被其他进程刷新
    }
    while(1)
    {
        cout<<"mainThread"<<endl;
        sleep(1); 
    }
    return 0;
}

每一个线程都有自己独立的栈结构。

在使用pthread_create函数创建多线程的时候,注意传参不要传入线程共享的参数。

2、使用C++多线程接口在Linux环境创建多线程

void thread_run()
{
    while(1)
    {
        cout<<"我是新线程"<<endl;
        sleep(1);
    }
}
int main()
{
    std:thread td(thread_run);
    while(1)
    {
        cout<<"我是主线程"<<endl;
        sleep(1);
    }
    td.join();
    return 0;
}

任何语言,在Linux中使用多线程编程,必须使用-pthread进行链接。

C++的thread库,底层有条件编译会判断当前的运行环境,执行适用于Linux或windows的多线程代码。

在Linux环境中,C++的多线程,本质就是对pthread库的封装。

三、线程等待

1、主线程使用pthread_join()等待新线程

和父进程等待子进程一样,进程等待的目的是为了防止子进程变为僵尸进程造成内存泄露同时回收子进程的退出信息;同样的,你可以不关心线程的退出信息,但是一定要回收线程PCB等资源,所以线程等待是必须的。

主线程会在pthread_join()处阻塞式等待新线程的退出。

PTHREAD_JOIN(3)   
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread:要等哪一个线程
retval:输出型参数,用于获取线程函数返回时的退出结果(回调函数返回值不是void*么,这里用void**接收这个返回值)
返回值:在成功时,pthread_join()返回0; 在错误时,它返回一个错误码。
编译并使用-pthread链接
class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
};
void* start_routine(void* args)//新线程
{
    ThreadData* td=static_cast<ThreadData*> (args);//安全的进行强制类型转换
    int cnt=10;
    while(cnt--)
    {
        cout<<td->namebuffer<<endl;
        sleep(1); 
    }
    //pthread_exit(nullptr);
    return nullptr;//线程函数结束,return的时候线程就算终止了
}
int main()
{
    //创建一批线程,先定义一个对象数组
    vector<ThreadData*> threads;
    #define NUM 10//定义创建10个线程
    //循环创建NUM个线程
    for(int i=0;i<NUM;++i)
    {
        ThreadData* td=new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s%d","thread",i);
        pthread_create(&td->tid,nullptr,start_routine,td);//传递new出来对象的地址,就不会出现缓冲区冲突的问题
        threads.push_back(td);
    }
    //进程对线程进行等待
    for(auto& iter : threads)
    {
        void* ret=nullptr;
        int n=pthread_join(iter->tid,&ret);//用ret取到返回值void*
        assert(0==n);
        cout<<"退出成功"<<(long long)ret<<endl;
        delete iter;
    }
    cout<<"主函数退出"<<endl;
    return 0;
}

线程会将void*的退出信息传入pthread库,通过pthread_join()函数中的参数void **retval接收这个退出信息。

我们知道,父进程是可以通过设置对SIGCHLD信号忽略的做法,无视子进程的退出信号,转而让操作系统去回收子进程的资源。那如果主线程对新线程的退出信息根本不关心,能否像进程那样让操作系统去回收新线程的资源呢?可以通过分离线程的方法达到该目的。

2、分离线程

2.1pthread_self()获取线程id

PTHREAD_SELF(3)     
#include <pthread.h>
pthread_t pthread_self(void);
返回值:此函数始终成功,返回调用线程的ID。
编译并使用-pthread链接

2.2pthread_detach()分离线程

PTHREAD_DETACH(3) 
#include <pthread.h>
int pthread_detach(pthread_t thread);
thread:线程ID
返回值:在成功时,pthread_detach()返回0; 在错误时,它返回一个错误码。
编译并使用-pthread链接

错误示范:

把pthread_detach放在新线程的执行函数里,有可能发生主线程已经在join处开始等待了,新线程才走到执行分离的代码,等新线程执行完回调函数内的代码时,主线程自然join等待成功了。这是错误写法。

正确写法:创建线程成功时,由主线程进行分离

void* start_routine(void* args)
{
    string threadname=static_cast<const char*>(args);
    sleep(5);
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    pthread_detach(tid);//创建线程成功时,由主线程进行分离

    // int n=pthread_join(tid,nullptr);
    // cout<<n<<":"<<strerror(n)<<endl;
    return 0;
}

四、线程的终止

切忌在新线程中使用exit(0)等终止进程的函数来终止线程,因为整个进程会被干掉。

1、线程函数return,线程就算终止

void* start_routine(void* args)//新线程
{
    ThreadData* td=static_cast<ThreadData*> (args);//安全的进行强制类型转换
    int cnt=10;
    while(cnt--)
    {
        cout<<td->namebuffer<<endl;
        sleep(1); 
    }
    return nullptr;//线程函数结束,return的时候线程就算终止了
}

2、使用pthread_exit终止线程

void* start_routine(void* args)//新线程
{
    ThreadData* td=static_cast<ThreadData*> (args);//安全的进行强制类型转换
    cout<<td->namebuffer<<endl;
    sleep(1);
    pthread_exit(nullptr);
}

使用pthread_exit的主要场景是在线程中发生了错误或者需要提前退出时使用。比如,当一个线程检测到某个条件不满足时,可以调用pthread_exit函数来立即退出线程。 需要注意的是,如果在线程中使用pthread_exit提前退出线程,需要确保已经释放了该线程分配的资源,否则可能会导致资源泄漏。

3、使用pthread_cancel取消线程

PTHREAD_CANCEL(3)     
#include <pthread.h>
int pthread_cancel(pthread_t thread);
thread:要取消哪一个线程
返回值:在成功时,pthread _ Cancel ()返回0; 在出错时,它返回一个非零的错误码。
编译并使用-pthread链接
for(auto& iter : threads)
{
    pthread_cancel(iter->tid);
}

线程如果被取消,那么这个线程的退出码是-1(宏PTHREAD_CANCELED)。

五、对原生线程库进行二次封装

1、Thread.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <functional>
#include <cassert>
#define NUM 1024
class Thread;//声明一下
class Context//线程上下文类
{
public:
    Context()
    :_this(nullptr)
    ,_args(nullptr)
    {}
    ~Context()
    {}
public:
    Thread* _this;
    void* _args;
};
class Thread
{
public:
    typedef std::function<void*(void*)> func_t;//定义一个函数对象类型,它的返回值和参数都是void*
    //构造函数
    Thread(func_t func,void* args,int number)//number是线程名字,后续int转string
    :_func(func)
    ,_args(args)
    {
        char buffer[NUM];
        snprintf(buffer,sizeof(buffer),"thread-%d",number);
        _name=buffer;
        //线程启动
        Context* ctx=new Context();
        ctx->_this=this;
        ctx->_args=_args;
        int n=pthread_create(&_tid,nullptr,start_routine,ctx);
        assert(n==0);
        (void)n;
    }
    void* run(void* args)
    {
        return _func(args);
    }
    //因为形参有个this指针,所以弄成static
    static void* start_routine(void* args)//这里的args就是 start()的ctx
    {
        Context* ctx=static_cast<Context*>(args);
        void* ret=ctx->_this->run(ctx->_args);
        delete ctx;
        return ret;
    } 
    
    
    void join()
    {
        int n=pthread_join(_tid,nullptr);
        assert(n==0);
        (void)n;
    }
    ~Thread()
    {

    }
    
private:
    std::string _name;//线程的名字
    func_t _func;//线程的回调函数
    void* _args;//喂给线程的参数,不过是用Context类封装一下喂给线程
    pthread_t _tid;//线程ID
};
//异常和if。意料之外
//assert。意料之中。99%概率为真。

2、main.cpp

#include <iostream>
#include <pthread.h>
#include <memory>
#include "Thread.hpp"
using namespace std;   
void* thread_run(void* args)
{
    string work_type=static_cast<const char*>(args);
    while(1)
    {
        cout<<"新线程"<<work_type<<endl;
        sleep(1);
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"thred1",1));
    unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"thred2",2));
    unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"thred3",3));
  
    thread1->join();
    thread2->join();
    thread3->join();
    return 0; 
}

猜你喜欢

转载自blog.csdn.net/gfdxx/article/details/129699925