浅析Linux下的多线程(上)

声明:
虚拟机版本:Centos 7.4
环境:gcc编写

本文目录

    • 一.关于线程
    • 1.1 线程的概念
    • 1.2 线程的优点
    • 1.3 线程的缺点
    • 1.4 多线程应用场景
    • 二.线程VS进程
    • 三.关于线程的相关函数
    • 3.1 创建线程
    • 3.2 终止线程
    • 3.3 等待线程
    • 3.4 线程分离
    • 3.5 函数的运用

为什么要有线程?
如果说你现在是一个工厂的老板,工厂里有一条生产线。现在供不应求,必须扩大生产规模。如果是进程的角度,就是另外再键一个工厂,复制之前的生产线;但如果我直接在原来的工厂里增加几条生产线呢?那么这个扩建规模是不是要小很多,这种方式就是线程的方式。

一.关于线程

1.1 线程的概念

操作系统进行调度运算的基本单位。说太多的概念容易混淆,我们直接通过图片来直观感受。
浅析Linux下的多线程(上)_第1张图片
从上图中可以得出一些结论:

  • 线程是在进程内运行的(进程地址空间),且一个进程至少有一个或多个线程。
  • Linux中,把线程叫做轻量级进程(LWP)
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

1.2 线程的优点

相比于进程(有对比才有伤害)

  1. 创建和销毁的开销更小。
  2. 切换调度的开销更小。
  3. 占用的资源更少。

这都是因为线程共用同一块虚拟地址空间。

1.3 线程的缺点

  1. 健壮性较低:(一个线程异常终止会导致进程异常终止)
  2. 编程(包括调试)难度增大:(易导致线程安全问题)

1.4 多线程应用场景

  • CPU密集型:线程一执行就会占用大量的cpu资源。
  • IO密集型:不占用cpu资源
    a)通过网络进行输入输出:(同时下载多部电视剧)
    b)响应UI界面:(多线程完成界面显示和数据计算,防止数据计算太久导致界面卡死)

二.线程VS进程

理论上来说,进程是资源分配的基本单位,线程是调度分配的基本单位。
从资源的角度来看,在同一个进程中的线程之间既有共用的资源,又有独占的资源。

  • 共用资源
    (1)虚拟地址空间
    (2)文件描述符表
  • 不共用资源(注意不是私有资源)
    (1)栈(函数调用栈,局部变量等)
    (2)上下文信息(CPU中的寄存器)
    (3)errno:每个线程都有自己各自的errno----thread local
    (4)线程号 tid
    (5)信号屏蔽字block

注意:(1)所说的栈并不是虚拟地址空间中的栈(这个是主线程的栈),这个栈(包括所有的不共用资源)都存在放在栈和堆中间那段共享内存区里面,里面包含了很多关于线程的信息。
那么将上图继续完善,就涵盖了进程与线程的区别。(详细例子参考滑稽吃鸡、工厂和工厂生产线)
浅析Linux下的多线程(上)_第2张图片

三.关于线程的相关函数

请注意:线程控制相关函数不是系统调用而是库函数,需要包含头文件:pthread.h p代表posix线程库

3.1 创建线程

创建函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

pthread_t *thread//所创建线程的id,是一个输出型参数
const pthread_attr_t *attr//设置线程属性,一般为NULL
void *(*start_routine) (void *)//参数和返回值都为void*的函数指针,相当于这个新线程的入口函数,将指定这个新线程执行哪段代码
void *arg//上一个参数--入口函数的参数

ps:如果创建一个进程,需要两个入口函数,应该怎么办?
将函数手动包含在一个结构体中,将结构体地址传进去即可。

下面通过一段代码来创建一个进程,熟悉一个这个函数。

#include 
#include 
#include 

void* ThreadEntry(void* arg)
{
  while(1)
  {
    printf("In ThreadEntry\n");
    sleep(1);
  }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,NULL);
  while(1)
  {                                                                                    
    printf("In MainThread\n");
    sleep(1);
  }
  return 0;
}

