Linux 线程概念及线程控制

1.线程与进程的关系

执行流(Execution Flow)通常指的是程序执行过程中的控制路径,它描述了程序从开始到结束的指令执行顺序。例如我们要有两个执行流来分别进行加法和减法的运算,我们可以通过使用 fork 函数来创建子进程,父进程进行加法运算,子进程进行减法的运算来完成任务,但是创建一个进程需要额外创建PCB,进程地址空间,页表等等,代价过大效率过低,于是出现了线程的概念。

在Linux系统中,线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中。每个线程都有自己的运行状态,包括程序计数器、寄存器集和栈。

在Linux内核中,并没有严格地区分进程和线程,它们都是通过task_struct数据结构来表示的。每个进程或线程都有一个对应的task_struct实例,其中包含了该进程或线程的许多重要信息。因此,可以将线程看作是共享进程地址空间的轻量级进程。 

线程是进程内的执行单元。在同一进程中创建的所有线程都共享相同的地址空间、全局变量和其他资源。这意味着,线程间数据交换的成本较低,不需要额外的复制操作。然而,这同时也意味着,任何一个线程对共享资源的更改都会直接影响到其它线程。

进程 = 内核数据结构 + 代码和数据

上图中,多个PCB,进程地址空间,页表等都属于进程的内核数据结构,因此这些所有内容加起来,才算做进程。

线程 = 内核数据结构 + 共享的代码和数据

进程是承担系统资源分配的基本实体,操作系统分配资源时,以进程为单位,当一个进程拿到资源后,再去分配给不同的线程。线程和进程有如下图的关系:

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int gval = 100;
void *ThreadRountine(void *args)
{char* name = static_cast<char*>(args);cout << name << " is running,&gval= " << &gval << endl;return nullptr;
}
int main()
{vector<pthread_t> threads;for (int i = 0; i < 5; i++){pthread_t tid;string name = "Thread" + to_string(i);pthread_create(&tid, nullptr, ThreadRountine, (void *)name.c_str());sleep(1);}return 0;
}

我们可以看到多个进程使用的是同一个gval,即线程共享相同的地址空间、全局变量和其他资源。

当然如果我们想要在不同的线程里创建全局变量,即全局变量线程各自有一份,就需要用到关键字thread_local或__thread(thread前有两个下划线)。

推荐使用thread_local,在 C++11 及以后的版本中,thread_local 关键字用于声明线程局部变量。这些变量在每个线程中都有独立的实例,它们的生命周期与线程的生命周期绑定。thread_local 变量在第一次使用时初始化,并在线程结束时自动销毁。使用 thread_local 变量可以避免数据竞争和不必要的同步,因为每个线程操作的是自己的数据副本。

thread_local int gval = 100;

2.Thread Control Block (TCB)

Linux中,由于线程和进程的内核数据结构相似,所以两者共用一套PCB数据结构来表示,但大部分操作系统描述管理线程,并不是使用PCB,而是额外设计了Thread Control Block (TCB)。

Thread Control Block (TCB) 是操作系统中用于管理和控制线程的数据结构。在上图中,多个TCB分别控制不同部分的代码,CPU调度线程时,也去调度TCB。比如主流的WindowsMacOS等操作系统,都使用这样的方式。它是线程在系统中的核心表示,存储了线程的关键属性、状态信息以及与线程生命周期管理相关的数据。TCB 包含了以下主要信息:

  • 线程标识符 (TID):唯一标识线程的编号。
  • 线程状态:如就绪、运行、阻塞、结束等,指示线程当前的活动状态。
  • 调度信息:包括线程的优先级、时间片、调度策略等,这些信息决定了线程何时以及如何被调度执行。
  • 上下文保存:包括CPU寄存器状态(如程序计数器、通用寄存器、状态寄存器等),这些信息在线程切换时需要保存和恢复。
  • 资源关联:关联了线程使用的系统资源,如私有堆栈、共享资源的锁定状态、等待队列链接等。

3. 轻量级进程 LWP

