37 线程控制

本文详细介绍了Linux中线程的概念、POSIX线程库的使用、线程的创建与管理、进程ID与线程ID的区别、线程终止与等待、c++11线程支持、多线程私有变量以及线程分离。着重讨论了线程库如何封装操作系统接口,以提供用户友好的线程API。
摘要由CSDN通过智能技术生成

内核中没有明确的线程的概念,线程作为轻量级进程。所以不会提供线程的系统调用,只提供了轻量级进程的系统调用,但这个接口比较复杂,使用很不方便,我们用户,需要一个线程的接口。应用层对轻量级进程的接口进行封装,为用户提供了应用层线程的接口,封装在pthread线程库。几乎所有的linux平台都默认自带这个库,编写多线程代码,需要使用第三方pthread库

目录

1.POSIX线程库
2.创建线程
3.进程ID和线程ID
4.线程id及进程地址空间布局
5.线程终止
6.线程等待
7.测试
8.c++11线程
9.多线程私有变量
10.线程分离

1. POSIX线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread”打头的
要使用这些函数库,需要引入头文件<pthrad.h>
链接这些线程函数库时要使用编译器命令的“lpthread”选项

2. 创建线程

功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)
(void
), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

错误检查:
传统的一些函数是成功返回0,失败返回-1.并且对全局变量errno赋值以指示错误
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误码通过返回值返回,可以调用sterror函数解析
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码,对于pthreads函数的 错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

测试

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

using namespace std;
void *run(void *args)
{
    while (true)
    {
        printf("new thread: %d\n", getpid());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, run, nullptr);

    while (true)
    {
        printf("main thread: %d\n", getpid());
        sleep(1);
    }
}

因为是第三方库,所以编译时需要-l带上pthread库名

g++ -o $@ $^ -std=c++11 -lpthread

在这里插入图片描述

pid是一样的,说明是一个进程

3. 进程ID和线程ID

在linux中,目前的线程实现是Native POSIX Thread Library,简称NPTL,在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程后,情况发生了变化,一个用户机场南管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求 进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题?
linux引入了进程组的概念

struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};

多线程的进程,又称为线程组,线程组内的每一个线程在内核中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上是对应进程id,起始对应线程ID,进程描述符的tgid,含义是Thread Group ID,对应用户层面的进程id

用户态系统调用内核进程描述符中对应的结构
线程idpid_t gettid (void)pid_t pid
进程idpid_t getpid (void)pid_t tgid

现在介绍的线程ID,不同于pthread_t类型的线程id,和进程id一样,线程ID是pid_t类型,而且是用来唯一标识线程的一个整型变量

查看轻量级进程

ps -aL

在这里插入图片描述在这里插入图片描述
ps命令的 -L选项,会显示如下信息:
LWP:线程id,即gettid()系统调用的返回值
NLWP: 线程组内线程的个数

进程id和线程id一样是主线程,内核称为(group leader),也就是进程,内核在创建第一个线程时,会将线程组的id的值设置为第一个线程线程id,group_leader指针指向自身,即主进程的进程描述符。不一样的是子线程

linux提供了gettid调用来返回线程id,可是glibc并没有将该系统调用封装起来,在开放接口来供使用。如果确实需要获得线程id,可以使用如下方法:

#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);

