【Linux】线程控制

发布于:2025-03-12 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

一、原生线程库:

二、线程控制:

1、线程创建:

2、线程等待:

自定义类型的接收对象:

​编辑

3、线程终止:

pthread_exit:

pthread_cancel:

4、线程ID:

线程库的底层原理

三、线程栈:

创建多线程:

栈区变量:

局部性存储:

拿到别的线程执行流数据:

四、线程分离:


一、原生线程库:

在编译代码的时候,需要新加选项-lpthread(在部分环境下是不用加的,如果出现链接错误就加上,没有就不用加),因为要使用pthread的原生线程库

什么是原生线程库:

我们知道,在Linux中是没有真正意义上线程的概念的,只有轻量级进程的概念,

所以Linux工程师在系统层和用户层之间封装了原生线程库方便用户进行使用

在Linux中,原生线程库也是pthread库,在使用的时候包含即可

二、线程控制:

1、线程创建:

pthread_t:这其实就是无符号长整型:unsigned long int

接下来看看参数:

参数1:pthread_t* thread

这是一个输出型参数,表示线程的tid

参数2:设置线程属性,一般直接nullptr即可

参数3:这是一个函数指针,这是一个很重要的参数,发现这个函数返回值是void*,参数也是void*,这是一个回调函数,当线程启动的时候会自动回调此函数

参数4:这是void*类型的,就是线程启动时,传递给上述函数的形参

返回值:成功返回0,失败返回一个错误码

void* newthread(void* argc)
{
    while(1)
    {
        cout << "我是新线程,我正在运行,pid : " << getpid() <<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,newthread,nullptr);    
    sleep(1);
    while(1)
    {
        cout << "我是主线程,我正在运行,pid : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

如上,写一段线程小代码,运行后:

如上,可以发现:这两个线程的pid也就是进程id是一样的,也就证明了线程是进程内部的分支

可以通过指令ps -aL来观察线程的属性

如上,可以发现每个线程有其独特的LWP,这就类似于进程的pid,CPU在调度线程的时候是通过LWP来进行调度的

如上,仔细观察还会发现有一个线程的LWP和PID是相等的,此时这个线程就叫做主线程

多线程错误:

同时对于这些线程如果一个线程发生了错误,那么所有共享一个进程的线程都会退出

如下,我们修改代码为存在除零错误

那么当主线程出现错误的时候,尽管别的进程没有错误,但是仍然会退出

同样的,向一个线程发送终止信号,其他线程也会被终止:

全局变量:

同样的,线程也共享者全局变量区:

如上,当在主线程中进行num的++,在新线程中打印出num的值也会++,所以线程间通信是比较方便的

2、线程等待:

线程和进程是差不多的,父进程需要等待子进程,防止子进程僵尸
主线程也需要等待新线程,在原生线程库中,封装了一个接口:pthread_join

参数1:thread:这是待等待线程的ID

参数2:retval:输出型参数,用于获取新线程的返回值,

返回值:成功返回0,失败返回错误码

void* newthread(void* arr)
{
    const char* name = (const char*)arr;
    int cnt = 5;
    while(1)
    {
        cout << name << "new thread : pid : " << getpid() << endl;
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }
    return (void*)11;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,newthread,(void*)"Thread 1 ");
    sleep(7);
    void* retval;
    pthread_join(tid,&retval);
    cout << "ret : " << (long)retval << endl;
    cout << "main quit success ..." << endl;
    return 0;
}

自定义类型的接收对象:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

using namespace std;

class Request
{
public:
    Request(int start,int end,const string& threadname)
    :_start(start)
    ,_end(end)
    ,_threadname(threadname)
    {}

public:
    int _start;
    int _end;
    string _threadname;
};

class Response
{
public:
    Response(int retval,int retcode)
    :_retval(retval)
    ,_retcode(retcode)
    {}
public:
    int _retval;
    int _retcode;
};

void* sumcount(void* argc)
{
    Request* rq = static_cast<Request*>(argc);
    Response* rsp = new Response(0,0);
    for(int i = rq->_start;i <= rq->_end;i++)
    {
        rsp->_retval += i;
    }
    delete rq;
    return rsp;
}

int main()
{
    pthread_t tid;
    Request* rq = new Request(1,100,"mythread");
    pthread_create(&tid,nullptr,sumcount,rq);

    void* ret;
    pthread_join(tid,&ret);
    Response *rsp = static_cast<Response*>(ret);
    cout << "rsp->result: " << rsp->_retval << ", exitcode: " << rsp->_retcode << endl;
    delete rsp;
    return 0;
}

如上,这就是计算两个数之间的和

上述有两个地方要注意:

1、在主函数中pthread_create中的第三个参数不用强转,而之前的字符串需要强转,这是因为之前的字符串默认是const char* 的,强制转换成void*这样可以避免权限放大

而这里自定义类型不用强转,尽管类型不同,这是因为void*类型的指针可以接受任意类型的指针

2、里面运用到了static_cast,这是C++引入的,比()强转更安全,因为:会在编译时进行类型检查,如果转换不合法(例如无关类型之间的转换),编译器会报错,而()强制类型转换,基本上都是可以转换的

3、线程终止:

pthread_exit:

将一个线程终止有许多方法,比如发信号,exit等等

如果在代码中调用exit,这是将进程退出,那么就会导致所有线程都会退出,这显然是不对的,
那么在原生线程库中就封装了接口:pthread_exit

参数retval:这就是类似于exit中的参数,退出码,返回值

这里的retval和pthread_join里的retval形参名字怎么是一样的呢?

首先我们要知道,主线程和创建出来的新线程是在不同的栈帧中的,那么想要远程修改就需要赋值传地址

如下,将新线程中的return可以改为pthread_exit(),这样优雅多了

pthread_cancel:

参数:指的是取消哪一个线程,并且退出的线程,退出码为-1

返回值:成功返回0,失败返回错误码

void* newthread(void* arr)
{
    const char* name = (const char*)arr;
    int cnt = 5;
    while(1)
    {
        cout << name << "new thread : pid : " << getpid() << endl;
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }
    pthread_exit((void*)11);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,newthread,(void*)"Thread 1 ");
    sleep(2);
    pthread_cancel(tid);
    void* retval;
    pthread_join(tid,&retval);
    cout << "retval : " << (long)retval << endl;
    cout << "main quit success ..." << endl;
    return 0;
}