在Linux中,一个进程可以有多个PCB,当PCB的数目为1,那么这个PCB可以代表一个进程;如果PCB数目有多个,那么这个PCB代表一个进程的多个线程。因此在Linux中,没有真正的线程,一个执行流由一个PCB维护。Linux把这种介于线程与进程之间的状态,称为轻量级进程 LWP (Light Weight Process)。

4. 进程资源分配给线程的机制

进程通过页表机制实现虚拟地址空间到物理地址空间的映射,从而管理和分配资源。在多线程的场景中,尽管有多个执行流(线程),但它们共享同一进程的虚拟地址空间和页表。这意味着所有线程可以访问进程的代码段、数据段、堆和栈等资源,而无需为每个线程单独分配这些资源。

当创建一个新线程时,它并不需要一个全新的页表,而是继承其父进程的页表。这样,线程可以直接使用进程已经映射到物理内存的虚拟地址。这种共享页表的方式极大地减少了内存的消耗,并且简化了线程的创建和管理过程。

不论是内存还是磁盘,都被划分为了以4kb为单位的数据块,一个数据块可以被称为页框 / 页帧。

操作系统管理内存,或者管理磁盘,都是以4kb为基本单位的。比如把磁盘中的数据加载到内存中,就是以4kb为基本单位进行拷贝。页框是被struct page管理的,Linux 2.6.10中,struct page源码如下:

struct page 
{page_flags_t flags;		atomic_t _count;		atomic_t _mapcount;	unsigned long private;		struct address_space *mapping;pgoff_t index;struct list_head lru;	#if defined(WANT_PAGE_VIRTUAL)void *virtual;	
#endif 
};

操作系统想要管理所有的页框,只需要创建一个数组,数组的元素类型是struct page。此时操作系统对内存或磁盘的管理,就变成了对数组的增删查改。而且从上方的struct page源码中可以发现,它是不存储页框的起始地址和终止地址,因为可以通过下标计算出起始地址,起始地址 + 4kb就可以求出终止地址。

我们以32位操作系统为例,页表的结构如下:

页表的任务是把虚拟地址解析为物理地址,当传入一个虚拟地址,页表就要对其解析。一个32位的地址,会被分为三部分,第一部分是前10位,第二部分是中间10位,第三部分是末尾12位。

第一部分就是上图中的深蓝色部分,其由页目录进行解析。2^10 = 1024,即前10位地址有1024种可能,而页目录就是一个长度为1024的数组。解析地址时,先通过前10位,在页目录中找到对应的下标。每个页目录的元素,指向一个页表。

第二部分是上图中的黄色部分,其由页表进行解析,同样的 2^10 = 1024,即中间10位地址也1024种可能,所以每个页表的长度也是1024。解析中间10位时,在页表中找到对应的下标,从而找到对应的内存。

第三部分时上图中的绿色部分。一个数据块大小是4 kb,这是内存管理的基本单位。而 2^12 byte = 4 kb,因此第三部分也叫做页内偏移,通过前两个部分,我们已经可以锁定到内存中的一个页框了,而第三部分存储的是物理地址相对于页框起始地址的偏移量,此时就可以根据起始地址 + 偏移量来确定一个地址。

以上就是页表解析地址的全过程。

5. 线程控制

Linux系统并不像某些其他操作系统那样提供由内核直接提供的线程库,因为本质上Linux是没有线程的,而是通过轻量级进程来模拟线程,所以Linux是在用户层实现了一套多线程方案,以库的方式提供给用户使用。最常用的线程库是POSIX线程库(pthread),它提供了创建、终止、同步和调度等一系列函数来管理线程。

故而在使用gcc / g++编译时,要带上选项 -l pthread,来引入原生线程库。例如:

g++ -o test.exe test.cpp -l pthread

线程创建 (pthread_create)

pthread_create函数是POSIX线程库(pthreads)中用于创建新线程的标准接口,包含在头文件<pthread.h>中。它允许用户指定线程的属性、线程函数及其参数,并返回新创建线程的标识符TID。这个函数在多线程编程中非常重要,因为它是启动新执行流程的基础。