写好后,用gcc编译会发现报错,为什么?
在这里插入图片描述
这是典型的链接错误(对于函数只看到了声明,未看到定义)该函数的定义在一个静态库或者动态库里面(如果需要知道在哪种库中,ldd+编译生成的可执行文件名:查得所在库路径为:/lib64/libpthread.so.0,是一个动态链接库)。
对于这种报错,使用gcc -l表示链接一个库,直接在l后面加上库名即可
所以我们在后面加上一个-lpthread:执行语gcc thread.c -o test -lpthread
运行结果:虽然看上去两个线程是交替执行的,但是如果再执行到后面就会发现,有可能会连续出现多个MainThread或者多个ThreadEntry,因为线程之间是抢占式执行的,用户无法决定一个线程是执行还是休眠,被成为多线程编程的万恶之源。
浅析Linux下的多线程(上)_第3张图片
为了能宏观上看线程,再新建一个窗口,先使用命令ps -eLf | head -n 1打印表头,
再使用命令ps -eLf |grep test查看当前所有的线程信息,LWP表示线程id。第一个线程和第二个线程的PID和PPID都相同,但LWP不同,证明是一个进程下的两个不同的线程。
浅析Linux下的多线程(上)_第4张图片
下面我再通过pthread_self()来打印一下线程的pid,需要将上面代码中的两条输出语句改写成如下:

printf("In ThreadEntry:%lu\n",pthread_self());//使用lu与pthread类型对应
printf("In MainThread:%lu\n",pthread_self());

再次运行后,就可以看到打印出来的线程id,发现线程的id是这样一串串数字。
浅析Linux下的多线程(上)_第5张图片
此时我再用ps -eLf查看一下当前的线程,发现了这样的情况:当前线程的id与打印出来的线程id并不一样!到底哪个是真的?
浅析Linux下的多线程(上)_第6张图片
通过一番折腾,原来这两个都是线程的id,只不过是站在两个不同的角度;ps得到的线程id是站在内核角度给PCB加了一个编号,而pthread_self()得到的线程id是站在posix线程库的角度得到的,一般无特殊情况以第二个为主。
除此之外,查看线程的方法还有:

  1. pstack+进程id查看,
  2. gdb attach+进程号附加进程,再通过info thread查看有几个线程。另外,thread n 切换到n号线程,再用bt可查看线程调用栈。

3.2 终止线程

(1)线程入口函数结束(最主要)

假如将上面代码中创建出来的线程的入口函数改写成一个有限循环:

void* ThreadEntry(void* arg)
{
  int count=5;
  while(count)
  {
    printf("In ThreadEntry\n");
    count--;
    sleep(1);
  }
}

那么当while循环结束,函数退出的时候,这个线程就结束了。

(2)调用函数pthread_exit()–结束本线程
哪个线程调用该函数,哪个线程就结束。注意不要与结束进程的函数exit()混淆

void pthread_exit(void *retval);//void* 是线程结束的返回结果,一般置为NULL

那么我现在来改变一下代码,在之前的这个线程入口函数调用该函数,其他地方保持不变,从而该线程结束。

void* ThreadEntry(void* arg)
{
  int count=5;
  while(count)
  {
    printf("In ThreadEntry\n");
    count--;
    sleep(1);
  }
  pthread_exit(NULL);
}

改变后,执行结果发生了变化:从箭头处开始,ThreadEntry这个线程就结束了。(还可以通过ps再次验证查看该线程是否还存在)
浅析Linux下的多线程(上)_第7张图片
(3)调用pthread_cancel()–结束任意线程(不太推荐)
注意:这里的任意线程是指本进程中的线程

int pthread_cancel(pthread_t thread);//传入需要结束的线程号

那么再来稍微修改一下代码,尝试一下这种方法。

#include 
#include 
#include 

void* ThreadEntry(void* arg)
{
  while(1)
  {
    printf("In ThreadEntry\n");
     sleep(1);
  }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,NULL);
  while(1)
  {                                                                                    
    printf("In MainThread\n");
    sleep(1);
    pthread_cancel(tid);
  }
 return 0;
}