如上,我们不应该取消成功后返回0吗,这里为什么是-1呢?

注意:这里的-1是指的是取消线程的退出码,而不是pthread_cancel的退出码

4、线程ID:

如上,这个接口是获取当前线程的tid

void* newthread(void* arr)
{
    const char* name = (const char*)arr;
    int cnt = 5;
    while(1)
    {
        cout << name << "new thread : pid : " << getpid() << " tid : " << pthread_self() << endl;
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }
    pthread_exit((void*)11);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,newthread,(void*)"Thread 1 ");
    cout <<"主线程的tid : " << pthread_self() << endl; 
    cout <<"创建的新线程的tid : " << tid << endl;
    void* retval;
    pthread_join(tid,&retval);
    cout << "retval : " << (long)retval << endl;
    cout << "main quit success ..." << endl;
    return 0;
}

如上,发现这个tid和新线程中的pthread_self是一样的

但是这个tid和我们ps -aL中的LWP是不一样的,本来二者也都是不一样的,因为LWP是操作系统层面的概念,操作系统自己知道即可,我们用户只关心tid

这个线程ID到底是什么东西呢?和tid是不是同一个东西呢?

线程库的底层原理

我们先理解理解如下的图

1、在Linux中是没有线程的概念的,只有轻量级进程的概念

2、所以我们在Linux中的线程的概念是库给我们维护的------所以线程库要维护线程的概念(这是用户关心的,比如说线程的tid,线程的时间片有没有到等)又因为这个线程在底层是维护的轻量级进程的执行流,是不用维护线程的执行流的

3、所以这个原生线程库要不要加载到内存中?------当然要,不然怎么用------那么加载到哪里?----- 加载到内存中,通过页表把pthread库映射到我们的共享区

4、我们每创建一个线程,都要在线程库中创建一个线程控制块供用户维护,
这个线程控制块对上提供信息如独立栈在哪?线程ID是什么,回调函数在哪等等,
更重要的是对下提供该线程的LWP指向底层的哪个轻量级进程的执行流

如上,这样就能够很好的管理我们的线程了,当想要拿到对应的线程ID时,直接访问这个描述好的结构体里直接拿就行了
而tid是一个地址:是每一个线程库级别的tcb起始地址,