参数:

  • pthread_t *thread:指向一个pthread_t类型的变量的指针,该变量将接收新创建线程的标识符TID。
  • const pthread_attr_t *attr:指向线程属性的指针。如果为NULL,则使用默认属性。
  • void *(*start_routine) (void *):线程函数的入口点,其签名必须匹配此形式,接受一个void *类型的参数并返回同样类型的值。
  • void *arg:传递给线程函数的参数。

返回值:

  • 如果创建成功,返回0
  • 如果创建失败,返回错误码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
void *ThreadRountine(void *arg)
{string name = static_cast<char*>(arg);while (1){cout << "I am " << name << ",mypid is " << getpid() << endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRountine, (void *)"Thread-1");while (1){cout << "I am Thread_main,mypid is " << getpid() << endl;sleep(1);}return 0;
}

在上述代码中,我们创建了一个线程 Thread-1,并且在主线程和新线程都在循环输出一段话,而输出的语句中我们发现主线程和新线程的 pid 相同,原因是他们在同一进程中,故 pid 相同,而我们区分不同线程是通过 tid 。

pthread_self

pthread_self 是 POSIX 线程库中的一个函数,它用于获取调用该函数的线程的唯一标识符。这个标识符是 pthread_t 类型的,可以用于识别和区分同一进程中的不同线程。pthread_self 在多线程编程中非常有用,尤其是在需要进行线程同步或管理线程资源时。

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;void *ThreadRountine(void *arg)
{string name = static_cast<char *>(arg);while (1){cout << "I am " << name << ",mypid is " << getpid() << ",mytid is" << pthread_self()<< endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRountine, (void *)"Thread-1");while (1){cout << "I am Thread_main,mypid is " << getpid() << ",mytid is" << pthread_self() << endl;sleep(1);}return 0;
}

我们发现线程 tid 很大,因为这个 tid 表示的是线程在内存的物理地址。

我们知道线程控制是通过pthread库实现的。在pthread动态库中线程被结构体描述,同时再被数据结构组织。

线程终止

pthread_exit

pthread_exit 是 POSIX线程库中的一个重要函数,用于从线程中退出。相比于简单地让函数返回,pthread_exit 允许传递一个 exit status 给可能正在等待该线程结束的其他线程或函数。

参数:

  • retval: 此参数是可选的,通常用来向主线程或其他等待该线程结束的线程传递退出状态。我们知道线程创建时函数的返回值为void*类型,retval就是用来终止线程然后代替返回值。

pthread_cancel

pthread_cancel 是 POSIX 线程库中的一个函数,用于向指定的线程发送取消请求。当一个线程收到取消请求后,它将进入取消状态,并根据线程的取消类型和取消点的设置,决定取消请求的处理方式。线程可以配置为在到达取消点时立即响应取消请求,或者延迟响应直至下一个取消点。

参数:

  • thread: 要发送取消请求的线程的 TID。

返回值:

  • 如果函数成功发送取消请求,返回 0;
  • 如果发送取消请求失败,返回非零值。

pthread_exit函数用于使当前线程正常退出,pthread_cancel函数用于向另一个线程发送取消请求。

线程等待(pthread_join)

pthread_join 函数是 POSIX 线程库中的一个同步函数,它用于等待一个或多个线程终止。当一个线程调用 pthread_join 并传递另一个线程的 ID 时,调用该函数的线程将被阻塞,直到被等待的线程结束执行。一旦被等待的线程终止,pthread_join 函数返回,并且可以选择性地获取该线程的退出状态。如果不需要获取退出状态,可以将第二个参数设置为 NULL

参数:

  • thread:等待的线程的TID
  • retval:输出型参数,线程退出后,该参数会接收到退出线程的函数返回值

返回值:

  • 如果等待成功返回 0
  • 如果失败返回错误码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
void *ThreadRountine(void *arg)
{string name = static_cast<char *>(arg);int cnt=3;while (cnt--){cout << "I am " << name << ",mypid is " << getpid() << ",mytid is " << pthread_self()<< endl;sleep(1);}return (void*)123456;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRountine, (void *)"Thread-1");void* retval;pthread_join(tid,&retval);printf("Thread-1 return value is %lld\n",(long long)retval);return 0;
}

线程分离(pthread_detach)

