Linux - 线程

目录

一.Linux线程的概念

1.1什么是线程

1.2 线程的优点

1.3 线程的缺点

1.4 线程异常

1.5 线程用途

二. Linux进程VS线程

2.1 进程和线程

三. Linux线程控制

3.1 POSIX线程库

3.2 创建线程

3.3 进程ID和线程ID

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

3.5 线程终止

3.6 线程等待

3.6.1 为什么需要线程等待?

3.7 分离线程


一.Linux线程的概念

1.1什么是线程

  1. 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  2. 一切进程至少都有一个执行线程
  3. 线程在进程内部运行,本质是在进程地址空间内运行
  4. 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  5. 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程 执行流

1.2 线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

1.3 线程的缺点

  • 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器

如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。

  • 健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制

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

  • 编程难度提高

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

1.4 线程异常

  1. 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  2. 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出

1.5 线程用途

  1. 合理的使用多线程,能提高CPU密集型程序的执行效率
  2. 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是 多线程运行的一种表现)

二. Linux进程VS线程

2.1 进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    • 线程ID
    • 一组寄存器
    • 栈 errno
    • 信号屏蔽字
    • 调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment(文本段)、Data Segment(数据段)都是共享的,如果定义一个函数,在各线程中 都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

三. Linux线程控制

3.1 POSIX线程库

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

3.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函数会这样做)。而是将错误代码通过返 回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通 过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>void* rout(void* arg)
{int i;for (; ; ) {printf("I'am thread 1\n");sleep(1);}
}
int main(void)
{pthread_t tid;int ret;if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0) {fprintf(stderr, "pthread_create : %s\n", strerror(ret));exit(EXIT_FAILURE);}int i;for (; ; ){printf("I'am main thread\n");sleep(1);}
}

3.3 进程ID和线程ID

  • 在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称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;...
};
  • 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct) 与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID

ps命令中的-L选项,会显示如下信息:

  • LWP:线程ID,既gettid()系统调用的返回值。
  • NLWP:线程组内线程的个数

可以看出上面 threadpool 进程是多线程的,进程ID为157397,进程内有3个线程,线程ID分别为157387 157414 157415。

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

#include <sys/syscall.h> 
pid_t tid; tid = syscall(SYS_gettid);
  • 从上面可以看出,threadpool 进程的ID为157397,下面有一个线程的ID也是157397,这不是巧合。线程组内的第一 个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将 线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以 线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。