为什么要以每个tcb的起始地址作为tid呢?

1、它里面存放的是地址
2、它是在用户空间的
3、是虚拟地址,可以直接访问的

三、线程栈:

每一个线程在其运行时有独立的栈结构
因为每一个线程有其独特的调用链
也就是必须有其调用链对应的栈帧结构
这个栈结构会保存任何一个执行流在其运行中的所有临时变量比如压栈的形参,返回值等等

其中主线程直接用地址空间结构中的栈结构即可
其他被主线成pthread_create创建的都是轻量级进程,这些线程用的不是地址空间的栈,而是用的是线程库中独立的栈结构

其做法是首先在pthread.so里面创建创建对应的描述这个线程的线程控制块tcb,这个tcb的起始地址就作为这个线程的tid,里面有一块默认大小的空间,这就是线程栈

当在内核中创建执行流,就是在库里面调用clone()接口,然后把线程执行的方法,已经刚刚创建的线程栈作为第一第二参数传递给clone(),这个函数不是我们使用的,Linux程序员已经将这个函数封装起来了,我们只需要使用这个函数封装后的接口如pthread_create,pthread_join等等

接下来用代码来理解理解:

创建多线程:

首先,我们用一个vector数组来存储多线程的tid,然后定义一个类对象和10进制转16进制的函数,接着就可以实现多线程的实现了

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>

using namespace std;

#define N 3

class threadDate
{
public:
    threadDate(int number)
    {
        _threadname = "thread-" + to_string(number); // thread-0
    }

public:
    string _threadname;
};

string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer,sizeof(buffer),"0x%x",tid);
    return buffer;
}

void* threadrun(void* argc)
{
    threadDate* td = static_cast<threadDate*>(argc);
    int i = 0;
    while(i < 10)
    {
        printf("pid : %d,tid : %s,threadname : %s\n",
            getpid(),toHex(pthread_self()).c_str(),td->_threadname.c_str());
        sleep(1);
        i++;
    }
    delete td;
}

int main()
{
    vector<pthread_t> tids;
    //创建多线程
    for(int i = 0; i < N; i++)
    {
        pthread_t tid;
        threadDate* td = new threadDate(i);
        pthread_create(&tid,nullptr,threadrun,td);
        tids.push_back(tid);
        sleep(1);
    }


    for(int i = 0;i < tids.size();i++)
    {
        pthread_join(tids[i],nullptr);
    }
    return 0;
}

栈区变量:

我们在线程执行代码块中加上一个变量并对其进行打印:

如上,我们可以发现:他们各自都是独立的,和全局存储互相影响是不一样的,他们是互不影响的,也就是说当我们执行对应的代码的时候,各个线程就会在自己的线程栈中开辟对应的栈帧

局部性存储:

我们上述知道线程会共享全局区的数据

如上,但是如果想要他们访问独立起来,怎么办呢?----- 当然是给val这个变量加上__thread进行修饰

如上,他们的值就是各自++了,并且其地址也是不一样的了

拿到别的线程执行流数据:

这个是通过全局变量拿到的

所以,在线程中并不存在真正的独立,我们前面所讲的独立栈本质还是在进程地址空间的,所以只要是想拿还是能够被访问的 ---- 所以,线程与线程之间是没有秘密的,线程栈上的数据也是能够被其他线程访问的      

四、线程分离:

在默认情况下,新线程最后是被主线程所等待pthread_join的,这是为了防止新线程没有被释放而造成内存泄漏,但是还有一种方法,能够做到当线程退出的时候,主线程不需要等待,而是在线程退出的时候自动释放资源

接口:

参数:所分离线程的tid

返回值:成功返回0,失败返回错误码

主线程分离:

自己分离:

如上,我们发现无论是自我分离,还是主线程分离,都只执行了几行代码,这是因为main函数已经执行完了,然后return退出了,这个时候进程就是退出了,进程的资源也就没有了,然而线程又是属于进程的,所以自然而然线程就会退出

线程分离是一种属性状态,当创建线程的时候是不分离状态,当在描述线程的结构体tcb中这个状态为0或者1表示这个线程是不分离状态还是分离状态,但是本质上还是在共享同一份资源,只是分离后的线程退出和主线程没有关系了