Linux_初识多线程

17 篇文章 1 订阅

1.线程概念

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

重点线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。如下图所示:
在这里插入图片描述

  • 由以上叙述,我们对进程与线程应该有新的理解与认知
  1. 进程:此时的进程不再是简单的将代码右硬盘加载进内存的概念,进程:承担分配系统资源的基本实体。
  2. 线程:调度的基本单位,线程是进程里面的执行流!(线程在进程的地址空间内运行)。进程与线程的关系(1:n)。
  3. Linux中没有真正意义上的线程,线程是用进程模拟的。(轻量级进程LWP)
  4. 多进程:启动一个进程处理一个任务,多个pcb进行程序调度完成多个任务,每个pcb都有一个自己独立的虚拟地址空间。
  5. 多线程:有多个pcb共同调度着同一虚拟地址空间,相当于一个身体有多个头。
  6. 虚拟地址空间中代码段就是要完成的任务,代码段里面有多个函数,不同的线程pcb调度着不同的函数同时完成多个函数(pcb在之前是进程)。

2.线程优点

  1. 线程间共用一个虚拟地址空间,因此线程间的通信更加方便灵活(全局变量、 函数重载、传参)也能进行线程间通信
  2. 线程的创建与销毁成本更低—创建进程还需要创建页表,进行分配资源
  3. 线程的切换调度成本更低—不需要切换页表,避免页表切换
  4. 线程间共用进程的大部分资源,因此创建和销毁成本更低,调用成本也更低
  5. 能充分利用多处理器的可并行数量
  6. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  7. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  8. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

那么是不是线程越多越好?

  1. 线程不能太多,pcb会进行大量的调度切换。调度切换浪费的时间大于提高的效率 就得不偿失了。
  2. 线程间用了共同的虚拟地址空间,就是运用了相同的栈,就会出现调用栈混乱,多线程同样是这样,那么我们解下来来了解一下线程的独有与共享。

3.线程缺点

  1. 性能损失: 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  2. 健壮性降低: 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  3. 缺乏访问控制: 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  4. 编程难度提高: 比特科技编写与调试一个多线程程序比单线程程序困难得多

4.线程异常

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

5. 线程用途

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

6. 线程的独有与共享

  • 独有
  1. 每个线程都有自己的栈,防止调用栈混乱。
  2. 每个线程都是一个执行流,有自己的一套寄存器保存数据。
  3. 每个线程都是一个pcb,都有自己的
  4. 线程标识(描述符)
  5. 优先级
  6. errno(perror就是靠errno获得系统调用错误的信息)
  7. 信号屏蔽字(某些线程阻塞自己不想操作的信号,有些重要的执行流(pcb)不想被信号打断,想执行自己的任务,把某些特定的信号阻塞起来)
  • 共享
  1. 虚拟地址空间(代码段,数据段)
  2. 文件描述符表
  3. 信号处理方式(每个线程的处理方式要是不一样,那么就不知道一个信号交给哪一个线程去处理)
  4. 当前进程的工作路径
  5. 用户ID,组ID

7. 进程和线程的关系

在这里插入图片描述

8. 线程控制

  • 操作系统并没有给用户直接提供创建一个线程的接口。(线程就是个pcb,轻量级进程)所以久封装了一套线程库,用于线程控制。封装了一套库,就调度内核的执行流所有的接口都是库函数接口。通过用户线程(用户态实现的线程) 也是依靠 内核中的轻量级进程(线程)执行流完成调度的
    注意:创建线程用的是库函数,意味着我们用的是动态库里面的信息,而它被映射到共享区
    共享区:线程是用户态创建的线程,是用到动态库的信息,所以线程所用到的东西几乎都是在共享区

8.1 POSIX线程库

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

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

8.3 代码示例

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