/* 线程组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);
}
  • 强调一点,线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。

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

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是 一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于 NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
    pthread_t pthread_self(void);

    pthread_t到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就 是一个进程地址空间上的一个地址。

3.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;失败返回错误码

3.6 线程等待

3.6.1 为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

功能:等待线程结束

原型

        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线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{printf("thread 1 returning ... \n");int *p = (int *)malloc(sizeof(int));*p = 1;return (void *)p;
}
void *thread2(void *arg)
{printf("thread 2 exiting ...\n");int *p = (int *)malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}
void *thread3(void *arg)
{while (1){ //printf("thread 3 is running ...\n");sleep(1);}return NULL;
}
int main(void)
{pthread_t tid;void *ret;// thread 1 returnpthread_create(&tid, NULL, thread1, NULL);pthread_join(tid, &ret);printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);free(ret);// thread 2 exitpthread_create(&tid, NULL, thread2, NULL);pthread_join(tid, &ret);printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);free(ret);// thread 3 cancel by otherpthread_create(&tid, NULL, thread3, NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED)printf("thread return, thread id %lX, return code:PTHREAD_CANCELED\n", tid);elseprintf("thread return, thread id %lX, return code:NULL\n", tid);
}

3.7 分离线程

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

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

pthread_detach(pthread_self());

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{pthread_detach(pthread_self());printf("%s\n", (char *)arg);return NULL;
}
int main(void)
{pthread_t tid;if (pthread_create(&tid, NULL, thread_run, (void *)"thread1 run...") != 0){printf("create thread error\n");return 1;}int ret = 0;sleep(1); // 很重要,要让线程先分离,再等待if (pthread_join(tid, NULL) == 0){printf("pthread wait success\n");ret = 0;}else{printf("pthread wait failed\n");ret = 1;}return ret;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/626454.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SASE:打造数据安全保障新模式

在企业纷纷拥抱数字业务的过程中&#xff0c;由于边缘计算、云服务、混合网络的逐渐兴起&#xff0c;使得本就漏洞百出的传统网络安全架构更加岌岌可危&#xff0c;而且远远无法满足企业数字业务的需要。 伴随企业全球化发展&#xff0c;企业的数据中心不再是用户与设备访问需…

全球7大指纹浏览器排行榜:哪个最适合你?

在数字时代&#xff0c;我们每一次上网都会留下独特的数字足迹&#xff0c;被称为“浏览器指纹”。为了保护这些私人信息不被滥用&#xff0c;指纹浏览器成为了一个重要工具。但是&#xff0c;并非所有的指纹浏览器都是一样的&#xff0c;它们各有特点&#xff0c;适用于不同的…

【春季发布】LinkSLA智能运维V6.0发布 聚焦架构升级 新增带外管理

LinkSLA智能运维为企业IT部门提供覆盖资源管理、监控告警、IT服务台、日志管理、MOC值守服务等多项功能为一体的运维平台&#xff0c;通过打通各业务单元、贯穿各技术栈&#xff0c;以故障定位和全生命周期管理为核心&#xff0c;持续保障业务连续性。 本次V6.0版本全面升级&a…

最佳AI实践|如何在 Dify 用 Workflow 构建一个 Blog SEO AI 应用?

最佳AI实践&#xff5c;如何在 Dify 用 Workflow 构建一个 Blog SEO AI 应用&#xff1f; 文章目录 最佳AI实践&#xff5c;如何在 Dify 用 Workflow 构建一个 Blog SEO AI 应用&#xff1f;常见的内容写作场景如何持续提升 AI 的写作能力&#xff1f;开始设计 Workflow确立 Wo…

Linux系统中LVM与磁盘配额

目录 一、LVM逻辑卷管理 二、LVM的管理命令 物理卷管理 卷组管理 逻辑卷管理 *创建并使用LVM步骤 三、磁盘配额概述 实现磁盘限额的条件 Linux 磁盘限额的特点 四、磁盘配额管理 磁盘限额 一、LVM逻辑卷管理 能够在保持现有数据不变的情况下动态调整磁盘容量&#…

git 分支-变基

在git中&#xff0c;将一个分支的更改集成到另一个分支有两种主要方式&#xff1a;合并&#xff08;merge&#xff09;和变基&#xff08;rebase&#xff09;。在本节中&#xff0c;将学习什么是变基&#xff0c;如何执行变基操作&#xff0c;为什么它是一个非常强大的工具&…

js脚本解决因挂VPN导致boss上高德地图无法正常规划公交路线问题

​ 情况说明&#xff1a; 最开始一直以为boss上的查询自己所在地点到面试地点的公交的功能有bug,总是规划路线失败&#xff0c;结果这个一调试&#xff0c;发现是自己的经纬度有问题导致的。 程序猿嘛&#xff0c;开机VPN必须是挂着的&#xff0c;这就导致了boss获取到了错误…

如何节约上架时间,小程序管理平台推荐

继微信正式推出微信小程序后&#xff0c;各个大厂陆续发布了各自的小程序平台 —— 支付宝小程序、百度小程序、头条小程序&#xff0c;各家不同的小程序标准一度让开发者们激情开骂&#xff0c;虽然目前跨平台的小程序开发可以通过taro、mpvue、kbone等跨平台开发框架来解决&a…

JookDB下载安装使用

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

Android 纵向双选日历

这个日历的布局分两部分&#xff0c;一部分是显示星期几的LinearLayout&#xff0c;另外就是一个RecyclerView&#xff0c;负责纵向滚动了。 工具类&#xff1a; implementation com.blankj:utilcode:1.17.3上activity_calendar代码&#xff1a; <?xml version"1.0&…

Jmeter测试学习笔记

第一章 jmeter基础知识 一.Jmeter工具中的组件 1.测试计划&#xff1a;Jmeter测试的起点。容器。 2.线程组&#xff1a;代表一定的用户 3.取样器&#xff1a;发送请求的最小单元 4.逻辑控制器&#xff1a;处理请求逻辑 5.前置处理器&#xff1a;请求之前的操作 6.后置处…

vue3推荐算法

Vue 3 推荐算法主要指的是在 Vue 3 框架中实现的或者适用于 Vue 3 的算法库或组件库。Vue 3 由于其优秀的设计和性能&#xff0c;被广泛应用于构建各种类型的应用程序&#xff0c;包括需要复杂算法支持的项目。以下是一些在 Vue 3 中可能会用到的推荐算法资源&#xff1a; Vue-…