程序运行后,ThreadEntry只会执行一次,在main线程中会将该线程结束。
浅析Linux下的多线程(上)_第8张图片
那么当pthread_cancel()函数出现的时候,假设这个要被结束的线程内的程序还没有执行完,那么会出现什么情况?
依旧在之前的基础上改进一下代码,加上一个全局数组,在新线程里面执行一个遍历赋值的操作,那么,在主线程pthread_cancel()执行的时候,新线程对于数组的操作还没有执行完,就会出现只改动一半的尴尬情况,就违背了原子性。所以这种方法不太推荐使用。

#include   
#include   
#include   
  
int arr[1000000]={0};  
  
void* ThreadEntry(void* arg)  
{  
  (void) arg;  
  for(size_t i=0;i<sizeof(arr)/sizeof(arr[0]);i++)  
  {  
    arr[i]=i;  
  }  
  return NULL;                                                                         
}                                                                                 
                                                                                  
int main()                                                                        
{                                                                                 
  pthread_t tid;                                                                  
  pthread_create(&tid,NULL,ThreadEntry,NULL);                                     
  printf("In MainThread");                                                        
  pthread_cancel(tid);                                                            
  return 0;                                                                       
}       

3.3 等待线程

举个例子:还是基于原来的代码:将线程入口函数修改一下,改成一个while死循环。

#include   
#include   
#include   
void* ThreadEntry(void* arg)    
{                               
  (void) arg;                   
  while(1)                      
  {       
    printf("In ThreadEntry\n");
    sleep(1);
  }                            
  return NULL;
}             
 
int main()
{         
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,NULL);
  printf("In MainThread");                   
  pthread_cancel(tid);    
  return 0;           
}     

本来正常情况下,新线程永远都不会结束,但由于pthread_cancel函数的执行,会使得新线程强制结束,就会导致问题(这就类似于进程中,子进程结束时,如果父进程不回收子进程的资源,将会造成僵尸进程)所以,当一个线程结束时,会将线程结束返回的结果保存到PCB中,其他线程都可以去查看。防止出现类似于僵尸进程的内存泄漏的情况。

等待函数(阻塞函数)

int pthread_join(pthread_t thread, void **retval);
pthread_t thread//要等待的线程
void **retval//输出型参数--一般为NULL
//如果等待的线程一直不结束,那么这个函数会一直阻塞,实质是为了等待线程结束再执行后面的代码,控制执行逻辑---计算一个庞大的矩阵相乘,每个线程计算其中一部分,主线程使用pthread_join来保证所有线程都执行完

3.4 线程分离

由于当pthread_join函数等待的线程不结束时,pthread_join函数会一直等待阻塞,然而我们并不希望这样的情况产生,但是线程结束后的资源又必须得回收,所以这时候就可以采用线程分离,该线程结束后会自动释放资源。它类似于忽略SIGCHLD 信号,即父进程扔下子进程不管。
分离函数:

int pthread_detach(pthread_t thread);
pthread_t thread//分离的线程号

该函数一般在创建该新线程后使用,不需要再通过pthread_join来回收资源

#include   
#include   
#include   
void* ThreadEntry(void* arg)    
{                               
  (void) arg;                   
  while(1)                      
  {       
    printf("In ThreadEntry\n");
    sleep(1);
  }                            
  return NULL;
}             
 
int main()
{         
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,NULL);
  pthread_detach(tid);
  printf("In MainThread");                      
  return 0;           
}     

3.5 函数的运用

了解了这些函数后,我来写一些相关代码熟悉一下这些函数

  1. 证明线程之间能够共享虚拟内存地址空间
    第一次:定义一个全局变量g_val,同时在主线程和新线程内使用改变量。
#include   
#include   
#include   

int g_val=0;
void* ThreadEntry(void* arg)
{
  (void)arg;
  while(1)
  {
    printf("In ThreadEntry\n");
    g_val++;//新线程的使用
    sleep(1);
  }
  return NULL;
}
int main()
{
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,NULL);
  pthread_detach(tid);
  while(1)
  {
    printf("In MainThread:%d\n",g_val);//主线程的使用                                             
    sleep(1);
  }
  return 0;
}

