多线程学习笔记

82 阅读6分钟

线程概念

线程是一个执行流,线程在进程内部执行。是操作系统调度的最小单位!一切线程都在进程内部执行,对比进程,线程与进程之间的区别和联系具体如何,下面从各个角度进行分析。

页表

回顾进程,先前,我们统一认为进程=进程的代码+数据+进程内核数据结构,以Linux系统为例,这里的内核数据结构就是task_struct和mm_struct,这里需要着重研究mm_struct,以下是mm_struct的每个分区的数据存储的情况:

mm_struct.png

对于页表,页表项,页框,页帧这几个概念我们必须进行区分:

页表:负责进行虚拟地址空间和物理内存空间的地址转换

页表项:记录每一个虚实地址转换的信息

页框:物理内存的最小划分单位

那么一个进程在内存中是按照地址空间进行划分的。在进程没有调入内存的时候,它就是一个程序。 更准确地描述,程序就是磁盘上的一个文件。那么既然进程是按照地址空间进行划分的。那么在编译器对写好的代码文件编译的时候,这个程序早就已经按照对应的进程地址空间的划分方式进行了划分。 准确地来说,一个程序文件也被分成了好几块。而每一个文件块,对应的都是4kB. 假如说,地址空间是4GB,如果对应一个地址就是一个页表项。那么光存放一个页表就要占掉不小的内存。同样,由于一一对应的页表项必须是连续存储。即使按照4Kb一个空间进行划分,也仍旧需要占用不小的连续的存储空间。所以为了解决这个问题,引入了多级页表的结构。这里以二级页表为例:

image.png

而这些工作都离不开硬件单元MMU的支持,当我们非法访存操作的时候。系统就是通过检测对应的地址转换机构发现了非法请求,通知操作系统发送信号终止进程。

线程 vs进程 &Linux如何看待进程和线程

线程和进程对比,主要有如下的优势

  • 线程切换的开销小,每次切换。操作系统都要维护对应的上下文信息。说白了,就是大量的寄存器里面的临时数据!而线程涉及到切换的时候,由于线程是共享进程的大部分资源的。因此在涉及进程切换的时候,无需切换地址空间和页表。开销比较小
  • 线程间通信方便。由于进程互相独立性较强,导致两个进程间交互数据的成本相对高。但是同一个进程内部的线程是可以看到对方绝大多数的资源。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • IO密集型应用,为了提高性能,将IO操作重叠,线程可以同时等待不同的IO操作

说明:计算密集任务:是指那些以计算为主的任务。比如数据加密解密,大数据查找。

IO密集型应用:以IO操作为主的任务,比如读写磁盘、访问数据库等等操作。

对于Linux系统而言,在操作系统认为。进程和线程,拥有绝大多数相似的行为,对于这样高度相似的两个实体。Linux并没有重新为线程独立设计一个新的数据结构,也就是在Linux操作系统认为,无论是线程还是进程,它看到的只有一个结构--->task_struct,所以线程对于Linux系统而言,有一个新的名字--->轻量级进程

#Linux查看线程的命令
ps -aL

Linux下线程的使用

这里要说明一件事,就是Linux创建线程并不是Linux系统原生提供的系统调用,而是调用的第三方库--->pthread,每一个使用Linux内核的系统都会提供这个pthread库

创建线程

创建线程使用的是pthread_create这个函数接口,对应的函数说明如下:

#include <pthread.h>
int pthread_create(       pthread_t *thread,\ 
                          const pthread_attr_t *attr,\
                          void *(*start_routine) (void *) , \
                          void *arg);

第一个参数是线程的id,第二个参数是线程的属性(设置成nullptr就行),第三个是我们希望线程执行的任务(回调函数),第四个参数是对应传递给回调函数的参数。下面是一个简单的demo代码

//在编译的时候带上选项-lpthread
//pthread_create函数调用
#include <iostream>
#include<pthread.h>
#include<unistd.h>
void* routine(void* args)
{   
  (void)args; //消除告警
   while(true)
   {
      std::cout<<"我是创建的线程,我在不停执行任务"<<std::endl;
      sleep(1);
   }
   return nullptr;
}
int main()
{ 
  pthread_t tid; 
  pthread_create(&tid,nullptr,routine,nullptr);
  while(true)
  {
    std::cout<<"我是主线程,我一直跑"<<std::endl;
    sleep(1);
  }
  return 0;
}

调用线程创建接口以后,当前的进程就会分出至少两个线程:一个是主线程,另一个是子线程,其中

  • 同一个进程内,任意一个线程的退出都会导致所有线程异常退出。
  • 主线程通常需要执行子线程退出以后的善后工作。

pthread_join

主线程通常需要进行子线程退出的善后工作,这个善后工作就是pthread_join函数,,先来看对应函数的说明:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

第一个参数对应的是线程的参数,第二个参数对应参数的含义比较复杂,今天作为简单使用就不进行拓展,默认设置为nullptr即可,下面是demo代码:

#include <iostream>
#include<pthread.h>
#include<unistd.h>
void* routine(void* args)
{   
  (void)args; //消除告警
   while(true)
   {
      std::cout<<"我是创建的线程,我在不停执行任务"<<std::endl;
      sleep(1);
   }
   return nullptr;
}
int main()
{ 
  pthread_t tid; 
  pthread_create(&tid,nullptr,routine,nullptr);
  while(true)
  {
    std::cout<<"我是主线程,我一直跑"<<std::endl;
    sleep(1);
  }
  //等待线程退出
  pthread_join(tid,nullptr);
  return 0;
}

线程终止

如果需要终止线程,可以使用如下两个接口

 #include <pthread.h>
 int pthread_cancel(pthread_t thread);
 void pthread_exit(void *retval);

线程分离

有的情况下,我们不需要主线程进行对于某些子线程的善后处理。这个时候就可以调用线程分离函数来对线程进行分离。对应的接口是pthread_detch,来看对应的函数声明

#include <pthread.h>
int pthread_detach(pthread_t thread);

传入对应的线程id就可以了