Linux多线程(二)

简介: Linux多线程

二、进程 VS 线程

线程共用同一个地址空间,因此代码段(Text Segment)、数据段(Data Segment)等都是共享的:


若定义一个函数,在各线程中都可以调用

若定义一个全局变量,在各线程中都可以访问到

除此之外,各线程还共享以下进程资源和环境:


文件描述符表(进程打开一个文件后,其他线程也能够看到)

每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)

当前工作目录(cwd)

用户ID和组ID

进程是承担分配系统资源的基本实体,线程是CPU调度的基本单位。线程共享进程数据,但也拥有自己的一部分数据:


线程ID

一组寄存器(存储每个线程的上下文信息)

栈(每个线程都有临时的数据,需要压栈出栈)

errno(C语言提供的全局变量,但每个线程都有自己的)

信号屏蔽字

调度优先级

三、Linux线程控制

3.1 POSIX线程库

在Linux中,站在内核角度上看并没有真正意义上线程相关的接口。但站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统在应用层提供了原生线程库pthread。原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口


应用层指的是这个线程库并不是操作系统直接提供的,而是由第三方使用系统接口编写的

原生指的是大部分Linux系统都会默认带上该线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"开头

要使用pthread库,要引入头文件<pthreaad.h>

链接pthread库时,要在编译时要使用"-lpthread"选项

注意:


传统的函数是,成功返回0,失败返回-1,并且对全局变量errno设置以指示错误。pthread函数出错时并不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误信息通过返回值返回

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

3.1 线程创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数:


thread:获取创建成功的线程ID,该参数是一个输出型参数

attr:用于设置创建线程的属性,传入NULL表示使用默认属性

start_routine:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数

arg:传给线程例程的参数(即传给start_routine的形参)

返回值:


线程创建成功返回0,失败返回错误码


使用案例


让主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行自己的新例程,而主线程则继续执行后续代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* Routine(void* args)
{
  while (1) {
        cout << "I am " << (char*)args << endl;
    sleep(1);
  }
}
int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, Routine, (void*)"thread 1");
  while (1) {
        cout << "I am main thread!" << endl;
    sleep(2);
  }
  return 0;
}

2941d34a8c2c4b3baebd170e8ee24f3e.png


使用 ps -aL 命令,可以显示当前的轻量级进程,不带 -L 选项默认显示进程


76a960627e344706a1538d90264c4a22.png


LWP(Light Weight Process)就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程。


在Linux中,应用层的线程与内核的LWP是对应的,实际上操作系统调度时使用的是LWP,而并非PID。单线程进程时PID和LWP是相等的,所以对于单线程进程而言,调度时采用PID和LWP是一样的;多线程进程时PID与主线程LWP相同。


3.3 线程等待

线程如同进程一般,也是需要被等待的。若主线程不对新线程进行等待,那么新线程的资源不会被回收,会发生类似于"僵尸进程"的问题,即内存泄漏。


使用pthread_join()可以进行线程等待


int pthread_join(pthread_t thread, void **retval);

参数:


thread:被等待线程的ID

retval:线程退出时的退出码信息

返回值:


线程等待成功返回0,失败返回错误码

调用该函数的线程将阻塞到ID为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的


若thread线程通过return返回,retval所指向的单元里存放的是线程的返回值

若thread线程被别的线程调用pthread_cancel()异常终止掉,retval所指向的单元里存放的是宏PTHREAD_CANCELED,即(void*)-1)

若thread线程是自行调用pthread_exit()终止的,retval所指向的单元存放的是传给pthread_exit的参数

若对thread线程的终止状态不感兴趣,可传NULL给retval参数

使用案例

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* Routine(void *args)
{
    cout << (char*)args << endl;
    sleep(3);
    return (void*)0;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)"new thread");
    void* ret = nullptr;
    int n = pthread_join(tid,&ret);
    if(n == 0) {
        cout << "等待成功" << endl;
        cout << "返回信息为: " <<(long long)ret << endl;
    }
    else {
        cout << "等待失败" << endl;
    }
    return 0;
}


9c4604fad226470587ddb49d7687ec27.png


3.4 线程终止

3.4.1 return退出