从打印结果可以看出g_val的++和打印操作确实在同时交替进行,可以证明,两个线程在同时访问这份空间。
浅析Linux下的多线程(上)_第9张图片
第二次:在main函数内部定义一个局部变量val,同时打印这个val,那么如果此时我需要新线程也访问这个变量,是不是就访问不到了?
我们可以利用pthread_create函数的第四个参数,因为这个参数是新线程入口函数的参数,所以这个参数写成&val就ok了。再来写成代码来尝试一下。

#include   
#include   
#include   

void* ThreadEntry(void* arg)                                                       
{                                                                                  
  int *p=(int*)arg;//记得转换类型                                                                    
  while(1)                                                                         
  {                                                                                
    printf("In ThreadEntry\n");                                                    
    (*p)++;                                                         
    sleep(1);                                                                      
  }                                                                                
  return NULL;                                                                     
}                                                                                  
int main()                                                                         
{
  int val=0;
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,&val);
  pthread_detach(tid);
  while(1)
  {
    printf("In MainThread:%d\n",val);
    sleep(1);
  } 
  return 0;
}       

执行打印后,结果与全局变量完全一致。
第三次:前两次证明都是开辟的在堆上的空间,那么如果是堆上面的一块空间,线程之间还能共享吗?那么再次通过修改代码来验证一下。

#include   
#include 
#include   
#include   

void* ThreadEntry(void* arg)                                                       
{                                                                                  
  int *p=(int*)arg;//记得转换类型                                                                    
  while(1)                                                                         
  {                                                                                
    printf("In ThreadEntry\n");                                                    
    (*p)++;                                                         
    sleep(1);                                                                      
  }                                                                                
  return NULL;                                                                     
}                                                                                  
int main()                                                                         
{
  int* p=(int*)malloc(4);
  *p=0;
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,p);
  pthread_detach(tid);
  while(1)
  {
    printf("In MainThread:%d\n",*p);
    sleep(1);
  } 
  return 0;
}       

执行后,与前两次结果一模一样,可以证明,线程之间能够共享虚拟内存地址空间。

  1. 线程异常终止的情况

恢复main函数里面的内容,让新线程里面执行一个指针的越界访问,使得新线程异常终止。

#include   
#include 
#include   
#include   

void* ThreadEntry(void* arg)  
{    
  (void)arg;  
  while(1)  
  {  
    sleep(1);  
    int* p=NULL;  
    *p=10;  
  }  
  return NULL;  
}    
int main()  
{    
  pthread_t tid;  
  pthread_create(&tid,NULL,ThreadEntry,NULL);  
  while(1)  
  {  
    printf("In ThreadEntry\n");                                                        
    sleep(1);                                              
  }          
  return 0;  
}  

执行结果:当看到段吐核的时候就代表所有线程都结束了。
在这里插入图片描述

  1. 多线程如何利用多核资源

由于多线程存在的目的就是为了利用cpu的多核资源,现在可以通过程序来验证一下,首先要保证代码运行环境下有多核,我的是内核数为2(如果没有,将处理器数量设置成多个就ok了)
浅析Linux下的多线程(上)_第10张图片
先简单写一个代码,体会一下没有线程时cpu的资源利用。写一个while(1)的循环。

#include   
int main()
{
    while(1);
    return 0;
}

再新复制一个会话,用top查看一下当前系统资源占用情况,结果发现,进程号为16963的进程占用了当前一个cpu的所有资源。那么这个进程是不是在执行while(1)的这个进程?用ps aux | grep test查看一下即可证明。
浅析Linux下的多线程(上)_第11张图片
接下来引入一个新线程,产生两个执行流,让它利用cpu的多核资源,也就是将另外一个处理器也利用起来,让资源利用率达到200%