pthread_detach 函数用于将一个线程设置为分离状态(detached state)。在分离状态下,当线程结束时,它所占用的资源会自动被操作系统回收,无需其他线程显式调用 pthread_join 来等待其结束。这意味着,一旦线程被设置为分离状态,它就不再与创建它的线程绑定,可以独立存在直至结束。

参数:

  • thread:要设置为分离状态的线程的标识符TID。

返回值:

  • 如果函数成功,返回值为 0
  • 如果函数失败,返回对应的错误码。

使用 pthread_detach 的场景通常是在那些不需要等待线程结束或者不关心线程退出状态的情况下。例如,在一个长时间运行的服务器程序中,主线程可能会创建多个子线程来处理客户端请求,而主线程本身需要继续监听新的请求。在这种情况下,可以在子线程的执行函数中通过调用 pthread_detach(pthread_self()) 来设置子线程为分离状态,这样即使主线程不等待子线程结束,子线程在完成任务后也能正确地释放资源. 

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
void *ThreadRountine(void *arg)
{pthread_detach(pthread_self());string name = static_cast<char *>(arg);int cnt=3;while (cnt--){cout << "I am " << name << ",mypid is " << getpid() << ",mytid is " << pthread_self()<< endl;sleep(1);}return (void*)123456;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRountine, (void *)"Thread-1");return 0;
}

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

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

相关文章

全面了解 NGINX 的负载均衡算法

NGINX 提供多种负载均衡方法&#xff0c;以应对不同的流量分发需求。常用的算法包括&#xff1a;最少连接、最短时间、通用哈希、随机算法和 IP 哈希。这些负载均衡算法都通过独立指令来定义&#xff0c;每种算法都有其独特的应用场景。 以下负载均衡方法&#xff08;IP 哈希除…

置分辨率设置多显示器的时候提示, 某些设置由系统管理员进行管理

遇到的问题 设置分辨率设置多显示器的时候提示&#xff08;如下图所示&#xff09;&#xff1a; 某些设置由系统管理员进行管理 解决方法 先试试这个方法&#xff1a; https://answers.microsoft.com/zh-hans/windows/forum/all/%E6%9B%B4%E6%94%B9%E5%88%86%E8%BE%A8%E7%8…

拓森空调计费系统

随着现代建筑技术的不断发展&#xff0c;中央空调系统已经成为许多大型建筑、商场、办公楼等场所的必备设施。为了更有效地管理和控制中央空调的使用&#xff0c;同时实现能源的合理分配和费用的精确计算&#xff0c;空调计费系统应运而生。 空调计费系统是一种用于精确计算每个…

Java时区国际化解决方案

当用户所在时区和服务器所在时区不一致时,会产生时区相关问题,如时间显示错误、程序取得的时间和数据库存储的时间不一致、定时任务的触发没有跟随用户当前的时区等等问题. 统一拦截时区 /*****/ Component Slf4j public class TimeZoneIdInterceptor implements HandlerInte…

前端开发设计模式——状态模式

目录 一、状态模式的定义和特点 二、状态模式的结构与原理 1.结构&#xff1a; 2.原理&#xff1a; 三、状态模式的实现方式 四、状态模式的使用场景 1.按钮的不同状态&#xff1a; 2.页面加载状态&#xff1a; 3.用户登录状态&#xff1a; 五、状态模式的优点 1.提…

RabbitMQ 入门(七)SpringAMQP五种消息类型(Topic Exchange)

一、Topic Exchange&#xff08;消息模式&#xff09; TopicExchange 与DirectExchange类似&#xff0c;区别在于routingKey可以是多个单词的列表&#xff0c;并且以.分割。 Topic类型的Exchange与Direct相比&#xff0c;都是可以根据RoutingKey把消息路由到不同的队列。只不过…

数据结构与算法——Java实现 42.二叉树的最大深度

苦尽甘来时&#xff0c;一路向阳开 —— 24.10.21 104. 二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&a…

微软数据恢复工具- “快速扫描” 和 “深度扫描” 两种模式 快速扫描的速度更快,使用 NTFS 文件系统下的目录结构

