【Linux】线程控制

        欢迎来到小林的博客!!
      ️博客主页:✈️林 子
      ️博客专栏:✈️ Linux
      ️社区 :✈️ 进步学堂
      ️欢迎关注:点赞收藏✍️留言

目录

  • 线程的异常终止
  • 线程等待
  • 线程退出
  • 线程取消
  • 线程id
    • pthread如何管理线程
  • 线程分离

线程的异常终止

进程内部是可以存在多个线程的。那么如果有一个线程出现了异常(产生了信号)。那么会发生什么后果呢?我们用下面这段代码来验证一下。

#include
#include
#include 

void* ThreadRoutine(void* args)
{
  int id = *(int*)args;
  delete (int*)args;
  int count  = 0 ; 
  while(1)
  {
    printf("%d thread runing.... count = %d\n",id,count);
    //如果线程id为2, 且count计数到3,那么制造异常,产生信号 
    if(id == 2 && count == 3) 
    {
      int a = 10;
      a /= 0; 
    }
    sleep(1);
    count++;
  }
}


int main()
{
  pthread_t tids[3]; 
  //创建三个线程
  for(int i = 0 ; i < 3; i  ++)
  {
    int* id = new int(i);
    pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);
  }
  while(1)
  {
    printf("main thread runing...\n");
    sleep(2);
  }
  return 0 ;
}

该代码的逻辑就是创建3个线程,让第三个线程计数到3的时候 /0 产生信号。

我们运行看看结果。

【Linux】线程控制_第1张图片

我们发现整个进程都崩溃了。所以可以得出结论:

一个线程产生异常,那么所有线程都会崩溃。本质是因为所有线程共享信号处理函数,而信号来临时选择了默认处理方式,那么就会干掉整个进程。进程都被干掉了,那么内部的线程肯定也无一幸免。

线程等待

在创建进程的时候,我们的父进程要等待子进程。否则子进程在结束时资源将无法释放,成为僵尸进程,造成内存泄漏。而线程这里也是一样的道理,如果主线程不等待子线程。子线程结束后,主线程还在运行,那么一样会造成资源的泄漏。所以等待子线程是很有必要的。

线程等待函数:

#include 
int pthread_join(pthread_t thread, void **retval);
第一个参数是要等待线程的tid 
第二个参数是创建线程时传入的线程执行函数的返回值,在内核中会回调那个函数。而那个函数的返回值会返回给内核,再由内核输出到这个参数上。
返回值:返回0为成功,非0为错误码。如果线程已分离还进行join,那么会返回-1

我们用以下代码来测试一下这个函数。

#include
#include
#include 

void* ThreadRoutine(void* args)
{
  int id = *(int*)args;
  delete (int*)args;
  int count  = 0 ; 
  while(1)
  {
    printf("%d thread runing.... count = %d\n",id,count);
    sleep(1);
    if(count++ == 3) break; 
  }
  return (void*)id; //返回自己的id
}


int main()
{
  pthread_t tids[3]; 
  //创建三个线程
  for(int i = 0 ; i < 3; i  ++)
  {
    int* id = new int(i);
    pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);
  }
  
  //线程等待
  int* ids[3] = {0};
  pthread_join(tids[0],(void**)ids+0);    
  pthread_join(tids[1],(void**)ids+1);    
  pthread_join(tids[2],(void**)ids+2); 

  //打印三个线程的返回结果
  for(int i = 0 ; i < 3 ; i++)
    printf("thread %d : %ld\n",i,(long long)(ids[i]));

  return 0 ;
}

这段代码的逻辑就是 创建三个线程,每个线程执行三次循环,随后返回传入时的id。主线程等待三个线程,等待结束后打印三个线程的返回值。

运行结果:

【Linux】线程控制_第2张图片

线程退出

如果我们在线程内调用exit函数,那么整个进程都会退出。因为exit是进程退出函数,无论在哪调用,都会导致进程退出。进程退出,那么所有的线程也会被释放。

所以线程退出,我们要用指定的退出函数。

线程退出函数:

#include 
void pthread_exit(void *retval);
参数是传入一个当前线程的返回值。