/* 线程组ID等于线程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);

至于线程组其他线程的id则由内核负责分配,其线程组id总是和主线程的线程组id一样,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样

if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}

强调一点,线程和进程不一样,进程有父进程的概念,在线程组里面,所有的线程都是对等关系

同一个线程组的线程,没有层次关系

4. 线程id及进程地址空间布局

创建轻量级进程的函数
在这里插入图片描述

上面的clone函数需要传入调用的函数,自定义栈空间,flag是要不要地址空间共享等待。一个是参数比较负责,一个是有的内容os不让调用。所以线程库对其封装,产生了上面的一些函数。
在这里插入图片描述
线程的概念是库维护的,线程库要加载到内存中。要维护多个线程属性合集,也要进行管理。线程要执行的函数是什么,栈空间在哪,线程id是什么,状态是什么,时间片还有多少等待,库需要维护。线程结构有一个线程控制块,存储用户关心的信息,包含线程的栈,id是什么,os关心的信息,LWP底层指向哪一个执行流,os才能运行。所以是用户级线程。对外返回的线程id实际上可以认为是这个线程结构的地址,如下图

pthread_creat会产生一个线程id,存放在第一个参数指向的地址中。该线程id和前面说的线程id不是一回事
前面的线程id属于进程调度的范畴,因为线程是轻量级进程,是os调度的最小单位,所以需要一个数值来唯一标识线程
pthread_creat函数第一个参数指向一个虚拟内存单元,该内存单元的地址即位新创建线程的现场id,属于NPTL线程库的范畴。现场库的后续操作,就是根据该线程id操作线程的

pthread_t pthread_self (void)

prhread_t是什么类型,取决于实现,对于目前实现的NPTL而言,pthread_t类型的线程id,本质是一个进程地址空间上的一个地址
在这里插入图片描述

5. 线程终止

如果需要终止某个线程有三种方法:
1.线程函数内调用return,这种方法对主线程不适用,从main函数return相当于调用exit
2.线程调用pthread_exit终止自己
3.一个线程可以调用pthread_cancel终止同一个进程中的另一个线程

pthread_exit

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了

pthread_cancel

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

6. 线程等待

为什么

已经退出的线程,空间没有被释放,仍然在地址空间中
创建新的现场不会复用刚才退出线程的地址空间

方法

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程终将被挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的 ,总结如下:
1.如果thread线程通过return返回,value_ptr指向的单元里存放的是thread的返回值
2.如果thread线程被别的线程调用thread_cancle异常终止,value_ptr所指向的单元里存放的是PTHREAD_CANCELED,值为-1
3.如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的是传给pthread_exit的参数
4.如果对thread线程的终止状态不感兴趣,可以传NULL做参数

7. 测试

线程的参数是void*,所以还可以返回结构体之类的结果。
用线程计算两个数之前所有数的求和,通过主线程返回取到结果,并判断结果的可靠性

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>

using namespace std;

struct Request
{
    Request(int st, int en)
    :start(st), end(en)
    {}
    int start;
    int end;
};

struct Response
{
    int result;
    int exitcode;  //结果是否可靠
};

void *sum(void *num)
{
    Request* eq = static_cast<Request*>(num);
    Response* ret = new Response;
    int i = eq->start;
    while (i <= eq->end)
    {
        ret->result += i;
        i++;
    }
    delete eq;
    return ret;
}

int main()
{
    pthread_t tid;
    Request* req = new Request(1, 100);
    pthread_create(&tid, nullptr, sum, req);
    void *ret;
    pthread_join(tid, &ret);

    printf("结果:%d\n", ((Response *)ret)->result);
}

用一个类接收两数区间,申请一个类,传入转换为对应类型计算,申请结构类放入返回,最后转换类型输出
在这里插入图片描述
如果计算量比较大,可以将一个计算分成几组交给每个线程,父线程将所有结果汇总

8. c++11线程

pthread是原生线程库,c++11对这个库进行了封装,也支持多线程了
编译的时候仍然需要带上库连接

void* run()
{
    while (true)
    {
        printf("子线程\n");
        sleep(1);
    }

}

int main()
{
    thread th(run);
    th.join();
    return 0;
}

原生线程只能用于linux平台,库的封装基于不同平台会有不同的实现,想跨平台的话得用库函数

线程的完整结构是用户级线程+内核级LWP,所谓用户级还是内核级的区分是线程在哪里实现。linux是用户级线程,用户执行流和内核lwp是1:1的

9. 多线程私有变量

生成多条线程,打印地址

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>

using namespace std;

void* run(void* num)
{
    long n = (long)num;
    int cnt = 5;
    while (cnt--)
    {
        printf("第%d条线程,地址:%p\n", n, pthread_self());
        sleep(1);
    }
       
}

int main()
{
    vector<pthread_t> v;
    for (long i = 0; i < 3; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, run, (void*)i);
        v.push_back(tid);
    }

    sleep(1);
    for (size_t i = 0; i < 3; i++)
    {
        pthread_join(v[i], nullptr);
    }
}

在这里插入图片描述

每条线程的栈空间都是不一样的。对于全局变量,每个线程都可以访问,这种叫共享资源。如果主线程想要得到某个线程的值,也可以取到,通过全局变量判断是哪条线程赋值,主线程访问全局变量。这里取第1条线程:
在这里插入图片描述
线程和线程之间,几乎没有秘密,栈的数据,也可以被其他线程看到并访问
尽量让每个线程的变量独立,不要互相影响

线程的局部存储

怎么让每个线程拥有独立的全局变量,可以前面加__pthread。前面线程的机构里说线程的tcb结构里有线程的局部存储,这部分存储的就是动态库中的这些变量,是编译器提供的编译选项,会给每个线程提供一份

__thread int g_val;

打印一下这个地址观察
在这里插入图片描述

每条线程对于这个变量都会有独立的地址,也就是每个线程都拥有私有的变量
局部存储只能定义内置类型,不能修饰自定义类型
局部存储可以保存线程里调用流需要多次读取的值,不需要传入参数或调用函数就可以访问

10. 线程分离

线程的等待是阻塞式的,但如果想让主线程做其他事情,同时不关心次线程的执行结果,可以将线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要join操作,否则无法释放资源造成内存泄露
分离后线程退出会自动释放资源

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程分离,也可以线程自己分离自己。joinable和分离是冲突的,不能既joinable又分离。分离后继续join会失败

在这里插入图片描述

detch的分离实际上是修改tcb的属性,记录有没有被分离,如果分离了就不能等待

线程里的地址可能不在堆栈之间,因为linux很多程序都有一个elf动态库的ld库,可能对地址进行修改。是os内置库,任何c++代码都要连接,方便加载,知道这个程序用哪些库,把哪些库加载进来

主线程用prhread_eixt退出,有可能其他线程不会退出

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值