Linux篇第十三篇——多线程(线程概念+线程控制)

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux篇第十三篇——多线程(线程概念+线程控制)相关的知识,希望对你有一定的参考价值。

⭐️ 本篇博客开始要给大家介绍多线程相关的知识,多线程的内容比较多,所以我分三篇进行讲述,本篇博客主要讨论多线程的概念和多线程的控制,希望对你认识线程有所帮助。

目录


🌏Linux下的线程

🌲线程的概念

线程: 线程是OS能够进行运算调度的基本单位。线程是一个进程中的一个单一执行流,通俗地说,一个程序里的一个执行路线就叫做线程。

可以知道的是,一个进程至少有一个执行线程,这个线程就是主执行流。一个进程的多个执行流是共享进程地址空间内的资源,也就是说进程的资源被合理分配给了每一个执行流,这些样就形成了线程执行流。所以说线程在进程内部运行,本质是在进程地址空间内运行。
需要注意的是,Linux下没有真正意义上的线程,线程是通过进程来模拟实现的。这句话如何理解?

Linux系统下,没有专门为线程设计相关的数据结构。那线程又是如何被创建的呢?我们知道,创建一个进程,我们需要为它创建相关的数据结构,如:PCB(task_struct)、mm_sturct、页表和file_struct等。线程的创建和进程的创建是一样的,线程也是创建一个一个的PCB,因为线程是共享进程地址空间的,所以这些线程都维护同一个进程地址空间。

这样可以看出一个线程就是一个执行流,每一个线程有一个task_struct的结构体,和进程一样,这些task_struct都是由OS进行调度。可以看出在CPU看来,进程和线程是没有区别的,所以说Linux下的线程是通过进程模拟实现的。

继续思考,CPU如何区分Linux下的线程和进程?

其实CPU不需要考虑这个问题,在它眼中,进程和线程是没有区别的,都是一个一个的task_struct,CPU只管负责调度即可。

那如何理解我们之前所学的进程?

我们都知道,进程是承担分配系统资源的基本实体,曾经CPU看到的PCB是一个完整的进程,也就是只有一个执行流的进程。现在看到的PCB不一定是完整的进程,可能是一个进程的执行流总的一个分支,也就是多执行流进程。所以说,现在CPU眼中,看到的PCB比传统的进程更加轻量化了。这种有多执行流的进程中的每一个执行流都可以看作是一个轻量级进程。总结地说,线程是轻量级进程

🌲线程的优点和缺点

优点:

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

缺点:

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

🌲线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃进程之间是具有很强的独立性的,但是线程之前并不具有很强的独立性。一个线程发生异常(进程发生局部异常)时,OS会将异常解释为信号,以进程为单位发生信号终止进程,这样其他线程也随之受到影响,所以一个线程异常会影响整个进程
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

🌲线程用途

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

🌏Linux下的进程和线程

进程: 承担分配系统资源的实体
线程: CPU调度的基本单位
注意: 进程之间具有很强的独立性,但是线程之间是会互相影响的
线程共享一部分进程数据,也有自己独有的一部分数据:

  • 线程ID
  • 一组寄存器(记录上下文信息,任务状态段)
  • 栈(线程私有栈)
  • errno(错误码)
  • 信号屏蔽字
  • 调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的。如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

关系图:

🌏Linux线程控制

🌲POSIX线程库

  • POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。
  • 与线程有关的函数构成了一个完整的系列,绝大多数的名字都是以“pthread_”打头的
  • 使用线程库需要映入头文件pthread.h,链接这些线程函数是,需要指明线程库名,所以编译时要加上选项-lpthread

注意: Linux内核没有提供线程管理的库函数,这里的线程库是用户提供的线程管理功能

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做,不然这个全局变量就成为临界资源了)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

🌲线程创建

函数名称: pthread_create
功能: 创建一个线程
函数原型:

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

再介绍一个函数:

实例演示:
实例1: 创建一个线程,观察代码运行效果和函数用法

函数名称: pthread_self
功能: 获取线程自身ID
函数原型:

 pthread_t pthread_self(void);

返回值: 线程自身ID

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)

	char* name = (char*)arg;
	while (1)
		printf("%s is running...\\n", name);
		sleep(1);
	


int main()

	pthread_t pthread;
	// 创建新线程
	pthread_create(&pthread, NULL, pthreadrun, (void*)"new thread");
	
	while (1)
		printf("main thread is running...\\n");
		sleep(1);
	
	return 0;

代码运行结果如下:

实例2: 创建4个线程,然后打印出各自的pid和线程id

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)

  long id = (long)arg;
  while (1)
    printf("threaad %ld is running, pid is %d, thread id is %p\\n", id, getpid(), pthread_self());
    sleep(1);
  


int main()

  pthread_t pthread[5];
  int i = 0;
  for (; i < 5; ++i)
  
    // 创建新线程
    pthread_create(pthread+i, NULL, pthreadrun, (void*)i);
  

  while (1)
    printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
  
  return 0;

代码运行结果如下: 可以看到的是,线程的pid是一样的,但是线程id却是不一样的

代码运行时,我们使用命令ps -aL 查看轻量级进程:

可以看到,这六个线程的PID是一样的,同属一个进程,但是它们还有一个表示,LWP(light wighted process),轻量级进程的ID。下面详细介绍

🌲进程ID和线程ID

  • 在Linux下,线程是由Native POSIX Thread Library 实现的,在这种实现下,线程又被称为轻量级进程(LWP)。在用户态的每个进程,内核中都有一个与之对应的调度实体(拥有自己的task_struct结构体)。
  • 在没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。引入线程概念之后,一个用户进程下管理多个用户态线程,每个线程作为一个独立的调度实体,在内核中都有自己的进程描述符。进程和内核的描述符变成了1:N的关系。
  • 多线程的进程,又被称为线程组。线程组内的每一个线程在内核中都有一个进程描述符与之对应。进程描述符结构体表面上看是进程的pid,其实它对应的是线程ID;进程描述符中的tpid,含义是线程组ID,该值对应的是用户层面的进程ID。
struct task_struct 
	...
	pid_t pid;// 对应的是线程ID,就是我们看到的lwp
	pid_t tgid;// 线程组ID,该值对应的是用户层面的进程ID
	...
	struct task_struct *group_leader;
	...
	struct list_head thread_group;
	...
;
  • 具体关系如下:
用户态系统调用内核进程描述符中对应的结构
线程IDpid_t gettid(void)pid_t pid
进程IDpid_d getpid(void)pid_t tgid

注意: 这里的线程ID和创建线程得到的ID不是一回事,这里的线程ID是用来唯一标识线程的一个整形变量。

如何查看线程ID?
上面介绍过了,使用ps命令,带-L选项,可以查看到lwp,

Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来供程序员使用。如果确实需要获得线程ID,可以采用如下方法:

#include <sys/syscall.h> pid_t tid; tid = syscall(SYS_gettid);

在前面的一张图片中(如下),我们可以发现的是,有一个线程的ID和进程ID是一样的,这个线程就是主线程。在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。

注意: 线程和进程不一样,进程有父进程的概念,但是在线程组中,所有的线程都是对等关系。

🌲线程ID和进程地址空间布局

上面说过了,pthread_create产生的线程ID和gettid获得的id不是一回事。后缀属于进程调度范畴,用来标识轻量级进程。前者的线程id是一个地址,指向的是一个虚拟内存单元,这个地址就是线程的ID。属于线程库的范畴,线程库后序对线程操作使用的就是这个ID。
对于目前实现的NPTL而言,pthread_t的类型是线程ID,本质是进程地址空间的一个地址:

这里的每一个线程ID都代表的是每一个线程控制块的起始地址。这些线程控制块都是struct pthread类型的,所以所有的线程可以看成是一个大的数组,被描述组织起来。