那么我们用代码来演示一下程序退出。

#include
#include
#include 

void* ThreadRoutine(void* args)
{
  int id = *(int*)args;
  delete (int*)args;
  pthread_exit((void*)(id + 10));
  int count  = 0 ; 
  while(1)
  {
    printf("%d thread runing.... count = %d\n",id,count);
    sleep(1);
    if(count++ == 3) break; 
  }
  return (void*)id; //返回自己的id
}


int main()
{
  pthread_t tids[3]; 
  //创建三个线程
  for(int i = 0 ; i < 3; i  ++)
  {
    int* id = new int(i);
    pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);
  }
  
  //线程等待
  int* ids[3] = {0};
  pthread_join(tids[0],(void**)ids+0);    
  pthread_join(tids[1],(void**)ids+1);    
  pthread_join(tids[2],(void**)ids+2); 

  //打印三个线程的返回结果
  for(int i = 0 ; i < 3 ; i++)
    printf("thread %d : %ld\n",i,(long long)(ids[i]));

  return 0 ;
}

代码逻辑很简单,一进线程就退出,传入的值为 传入的id+10。

运行结果:

在这里插入图片描述

线程取消

线程退出函数必须要在要退出的线程内部执行。但如果我想在主线程中让指定的线程退出。那么我们可以用线程取消的函数。

线程取消函数:

#include 
int pthread_cancel(pthread_t thread);
传入的是要取消的线程tid。
返回值为0则成功,失败返回一个错误的非0数字。

我们用一段代码来验证这个函数。

#include
#include
#include 

void* ThreadRoutine(void* args)
{
  int count  = 0 ; 
  while(1)
  {
    printf("%s runing.... count = %d\n",(char*)args,count);
    sleep(1);
  }
  return nullptr; //返回自己的id
}

int main()
{
  pthread_t tid; 
  pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
  
  sleep(3); 
  pthread_cancel(tid); //取消线程 
  sleep(3);
    
  return 0 ;
}

这段代码逻辑很简单,创建一个线程死循环执行。主线程三秒之后取消这个线程。再过三秒之后主线程结束(整个进程结束)。 在这个过程中我们实时监视。

监视结果:

【Linux】线程控制_第3张图片

我们发现三面之前这个进程有2个线程,三秒之后就变成了一个线程。说明创建的线程被取消了,而三秒之后一个线程也没有了,说明主线程执行完毕,进程退出了。

线程id

我们可以用pthread_self()来返回线程id,并分别用16进制和10进制打印这个id。因为我提前知道了这个id会是个很大数(透剧怪登)。

#include 
pthread_t pthread_self(void);

那么我们写段代码来打印一下线程的id。

#include
#include
#include 

void* ThreadRoutine(void* args)
{
  int count  = 0 ; 
  pthread_t tid = pthread_self();
  while(1)
  {
    printf("%s runing.... count = %d ,thread id %ld , %lX \n",(char*)args,count,tid,tid);
    sleep(1);
  }
  return nullptr; //返回自己的id
}


int main()
{
  pthread_t tid; 
  pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
  pthread_join(tid,nullptr) ; 

  return 0 ;
}

这段代码的逻辑就是创建一个线程,然后通过pthread_self()函数获取当前线程的id,随后分别以10进制和16进制打印这个id。

运行结果:

在这里插入图片描述

当10进制打印进程id时,我们会发现它是一个很大的数字,并且看不出什么,但是当我们以16进制打印时。我们惊奇的发现,这是不是很像一个地址?

没错,线程id就是一个地址!

那么就有疑问了,CPU不是LWP进行调度吗?为什么线程id不是LWP,却是一个很大的数字?我们要搞清楚一点,pthread库是一个动态库!!而LWP是系统内核管理线程的标识。线程id是pthread库管理线程的标识。

在内核方面,用LWP来标识PCB进行调度,所以没有进程线程的区别。但是不要忘记了,线程是用pthread库创建出来的。那么创建出来要不要进行管理?这是必须的!!

pthread如何管理线程

首先我们都知道pthread是一个动态库,那么在程序执行的时候。会先把动态库加载进内存,随后根据页表把它映射到进程的共享空间内。