void *pthread_run(void* arg)
{
  while(1)
  {
    printf("I am new %s:My pid is: %d My id is: %p \n",(char*)arg,getpid(),pthread_self());
    printf("\n");
    sleep(1);
  }
}
int main()
{ 
  pthread_t tid;
  pthread_create(&tid,NULL,pthread_run,(void*)"pthrtead");

  while(1)
  {

    printf("I am man pthread:My pid is : %d My id is: %p \n",getpid(),pthread_self());
    printf("The new pthread tid is: %p \n",tid);
    sleep(1);
  }

  return 0;
}

在这里插入图片描述
运行结果分析:
在这里插入图片描述

  • ldd pthread可以查看pthread依赖的库函数。

在这里插入图片描述

9 LWP概念

  • 在单进程单线程之中,PID和LWP是一样的,而单进程多线程之中,linux引入了线程组的概念,线程组中每一个线程(轻量级进程)都存在一个进程描述符LWP,这个轻量级进程描述符就是用户级进程ID,是OS调度的最小单位,主线程的LWP和PID是一样的。

9.1 PID与进程LWP

  1. 每一个进程都有自己独立的PID,PID是用来标识每一个不同进程的,LWP是用来标识轻量级进程的。CPU在进行调度时用的是LWP(线程是调度的基本单位)PID与LWP的关系是(1:N)如果不同的LWP拥有同一个PID则它们属于同一个进程。

9.2 查看LWP

ps -aL

在这里插入图片描述

9.3 线程ID和进程ID

  • 进程ID是每一个进程独有的ID,可以通过getpid函数获得,也可以通过 ps -ajx查看属于进程ID,而线程ID是每一个线程独有的ID,当使用pthread_create函数时,通过第一个参数将线程ID传出。

9.4 查看线程ID

pthread_t pthread_self(void)
  • 无参,返回调用这个函数的线程ID。

在这里插入图片描述

  • 命令ps -aL查看到的LWP和用函数pthread_self()查看到的线程ID都是用来标识不同线程的,那么有什么区别呢?pthread_self得到的是非常大的一个数字,ps -aL得到的LWP是比较正常的,这是为什么呢?

LWP得到的线程ID是真正的线程ID。pthread_self得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
这道题目是要求我们实现链表的基本操作,包括创建链表、插入节点、删除节点、遍历链表等。以下是一个简单的链表实现代码示例: ```c++ #include <iostream> using namespace std; // 定义链表节点结构体 struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; // 创建链表 ListNode* createList(int nums[], int n) { if (n == 0) return NULL; ListNode *head = new ListNode(nums[0]); ListNode *cur = head; for (int i = 1; i < n; i++) { cur->next = new ListNode(nums[i]); cur = cur->next; } return head; } // 插入节点 void insertNode(ListNode *&head, int pos, int val) { if (pos < 0) return; if (pos == 0) { ListNode *newNode = new ListNode(val); newNode->next = head; head = newNode; return; } ListNode *cur = head; while (cur && pos > 1) { cur = cur->next; pos--; } if (cur) { ListNode *newNode = new ListNode(val); newNode->next = cur->next; cur->next = newNode; } } // 删除节点 void deleteNode(ListNode *&head, int pos) { if (pos < 0) return; if (pos == 0) { ListNode *delNode = head; head = head->next; delete delNode; return; } ListNode *cur = head; while (cur && pos > 1) { cur = cur->next; pos--; } if (cur && cur->next) { ListNode *delNode = cur->next; cur->next = delNode->next; delete delNode; } } // 遍历链表 void traverseList(ListNode *head) { while (head) { cout << head->val << " "; head = head->next; } cout << endl; } int main() { int nums[] = {1, 2, 3, 4, 5}; int n = sizeof(nums) / sizeof(int); ListNode *head = createList(nums, n); traverseList(head); insertNode(head, 2, 6); traverseList(head); deleteNode(head, 3); traverseList(head); return 0; } ``` 在上面的代码中,我们定义了一个 `ListNode` 结构体作为链表节点,包括节点值 `val` 和指向下一个节点的指针 `next`。同时,我们实现了创建链表、插入节点、删除节点和遍历链表等基本操作。在使用链表时,我们可以先通过 `createList` 函数创建一个链表,然后对链表进行插入、删除和遍历操作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值