🌲线程终止

如果值想终止某个线程而不是整个进程,有三种方式:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

return返回退出某个线程:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)

  int count = 0;
  while (1)
    printf(" new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
    if (count++ == 5)
      return (void*)10;
    
  


int main()

  pthread_t thread;
  pthread_create(&thread, NULL, pthreadrun, NULL);

  while (1)
    printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
  
  return 0;

代码运行结果如下: 代码运行6s后,新线程退出了

pthread_exit函数:

功能: 线程终止
函数原型:

 void pthread_exit(void *retval); 

参数:

  • retval:不能指向局部变量

实例演示:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)

  int count = 0;
  while (1)
    printf(" new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
    if (++count == 3)
      pthread_exit(NULL);
    
  


int main()

  pthread_t thread;
  pthread_create(&thread, NULL, pthreadrun, NULL);

  while (1)
    printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
  
  return 0;

代码运行结果如下:

注意: 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配

pthread_cancel函数:

功能: 取消一个线程
函数原型:

 int pthread_cancel(pthread_t thread);

参数:

  • thread:线程ID

返回值: 成功返回0,失败返回错误码

实例演示:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)

  int count = 0;
  while (1)
    printf(" new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
  


int main()

  pthread_t thread;
  pthread_create(&thread, NULL, pthreadrun, NULL);
  int count = 0;
  while (1)
    printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
    if (++count == 3)
      pthread_cancel(thread);
      printf("new thread is canceled...\\n");
    
  
  return 0;

代码运行结果如下:

🌲线程等待

线程等待的原因:

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
    pthread_join函数

功能: 等待一个线程结束
函数原型:

 int pthread_join(pthread_t thread, void **retval);

参数:

  • thread:线程ID
  • retval:输出型参数,指向线程退出的返回值

返回值: 成功返回0,失败返回错误码

实例演示:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

long retval = 10;

void* pthreadrun(void* arg)

  int count = 0;
  while (1)
    printf(" new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
    if (++count == 3)
      pthread_exit((void*)retval);
    
  


int main()

  pthread_t thread;
  pthread_create(&thread, NULL, pthreadrun, NULL);
  
  printf("main thread is waiting new thread\\n");
  void* ret = NULL;
  pthread_join(thread, &ret);
  printf("new thread has exited, exit code is %ld\\n", (long)ret);
  return 0;

代码运行结果如下:

总结:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED(-1)。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

🌲线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

pthread_detach函数:

功能: 对一个线程进行分离
函数原型:

 int pthread_detach(pthread_t thread);

参数:

  • thread:线程ID

返回值: 成功返回0,失败返回错误码

实例演示:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)

  int count = 0;
  pthread_detach(pthread_self());
  while (1)
    printf("new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
    sleep(1);
    if (++count == 3)
      pthread_exit(NULL);
    
  


int main()

  //pthread_t pthread[5];
  pthread_t thread;
  pthread_create(&thread, NULL, pthreadrun, NULL);
  sleep(1);// 让线程先分离
  if (pthread_join(thread, NULL) == 0)
    printf("wait success\\n");
  else
    printf("wait failed\\n");
  
  return 0;

代码运行结果如下:

🌐总结

以上就是线程控制的全部内容。喜欢的话,欢迎点赞、收藏和关注支持~

以上是关于Linux篇第十三篇——多线程(线程概念+线程控制)的主要内容,如果未能解决你的问题,请参考以下文章

Linux从青铜到王者第十三篇:Linux多线程四万字详解

Linux篇第十四篇——多线程(线程同步和互斥+线程安全+条件变量)

Linux篇第十四篇——多线程(线程同步和互斥+线程安全+条件变量)

Linux篇第十六篇——多线程(读写锁+线程池)

Linux篇第十六篇——多线程(读写锁+线程池)

Linux篇第十六篇——多线程(读写锁+线程池)