【Linux】线程控制_第4张图片

当pthread_create创建了一个线程时,那么就会在动态库中执行这个函数。随后就会创建一个struct pthread的结构体,线程局部存储以及线程栈。而这个结构体的地址(共享区中的虚拟地址)就是线程的id。这个结构体就是对这个线程的描述,存储着线程的信息,用来管理该线程。

【Linux】线程控制_第5张图片

虽然进程内部的线程都是共享进程地址空间的,那么就意味着进程地址空间中的栈(也叫主线程栈)可以被所有的线程访问。这是必然的,但访问归访问。线程本身的数据是不能存储在主线程栈中的。因为这样会导致存储十分混乱。也不能一个一个线程进行覆盖,这样会导致数据缺失。所以pthread动态库为每一个线程都提供了一个线程栈,这个线程栈是每个线程独有的(想跨线程访问也可以访问,但还是别这样做)。

线程局部存储是什么呢?

我们可以用__thread 来让一个全局变量变成线程私有的。

先看一段代码:

#include
#include
#include 

int val = 0;

void* ThreadRoutine(void* args)
{
  while(1)
  {
    printf("new thread , val = %d , &val = %p \n",val,&val);
    sleep(1);
  }
  return nullptr; //返回自己的id
}


int main()
{
  pthread_t tid; 
  pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
  while(1)
  {
    printf("main thread , val = %d, &val = %p\n",val,&val);
    val++;
    sleep(1);
  }

  pthread_join(tid,nullptr) ; 
  return 0 ;
}

这段代码的逻辑就是创建一个线程不断打印全局变量,主线程也不断打印全局变量,但是主线程会在每打印一次自增一次全局变量。

这段代码的结果是这样的:

【Linux】线程控制_第6张图片

没有意外,因为线程之间共享全局变量。所以打印的地址是一样的,值也是同步的。

那么我们把int val换成__thread int val再来试试效果。

#include
#include
#include 

int val = 0;

void* ThreadRoutine(void* args)
{
  while(1)
  {
    printf("new thread , val = %d , &val = %p \n",val,&val);
    sleep(1);
  }
  return nullptr; //返回自己的id
}


int main()
{
  pthread_t tid; 
  pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
  while(1)
  {
    printf("main thread , val = %d, &val = %p\n",val,&val);
    val++;
    sleep(1);
  }

  pthread_join(tid,nullptr) ; 
  return 0 ;
}

然后我们运行这段程序。

运行结果:

【Linux】线程控制_第7张图片

我们发现两个val的值不同步了,地址也不一样了。

原理很简单,本质就是在编译时,把val值的数据拷贝了一份。拷贝进了对应线程的线程局部存储中。

结论:

线程id本质是一个地址,这个地址在共享区中,与页表建立映射。最终映射到物理内存中对应的struct pthread结构体的地址起始处。在pthread_create创建了一个线程之后,pthread会在共享区创建一块内存。这个内存存储着线程的结构体,线程的局部存储以及线程栈。 而通过线程id可以找到线程结构体的起始位置。

线程分离

如果我们的主线程创建了线程之后却不想管它。那么我们可以用pthread_detach来让线程分离。 用pthread_detach来分离线程,那么被分离的线程在结束后就会自动销毁。如果不detach也不join,那么线程的一些资源就无法被释放,此时的线程就会陷入与僵尸进程相似的状态。

线程分离函数:

int pthread_detach(pthread_t thread);
参数是传入要分离线程的tid。
返回值:0为成功,非0则分离失败。

pthread_detach代码:

#include
#include
#include 

void* ThreadRoutine(void* args)
{
  pthread_detach(pthread_self());
  int count = 5;
  while(count--)
  {
    printf("new thread , count = %d\n",count);
    sleep(1);
  }
  return nullptr; //返回自己的id
}


int main()
{
  pthread_t tid; 
  pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
  while(1)
  {
    printf("main thread runing.... \n");
    sleep(1);
  }

  return 0 ;
}

你可能感兴趣的:(Linux之路,linux,运维,服务器)