在创建线程时指定的例程中使用return代表当前线程退出,但在main函数中使用return代表整个进程退出,即主线程退出了那么整个进程就退出了。


3.4.2 pthread_exit()

void pthread_exit(void *retval);

参数retval:线程退出时的退出信息


注意:


pthread_exit()或者return返回的指针所指向的内存单元必须是全局的或者堆区开辟的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程已经退出了

线程退出不能使用exit()函数,其作用是退出整个进程,任何一个线程调用都是如此

3.4.3 pthread_cancel()

int pthread_cancel(pthread_t thread);

参数thread:被取消线程的ID


返回值:线程取消成功返回0,失败返回错误码


线程是可以取消自己的(使用pthread_self()函数)。也可以让新线程取消主线程,但不建议这么使用,一般都是使用主线程去控制新线程的。


取消成功的线程的退出码一般是宏PTHREAD_CANCELED,即(void*)-1)


c83b3dfdc45a4295a5108bac9db8db84.png


3.5 线程分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。但若本身并不关心线程的返回值,那么join也是一种负担,此时可将该线程进行分离,后续当线程退出时就会自动释放线程资源

线程若被分离了,这个线程依旧使用该进程的资源,且依旧在该进程内运行,甚至这个线程崩溃了一定会影响整个进程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离

joinable和分离是冲突的,一个线程不能既是joinable又是分离的

使用pthread_detach()函数进程分离线程

int pthread_detach(pthread_t thread);

参数thread:被分离线程的ID


返回值:线程分离成功返回0,失败返回错误码


3.6 线程ID与进程地址空间布局

pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,但该线程ID和内核中的LWP并不是一回事

内核中的LWP属于CPU调度的范畴,因为线程其实就是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程

pthread_create()函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,这个ID属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的

线程库NPTL提供的pthread_self()函数,获取的线程ID和pthread_create()函数第一个参数获取的线程ID是一样的

线程ID到底是什么?


可以将线程ID打印出来看看

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* Routine(void* args) {
    cout << (char*)args << " : " << pthread_self() << endl;
    return (void*)0;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,Routine,(void*)"new thread");
    sleep(1);
    cout << "main thread : " << pthread_self() << endl;
    pthread_join(tid,nullptr);
    return 0;
}


b5164018a39f43b0804ff7aca16ea04e.png


可以发现,这个线程ID数值特别大,并不是LWP,那么这个线程ID到底是什么呢?


Linux系统中不提供真正的线程ID,只提供LWP,即操作系统只需通过LWP对轻量级进程进行管理,而供用户使用的线程接口等其他数据,由线程库来管理,因此管理线程时的"先描述,再组织"就应该在线程库中完成


使用 lld 命令可以看到,线程库实际上是一个动态库(默认使用动态库)


a57f9c635fa440d8a58612fd0675c3a7.png


进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时进程内的所有线程是共享这个动态库的


67cb0949b571467a8b34e6154dd2ce4d.png


之前提到每个线程都有独占的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有各自的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。


每一个新线程在共享区都有一个struct pthread对其进行描述,因此要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息

14ed2ad053634fe7b77be08cacfbfad7.png



上面讲述的各种线程函数,本质上都是在库内部对线程属性进行的各种操作,即线程数据的管理本质是在共享区的进行的


至于pthread_t到底是什么类型取决于实现,但对于Linux目前实现的NPTL线程库来说,线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程


相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
7月前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
140 1
|
5月前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
110 0
|
3月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
50 0
Linux C/C++之线程基础
|
3月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
5月前
|
存储 设计模式 NoSQL
Linux线程详解
Linux线程详解
|
5月前
|
缓存 Linux C语言
Linux线程是如何创建的
【8月更文挑战第5天】线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。
|
5月前
|
负载均衡 Linux 调度
在Linux中,进程和线程有何作用?
在Linux中,进程和线程有何作用?
|
5月前
|
缓存 Linux C语言
Linux中线程是如何创建的
【8月更文挑战第15天】线程并非纯内核机制,由内核态与用户态共同实现。
|
7月前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现
|
7月前
|
Linux API
Linux线程总结---线程的创建、退出、取消、回收、分离属性
Linux线程总结---线程的创建、退出、取消、回收、分离属性

热门文章

最新文章