提供了 “快速扫描” 和 “深度扫描” 两种模式。快速扫描的速度更快&#xff0c;使用 NTFS 文件系统下的目录结构和文件名恢复文件&#xff1b;而深度扫描则能帮你恢复更多丢失目录结构和文件。有了 WinFR 界面版&#xff0c;你不需要再学习任何复杂的命令行操作了&#xff0c…

extra_model_paths.yaml解读

为了将模型文件放置在1个共享位置&#xff0c;以方便重装comfyui或其他需要用到模型共享的情况&#xff0c;将在修改extra_model_paths.yaml中遇到的错误情况汇总如下&#xff1a; 1、当模型路径指引前面空格不是4个时错误如下&#xff08;示例范本中后面的例子就是因为是5个空…

重磅揭秘,AI 编程崛起,真的会让程序员面临裁员危机吗?

"完了&#xff0c;AI 要取代程序员了&#xff01;" 我的朋友圈里经常会分享一些 AI、AI 编程的东西&#xff0c;最近收到不少人的私信&#xff1a; "要不要转行啊&#xff1f;""现在学编程还有意义吗&#xff1f;""听说隔壁公司已经用 AI…

117. 填充每个节点的下一个右侧节点指针 II【 力扣(LeetCode) 】

文章目录 零、LeetCode 原题一、题目描述二、测试用例三、解题思路3.1 层次遍历3.2 层次遍历&#xff08;优化&#xff09; 四、参考代码4.1 层次遍历4.2 层次遍历&#xff08;优化&#xff09; 零、LeetCode 原题 117. 填充每个节点的下一个右侧节点指针 II 一、题目描述 给…

OpenCV高级图形用户界面(17)设置一个已经创建的滚动条的最小值函数setTrackbarMin()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::setTrackbarMin 这个函数的作用就是设置指定窗口中轨迹条的最小位置。这使得开发者能够在程序运行时动态地调整轨迹条的范围&#xff0c;而不…

如何安装和初始化飞牛私有云 fnOS?

如何安装和初始化飞牛私有云 fnOS&#xff1f;

万家数科:零售业务信息化融合的探索|OceanBase案例

本文作者&#xff1a;马琳&#xff0c;万家数科数据库专家。 万家数科商业数据有限公司&#xff0c;作为华润万家旗下的信息技术企业&#xff0c;专注于零售行业&#xff0c;在为华润万家提供服务的同时&#xff0c;也积极面向市场&#xff0c;为零售商及其生态系统提供全面的核…

对称二叉树

给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; 树中节点数目在范围…

一款实现PLC扩展CANFD的好工具 — PXB-6020D协议转换器

如何轻松实现PLC扩展CAN FD&#xff1f;本文将简单介绍PLC上的CAN接口&#xff0c;并分享一款简单的好工具——PXB-6020D&#xff0c;它能帮助我们轻松实现从Modbus到CANFD的无缝转换。 在工业自动化领域&#xff0c;PLC&#xff08;可编程逻辑控制器&#xff09;是核心组件之一…

民宿在线预订:SpringBoot技术实践指南

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

基于SSM服装定制系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;服装类型管理&#xff0c;服装信息管理&#xff0c;服装定制管理&#xff0c;留言反馈&#xff0c;系统管理 前台账号功能包括&#xff1a;系统首页&#xff0c;个人中心&#xf…

Linux LCD 驱动实验

LCD 是很常用的一个外设&#xff0c;在裸机篇中我们讲解了如何编写 LCD 裸机驱动&#xff0c;在 Linux 下LCD 的使用更加广泛&#xff0c;再搭配 QT 这样的 GUI 库下可以制作出非常精美的 UI 界面。本章我们就来学习一下如何在 Linux 下驱动 LCD 屏幕。 Framebuffer 设备 先来…

基于vue框架的的点餐系统1o2te(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,商家,菜品分类,菜品信息 开题报告内容 基于Vue框架的点餐系统开题报告 一、研究背景与意义 随着移动互联网技术的飞速发展&#xff0c;餐饮行业也迎来了数字化转型的浪潮。传统的点餐方式&#xff0c;如纸质菜单和人工记录&…