Linux线程控制

创建线程

首先,Linux内核中是没有线程的概念的,但是有轻量级进程的概念。系统不会给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用。所以,我们要创建线程,就要使用pthread线程库。pthread线程库对轻量级进程接口进行了封装,为我们用户提供了直接线程的接口。使用库的方法是:添加头文件时,加上#include <pthread.h>。

线程创建函数pthread_create

 使用此函数,可以创建一个新线程,参数从头到尾依次代表:新线程ID、线程属性(暂时不管,一般都设为nullptr)、线程函数地址(新线程执行的地方)、新线程回调函数的参数。

下面让我们来试着创建一个线程,执行如下代码:

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>


using namespace std;

//new thread
void *threadRoutine(void *args)
{
    while(true)
    {
        cout << "new phread: " << getpid() << endl;
        sleep(2);       
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);  //不是系统调用
    while(true)
    {
        cout << "man thread: " << getpid() << endl;
        sleep(2);
    }
    return 0;
}

由于pthread线程库是个动态库(此动态库是电脑自带的),光是在程序开头引用头文件是不行的,在使用makefile文件编译时,我们还需要加上-pthread选项:

成功执行代码: 我们可以看到,主线程与新线程都有在运行,说明我们创建新线程成功。

但我们会发现一个问题,在执行结果中,为什么主线程与新线程的pid都是一样的?(都是13430)这是怎么回事?当然是一样的!因为线程本就是进程中的一个执行流,要知道,线程是操作系统调度的基本单位,进程是操作系统分配资源的基本单位,有线程<=执行流<=进程,可以说线程就是进程的一部分。那我们应该怎样去识别某个线程呢?别着急,运行程序后,再使用ps -aL指令看看:

注意看图中红框部分的线程属性,除了PID相同外,我们发现LWP列的数是不一样的,是的,就是你想的那样,LWP全称light weight process也就是轻量级进程的意思 ,LWP就是线程的ID(OS中对线程的标识符),当PID与LWP相等时,它就是主线程,不相等时就是用户所创建的新线程。

下面,我们再讨论一个问题,之前说过,进程独立的,进程间互不影响,我们删除一个进程,不会影响到另一个进程,那线程也是这样吗?我们不妨试一下,执行刚刚的代码,再创建一个窗口分别对主线程与新线程发送9号信号(杀死线程),我们看看会发生什么:

我们发现,无论我们是杀死主线程,还是新线程,我们的进程都会被杀死,即进程中的任何一个线程被干掉,进程都会被干掉,这也说明了线程的健壮性很差。

线程等待

创建子进程时,父进程会等待子进程,进而进行诸多操作,线程也不例外,主线程也需要等待其他线程。

线程等待函数pthread_join

 两个参数分别是等待进程的ID、等待进程的返回值。

线程退出函数 

参数为线程的退出码(也就是线程回调函数的返回值)

使用上述两个函数,可以实现主线程等待其他线程的功能。

小贴士:除了pthread_exit退出函数以外,还有以下函数也可以实现线程的退出:

  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  • 线程可以调用pthread_ exit终止自己。
  • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程(如下图,参数为需要终止的线程ID)。

分离线程 

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资 源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

线程分离函数 

 

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

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

前面我们说过,OS是没有线程的概念的,那么,线程到底是由谁来维护的呢?答案是线程库(pthread.h)!前面所讲的pthread_create函数第一个参数是存放线程ID的指针,其实,这里存放线程ID和前面所讲的线程ID(LWP)不是一回事。LWP表示的线程ID是属于进程调度的范畴,因为线程是轻量级进程,是OS调度器的最小单位,所以需要一个数值来唯一表示该线程。

pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的首地址即为新创建线程的线程ID,属于线程库的范畴。也就是说,线程是由线程库维护的(只维护线程的概念,不维护执行流!

如上图,我们知道线程库是动态库,那么它在内存中的位置是堆与栈之间的共享区域。pthread库将线程的所有属性进行描述并组织,打包为一个模块,也就是线程的tcb,再用链表的方式将每个线程tcb连接起来,最后对线程的管理,不还是对链表的增、删、查、改等操作,这也再次验证了管理数据的6字真言:先描述,再组织!

在线程tcb中,有LWP属性,由于线程库是应用层面的,线程库使用LWP属性去联系内核,作为线程在OS中的唯一标识符,让OS去识别本线程属于进程执行流中的哪一段,从而实现调度。

通过线程实现一个简单的和计算

首先,我们定义两个类,分别是和Request与Repose,Request里包含了和运算的参数、线程名与和运算函数:Repose则是包含了计算结果:

 

接着,我们编写主函数与线程回调函数:

void *Sum(void *args)
{
    Reponse *rp = new Reponse(0);
    Request *rqt = static_cast<Request*>(args);
    rp->_res = rqt->SumCount(rqt->_start, rqt->_end);
    
    delete rqt;
    return rp;
}

int main()
{
    pthread_t tid;
    Request *rqt = new Request(1, 100, "thread_1"); 
    pthread_create(&tid, nullptr, Sum, rqt);
    void *rtn;
    pthread_join(tid, &rtn);
    Reponse *rep = static_cast<Reponse *>(rtn);
    cout <<"Sum Value is: " << rep->_res << endl;
    
    delete rep;
    return 0;
}

代码编写完成后,编译并执行程序,执行结果:

我们可以看到窗口依次把需要求和的数打印了出来,当完成对最后一个数求和后,退出线程回调函数,并返回计算结果给主线程,主线程再将其接收并输出。 

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

我们可以用一段代码来验证这个话题,在主线程中创建多个线程,让创建的每个线程执行同一个回调函数,再在回调函数里定义局部变量,并打印其值与地址:

struct threadData
{
    string threadname;
};

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

string thread_Name(string threadname, int i)
{
    string name = "threadname" + to_string(i);
    return name;
}

void *threadRoutine(void *arge)
{
    int test_i = 0;
    threadData *td = static_cast<threadData*>(arge);
    int i = 0;
    while(i < 3)
    {
        cout << "pid: " << getpid() << ", tid: " 
        << toHex(pthread_self()) << ", threadname: " << td->threadname 
        << ", test_i " << test_i << ", &test_i: " << toHex((pthread_t)&test_i) << endl;
        i++;
        test_i++;
    }
    delete td;
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for(int i = 0; i < NUM; i++)
    {
        threadData *td = new threadData;
        td->threadname = thread_Name("thread ", i);
        pthread_t tid;
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    for(int i = 0; i < NUM; i++)
    {
        void *rtn;
        pthread_join(tids[i], &rtn);
    }

    return 0;
}

 运行结果;

注意看用三种不同颜色框住的地方,很容易看出,这是三个不同地址 ,所以,也成功验证了每个线程都有其独立的栈结构。但其实,我们线程与线程之间是没有秘密的,如果你想去访问到某个进程的局部变量,也是可以实现的,具体方法这里就不说了。

那我们试着定义一个全局变量,再去看看其值与地址会有什么不同?在程序里定义一个全局变量g_val,再在线程回调函数中去打印其值与地质:

void *threadRoutine(void *arge)
{
    int test_i = 0;
    threadData *td = static_cast<threadData*>(arge);
    int i = 0;
    while(i < 3)
    {
        cout << "pid: " << getpid() << ", tid: " 
        << toHex(pthread_self()) << ", threadname: " << td->threadname 
        << ", g_val " << g_val << ", &g_val: " << toHex((pthread_t)&g_val) << endl;
        i++;
        test_i++;
    }
    delete td;
    return nullptr;
}

结果如下:

可以看出,全局变量在内存的区域是所有线程所共享的,其实,除了这个区域外,堆区等范围也是所有进程所共享的。

其实,线程库还为线程设计了私有全局变量,定义方式如下,在变量前加上__thread(只能对内置类型有用) :

这样做以后,我们不改变代码其他部分,再次运行代码:

这样,又为每个线程创建了私有全局变量,至于私有全局变量的作用与用途,就留给各位小伙伴们去思考了。 

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西红柿鸡蛋超级美味

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值