Linux:浅析线程(线程控制)

我们知道一个程序运行时,加载到内存成了进程。假如一个进程需要做一大堆事情,这个事情多的嘛就不谈了。那么进程可不可以有一些小弟?这些小弟可以替代进程做一些事情,这样可以提高进程的工作效率。那么这个时候线程就出来了。

进程在创建的时候需要创建资源、创建PCB。那么进程如果作为大哥的话,它也应该给它的小弟分配东西,只有这样小弟才能有东西去干活。
所以进程也就是承担分配资源的实体。而线程就是进程给线程分配的资源,让线程去执行。所以我们可以区别一下进程与线程,进程拥有资源,并且拥有分配资源的能力。而线程仅仅是用着进程所分配的资源给进程做事情。这样我们就能搞清楚线程是怎么来的了。

线程的概念

  • 在一个程序里面一个执行路线就叫做线程。线程是一个进程内部的控制序列。
  • 一个进程至少都有一个执行线程。

实际上我们可以理解线程就是一个PCB,它与其他线程共用一块虚拟地址空间。
所以我们之前所说的进程,实际上就是拥有一个线程的进程。

进程与线程

  • 由于进程拥有资源,所以进程时资源竞争的基本单位
  • 线程是程序执行的最小单位,是调度的基本单位
  • 线程共享进程的数据,但是线程也拥有自己的数据,比如:线程的上下文数据,线程的私有栈
  • 线程在进程的程序地址空间内部运行

线程的优缺点

相比进程线程的优点有:

  • 创建线程比创建进程开销小的多
  • 线程之间切换的效率也高于进程
  • 线程占用的资源进程少很多
  • 能够充分的利用多处理器的可并行数量
  • 线程的执行粒度更细

线程的缺点有:

  • 线程之间由于资源是共享的,所以在处理某些临界资源时,可能会造成难以估计的不良影响,所以线程之间是缺乏保护的
  • 在某些情况下,由于多线程中额外的调度与开销很大,反而造成了性能的损失
  • 编码难度高,调试多线程程序比调试单线程程序困难很多

线程控制

POSIX线程库

#include <pthread.h>

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,失败返回错误码

线程的所有调用函数都不是系统调用,并且在链接是需要手动链接线程库-lphread

传统的函数成功返回0,失败返回-1,而且对全局变量errno赋值来指示错误,而phreads函数出错的时候不会设置全局变量errno(而大部分POSIX函数会这样做),而是直接将错误代码通过返回值返回,phreads同样提供了线程内的errno变量,来支持其他使用errno的代码。但是在检查错误时,建议通过返回值来判定,因为读取返回值要比读取线程内errno变量的开销更小。

#include <pthread.h>

int pthread_join(pthread_t thread, void** value_ptr);
//功能:等待线程结束
//参数:
//thread:线程ID
//value_ptr:输出型参数,指向一个指针,这个指针指向线程的返回值
//返回值:成功返回0,失败返回错误码
#include <pthread.h>

void pthread_exit(void* value_ptr);
//功能:线程终止
//参数:
//value_ptr:value_ptr不要指向一个局部变量
//返回值:无返回值
#include <pthread.h>

int pthread_cancel(pthread_t thread)
//功能:取消一个执行的线程
//参数:
//thread:线程ID
//返回值:成功返回0,失败返回错误码
#include <pthread.h>

pthread_t pthread_self(void);
//功能:获取线程自身ID

线程ID

在Linux下,线程又被称作轻量级进程,每一个线程在内核中都有一个调度的实体,也同样拥有自己的进程描述符(PCB),这时候我们发现在进程中,有多线程,而每个线程都有一个自己的进描述符,那么内核在调度进程的时候要求所有线程都是对应同一个进程PID,这是怎么做到的呢?
在Linux下引入了线程组,多线程的进程被称作线程组。 线程组内每一个线程都拥有自己的进程描述符,在自己的进程描述符内部的PID,其实是这个线程的线程ID,而其内部有一个TGID,含义为Thread Group ID这才是真正我们所看到的PID。也就是说其实我们看到的PID实际上是线程组ID。而所有线程组内部的线程,它们的PID是不同的,但是它们的TGID肯定相同。我们把它们共同的这个TGID称作进程PID。
我们可以用gettid()来获取线程的线程ID。由于glibc并没有将gettid这个系统调用封装,所以我们要获得线程ID,需要下面的方法:

#include <sys/syscall.h>

pid_t tid;
tid = syscall(SYS_gettid);

接下来我们来查看一下我们的线程ID

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