#include   
#include 
#include   
#include   
void* ThreadEntry(void* arg)
{
  (void)arg;
  while(1);
  return NULL;
}
int main()
{
  pthread_t tid;
  pthread_create(&tid,NULL,ThreadEntry,NULL);
  while(1);                                                                
  return 0;
}

可以看到,此时cpu利用率已经达到200,说明我的线程已经利用了多核的c资源。
浅析Linux下的多线程(上)_第12张图片
如果此时继续增加线程数,对于我的机器来说,cpu利用率不会再增高。因为我的机器内核总数为2,所以两个线程数就已经使得cpu利用率达到上限。如果一台机器内核总数为4,那么它的线程数最多可以为4就使得cpu利用率达到最大。
总结: 虽然多线程可以利用cpu的多核资源,但线程数不是越多越好,当cpu利用率达到上限后,如果继续曾加线程数,加大调度的开销,反而会降低效率。

  1. 通过多线程提高程序执行效率

说到效率,就想起时间,这里我构造了一个场景:假设存在一个很大的数组,将数组中每个元素都进行一个乘方运算,再赋值回数组。 可以通过对引入多线程前后程序执行时间的计算来比较效率。

//单线程:
#include   
#include 
#include   
#include   
#include 
#define SIZE 100000000
 //获得当前精确时间--微秒
 int64_t GetUs()//int64_t:64位上的long long
 {
   struct timeval tv;
   gettimeofday(&tv,NULL);
   return tv.tv_sec*1000000+tv.tv_usec;
 }
void Calu(int* arr,int begin,int end)
 {
   int i=begin;
   for(;i<end;i++)
   {
     arr[i]=arr[i]*arr[i];
   }
 }
 int  main()
 {
   srand(time(NULL));//时间种子
   //由于在栈上无法开辟一个很大的数组,所有在堆上开辟
   int* arr=(int*)malloc(sizeof(int)*SIZE);
   //当前想计算从这个程序的执行时间
   //用时间戳
   //先记录开始的时间
   int begin=GetUs();
   Calu(arr,0,SIZE);
   //再记录结束的时间
   int end=GetUs();
 
   //两个时间做差得到执行时间
   printf("time=%ld\n",end-begin);
   return 0;
 }

程序执行后,这里我就以1.5秒为准。
浅析Linux下的多线程(上)_第13张图片
引入多线程后:多个线程同时计算,每个线程执行程序的一部分。这样就可以大大提高程序的执行效率。

//多线程
typedef struct Arg
 {
   int begin;
   int end;
   int* arr;
 }Arg;
 
 void* ThreadEntry(void* arg)
 {
   Arg* p=(Arg*)arg;
   Calu(p->arr,p->begin,p->end);
   return NULL;
 }
int main()
{
  int* arr=(int*)malloc(sizeof(int)*SIZE);
  Arg args[THREAD_NUM];
  int i=0;
  int base=0;
  //给每个线程分配任务,使得它们执行不同的模块
  for(;i<THREAD_NUM;i++)
  {
    args[i].begin=base;
    args[i].end=base+SIZE/THREAD_NUM;
    args[i].arr=arr;
    base+=SIZE /THREAD_NUM;
  }
  pthread_t tid[THREAD_NUM];
  int64_t begin=GetUs();
  i=0;
  for(;i<THREAD_NUM;i++)
  {
    pthread_create(&tid[i],NULL,ThreadEntry,&args[i]);
  }
  i=0; 
  for(;i<THREAD_NUM;i++)
  {
    pthread_join(tid[i],NULL);
  }
  int64_t end=GetUs();
  printf("time= %ld\n",end-begin);
  return 0;
}      

虽然我们在多线程的编写中会感到一些困难,但是在实际中有一些库或者框架能让我们很方便的书写多线程(Open MPI)
线程的同步和互斥
(1)当线程数目过多时,可能多个线程会争抢同一份资源(互斥)
(2)当线程使用同一个资源时,导致同一份资源释放多次(互斥)
(2)资源分配不均匀,导致某个线程一直得不到执行的机会(同步)

你可能感兴趣的:(Linux,linux,多线程)