void *thread(void* arg)
{
  char* msg = (char*)arg;
  pid_t tid = syscall(SYS_gettid);
  printf("%s tid:%d \n",msg, tid);

  return NULL;
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, thread, (void*)"thread1");

  printf("main pid %d main tid %lu \n",getpid(),syscall(SYS_gettid));
  sleep(2);
  pthread_join(tid, NULL);

  return 0;
}

这里写图片描述
我们发现,main函数也就是主线程的TID与我们getpid系统调用所得到的结果一样,这说明了在线程组中,线程组的ID取决于主线程的线程ID,而我们所说的进程PID其实就是主线程的线程ID。
而我们刚才提到的线程操作函数里面有一个pthread_self(void)这个函数也是获取线程自身ID的,那这个获取的是什么内容呢?我们来看看。

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

void *thread(void* arg)
{
  char* msg = (char*)arg;
  printf("%s pthread_self id %p  %lu\n",msg,(void*)pthread_self(),pthread_self());

  return NULL;
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, thread, (void*)"thread1");
  printf("main tid %p   %lu\n",(void*)tid, tid);


  sleep(1);
  pthread_join(tid, NULL);

  return 0;
}

这里写图片描述
我们发现,其实pthread_self()函数获得的其实是我们在创建线程时所传入的tid,相当于一个标识符,这个表示符能够让线程库更好的对线程进行操作等。而并不是我们想要的线程ID。
我们pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,这个线程ID并不是我们理解的线程ID,这个线程ID是线程库为了方便操作这些线程而使用的ID。而我们说的线程ID是进程调度范畴的,线程是轻量级进程,是调度器调度的基本单位,所以需要一个唯一的ID与之对应,这样可以更好的去调度。

线程终止

在进程方面,我们终止进程可以有很多办法,比如return,exit等。
而终止某个线程而不终止整个进程可以有三种方法:

  1. 从线程函数return,这种方法不适用于主线程,从main函数return相当于调用exit,终止整个进程
  2. 调用pthread_exit()终止自己
  3. 一个线程可以调用pthread_cancel(pthread_t thread)来终止某个线程
#include <stdio.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>

void *thread(void* arg)
{
  char* msg = (char*)arg;
  while(1) {
    printf("%s is running!\n",msg);
    sleep(1);
  }

  return NULL;
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, thread, (void*)"thread1");

  sleep(5);
  pthread_cancel(tid);

  return 0;
}

这里写图片描述

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

void *thread(void* arg)
{
  char* msg = (char*)arg;
  printf("%s is running!\n",msg);
  pthread_exit(0);
  sleep(5);

  return NULL;
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, thread, (void*)"thread1");

  pthread_join(tid, NULL);

  return 0;
}

这里写图片描述

线程等待

在学习进程时,子进程退出父进程需要等待。这是因为父进程要获取子进程退出的信息,并且维护子进程退出后的数据等。如果不进行等待,子进程会成为僵尸进程,引起内存泄漏的风险。
线程也是一样,在线程退出后,线程的空间没有被释放,仍然在进程的地址空间内部。这样就会导致内存泄漏的问题出现。
我们在等待线程的时候调用pthread_join(pthread_t thread, void** value_ptr)调用该函数的线程将被挂起等待,直至thread线程终止。其中第二个参数是存放等待线程的退出信息,属于一个输出型参数。
由于thread退出的方式不同,在join时得到的退出信息也是不同的:

  • 如果thread通过return退出,此时value_ptr所指向的内容里存放的是thread线程函数的返回值
  • 如果thread通过自己调用pthread_exit()退出,value_ptr所指向的内容存放的是传给pthread_exit()里的参数
  • 如果thread被别的线程调用pthread_cancel()而终止,此时value_ptr所指向的内容存放的是常数PTHREAD_CANCELED
  • 如果对thread的退出信息不感兴趣,可以设置value_ptr为NULL

线程结合与分离

在默认的情况下,我们创建的线程都是结合的。线程退出以后需要对线程进行pthread_join操作,否则线程资源无法被释放,造成内存泄漏。如果不关心线程返回值的话,这是pthread_join就没有必要在等待了,可以利用int pthread_detach(pthread_t thread)来将这个线程分离,这样在线程退出的时候自动就会释放线程的资源。

int pthread_detach(pthread_t pthread)//分离线程

在调用时,可以自己将自己分离,也可以分离别的线程。


欢迎大家共同讨论,如有错误及时联系作者指出,并改正。谢谢大家!

猜你喜欢

转载自blog.csdn.net/liuchenxia8/article/details/79999428
今日推荐