多线程(linux)

本文详细介绍了Linux系统中的多线程概念,包括竞态条件、volatile关键字的作用,以及多线程的创建、控制、优缺点。文章还探讨了线程与进程的区别,线程的独有和共享资源,并提供了线程创建、线程等待和线程分离的示例。

竞态条件

竞态条件是指多个执行流访问同一个资源的情况下,会对程序产生一个二义性的结果。

  • 重入: 多个执行流可以访问到同一个资源
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int count = 0;
void sigcallback(int signo)
{
 // count++;
  printf("i recv sig no is [%d],and count = [%d]\n",signo,count);
}

int main()
{
  signal(2,sigcallback);

  while(count < 20)
  {
    count++;
    printf("i am main thread : [%d]\n",count);
    sleep(1);
  }
}

在这里插入图片描述

  • 可重入: 多个执行流访问同一个资源,但是不会对程序的结果产生影响
  • 不可重入:多个执行流访问同一资源,会产生一个二义性的结果(与我们所想的结果不一样)

在这里插入图片描述

volatile关键字

volatile关键字的作用就是让变量保持内存可见性
程序中的数据的流向,一般是,cpu -> 寄存器 -> 缓存 -> 内存 -> 磁盘
在这里插入图片描述
这个程序,我们虽然改掉了全局变量 g_val 的值,但是主函数中的 g_val 其实还是1,因为主函数中没有进行调用,他g_val的值一直都是寄存器中的值,我们只是在回调函数中改掉了内存中的值,寄存器中的值还没有改变,所有程序才会一直陷入死循环中。
为了解决这一种情况,我们可以在类型前加上volatile关键字,从而用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,从而摒弃编译器优化选项。
编译器优化选项有 -O0, -O1,-O2,-O3,后面的总比前面的优化程度高。

makefile
threadexit:threadexit.c
	gcc -O2 $^ -o $@ -g

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

volatile int g_val = 1;

void sigcallback(int sigo)
{
  g_val = 0;
  printf("signal [%d],[%d]\n",sigo,g_val);
}

int main()
{
  signal(2,sigcallback);

  while(g_val)
  {
  }
  return 0;
}

多线程

为什么要加入多线程,多线程又有哪些好处呢?
首先如果我们在自己的程序中创建执行流,让这个执行流去干活,也就是让他一个人干全队的活。这样好像显得有点慢,那么要是我们可以在一个进程中有多个执行流的时候,这些执行流在运行的时候分别执行着不同的运算,也就是在同一时刻拿着不同的CPU进行运算(执行代码),相当于是我们熟悉的并行状态。
因为由于在同一时间一个进程中有多个执行流都在并行的执行代码,所以程序的执行效率就可以得到很大的提高。
在这里插入图片描述

从内核的角度分析线程

在进程的学习中,我们认为创建一个进程就是创建一个PCB,而对一个进程来说,每一个进程都有一个标识符,叫做PID;而线程就是程序创建出来的执行流,创建一个线程就是相当于在内核中创建了一个PCB,也就是一个task_struct结构体。创建出来的PCB当中的内存指针是指向进程的虚拟地址空间。

在linux内核中其实并没有线程这样的说法,线程是我们自定义的一个统一的说法。内核对于创建线程而言,他就是创建了一个轻量级进程–>LWP。
线程的说法是C库中规定的,那些写C库的大佬搞出来的,是由于操作线程的一些列接口都是库函数,他的底层是调用的clone接口,而不是操作系统定义的接口。

在task_struct结构体当中变量pid指的是线程id,对于进程号来说,他是在tgid中保存的。(线程id --> pid,进程 --> tgid)

  • tgid (thread group ID): 线程组id (进程号)
  • pid ( process ID ) :轻量级进程ID,线程ID

我们使用ps aux | grep [] 查看的线程的id就是tgid
在这里插入图片描述

  • 当前程序如果只有一个执行流的时候,意味着当前程序只有一个线程,我们就把执行main函数的线程称为主线程(老大)。tgid(线程组id – 进程id) == 主线程的id(pid)
  • 当前程序有多个执行流的时候,意味着当前程序有多个线程。主线程负责执行main函数部分 (pid == tgid),而工作线程:新创建出来的执行流。tgid:相当于进程号(线程组id)是一定不会变的,他标识着当前的线程属于哪一个进程;pid:相当于线程id,每一个线程id都是不相同的(相同怎么区分不同线程?)。 tgid != pid

线程优缺点

优点

  1. 创建一个线程的开销要比创建一个进程的开销小
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 创建一个线程所用的资源要比进程小
  4. 一个进程中的不同线程可以并行的运行,提高程序运行效率
  5. 在等待慢速 I/O 操作结束的同时,程序可以执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,可以将计算分解到多个线程中实现
  7. I/O 密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

缺点

  1. 健壮性低
    编写多线程需要更深入的考虑,在一个多线程的程序里,有时因为时间分配上的细微偏差,或者因为共享了不该共享的变量而造成不良影响的可能性是很大的,也就是缺乏保护的。一般多线程的程序中有多个执行流的时候,一旦一个执行流异常,会导致整个执行流异常。
  2. 缺乏访问控制
    进程是访问控制的基本单元,在一个线程中调用某些OS函数会对整个进程的结果造成影响,二义性。
  3. 性能损失
    一个很少被外部世界阻塞的计算密集型线程往往无法与其他线程共享一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加量额外的同步和调度开销,而可用的资源不变。也就是在频繁切换不同线程的时候,会占用CPU去执行上下文信息和程序计数器所保存的指令,每次切换都有着一个恢复的过程,造成浪费。
  4. 编程难度高
    多个执行流之间可以并发的执行,而在并发的时候可能会访问同一个临界资源,我们就需要对访问临界资源的顺序进行控制,防止产生程序二义性的结果。
    在这里插入图片描述

线程的万恶之源
前面了解到内核创建出来的线程,其实在内核当中也是一个task_struct结构体。对于进程调度而言,内核在调度线程的时候,其实是调度PCB的。这里由于线程是一个PCB,所以操作系统在调度的时候有可能并行的运行,也有可能会导致程序产生一个二义性的结果(操作系统调度的“抢占式执行”)

线程的独有和共享

每一个线程独有的数据

  • tid:线程ID
  • 栈(防止调用栈混乱,自己调用自己的栈)
  • 调度优先级(决定先调度那个线程)
  • 信号屏蔽字(每个线程屏蔽的信号)
  • errno(每个进程自己的错误信息)
  • 一组寄存器(保存CPU在之前执行时的数据结构)

线程共享的数据

  • 共享进程的虚拟地址空间
  • 文件描述符表
  • 当前进程的工作路径
  • 用户ID和用户组ID

在这里插入图片描述
为什么线程没有调用栈混乱的问题?
因为不同线程之间共享着主线程的虚拟地址空间,而在虚拟地址空间的共享区中,不同的线程占据着不同的位置,每一个位置都保存有这个线程自己独有的信息,保证了线程在执行的时候都是自己执行自己的东西,就避免了调用栈混乱的问题。

线程和进程的区别

线程是操作系统调度的最小单位。进程是操作系统分配资源的最小单位。

多线程和多进程的区别

多进程:每一个进程都有着自己独立的虚拟地址空间,这也是进程独立性的原因。这样一个进程的崩溃,并不会导致另外一个进程受到影响。多进程的程序也可以提高运行效率(其本质也是增加执行流),但是带来了进程间通信的问题。

多线程:每一个进程当中的执行流都是共用的一块虚拟地址空间,所以一个执行流的异常通常会导致整个程序的退出。多线程的程序也是可以提高运行效率的,但是会带来程序健壮性低,代码的编写复杂(并发带来的执行后果)

线程控制

前提:线程控制当中的接口都是库函数,使用线程控制的接口需要链接线程库,线程库的名称是pthread。链接的时候需要在后面加 -lpthread。

线程创建

pthread_create:会在内核当中创建一个PCB,这个PCB是指向父进程的虚拟地址空间,内核会给创建出来的线程在虚拟地址空间中的共享区内开辟一段空间,从而保存线程中独有的东西
在这里插入图片描述

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
  • thread:线程标识符,和线程id不同,pthread_t 是线程独有空间的首地址,通过这个标识符可以对当前的线程进行操作,调用pthread_create作为出参返回给调用者。
  • attr:线程属性,pthread_attr_t 是一个结构体,这个结构体完成对新创建线程属性的设置;如果这个参数是NULL,就采用默认的属性。他表示的内容有:线程栈的大小,线程栈的起始位置,线程的分离属性,线程优先级的调度属性…
  • thread_start:线程入口函数,是我们自定义的一个调用线程的函数,函数的返回值是void*,参数类型也是void*。
  • arg:给我们自定义的入口函数传的参数,参数是void*类型,以确保可以传递任意类型的参数,包含自定义结构体,类实例化指针,在堆上开辟的内存,但是不能传递临时变量,要是没有参数就传NULL。
    如果传递一个堆上开辟的内存到线程入口函数中当做参数使用,一定要注意在线程入口函数结束的时候就释放掉堆上开辟的内存空间,避免有内存泄漏的风险。不在pthread_create函数调用后释放申请的内存,这是因为在这里我们不知道这个线程到底执行到什么状态?到底有没有结束。。。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

typedef struct thread_info
{
  int thread_num_;
  thread_info()
  {
    thread_num_ = -1;
  }
}THREADINFO;

void* thread_start(void* arg)
{
 // int *i = (int*)arg;
  THREADINFO* ti = (THREADINFO*)arg;
  while(1) 
  {
    printf("i am new thread ~~~~ i = [%d]\n",ti->thread_num_);
    sleep(1);
  }
  //对于传递的堆上开辟的内存
  //可以在入口函数结束的最后进行释放,可以避免内存泄漏
  delete ti;
  return NULL;
}

int main()
{
  pthread_t tid;
  int ret;
 // 错误的传参方式
 // {
 //    int i = 10;
 //    ret = pthread_create(&tid,NULL,thread_start,(void*)&i);
 // }
  
  int i = 0;
  for(; i < 4; i++)
  {
    //给线程传递参数
    //从堆上开辟内存
    THREADINFO* threadinfo = new thread_info();
    threadinfo->thread_num_ = i;

    ret = pthread_create(&tid,NULL,thread_start,(void*)threadinfo);
    if(ret < 0)
    {
      printf("pthread_create error \n");
      return 0;
    }
  }
  
  while(1)
  {
    printf("i am main thread~~~\n");
    sleep(1);
  }
  return 0;
}

makefile文件
cthread:cthread.c
	g++ $^ -o $@ -g -lpthread 

在创建4个线程后,我们用pstack [主线程id],可以查看创建线程的情况
在这里插入图片描述
再次证明抢占式执行,我们可以发现,虽然我们创建线程是从0到3创建的,但是线程的执行顺序却不是0,1,2,3,而是抢占式的。
在这里插入图片描述

pthread_ttr_t结构体

对于pthread_attr_t结构体,我们一般传NULL,是因为操作系统对结构体内的函数进行了封装,我们是看不见的,为了安全,还是传一个NULL,至于具体的操作就让系统自己去做吧。
在《linux 编程技术详解》这本书中讲到线程属性pthread_attr_t的定义是:

typedef struct
{
	int detachstate;    //线程的分离状态
    int  schedpolicy;   //线程调度策略
    struct sched_param schedparam;   //线程的调度参数
    int inheritsched;   //线程的继承性
    int scope;          //线程的作用域
    size_t guardsize; 	//线程栈末尾的警戒缓冲区大小
    int stackaddr_set;
    void* tackaddr;     //线程栈的位置
    size_t stacksize;   //线程栈的大小
 }pthread_attr_t;

而在/usr/include/bits/pthreadtypes.h这个文件中找到pthread_attr_t的定义却是这样的:

typedef union
{
  char __size[__SIZEOF_PTHREAD_ATTR_T];
  long int __align;
} pthread_attr_t;

但是我们用sizeof查看这个结构体的大小时,发现他的大小时56,正好是这个union共用体的大小。__SIZEOF_PTHREAD_ATTR_T这个宏定义的数字就是56。
在这里插入图片描述

线程终止

线程终止的方式

  1. 从线程入口函数的return 返回在这里插入图片描述
  2. pthread_exit 函数来返回,那个线程调用那个线程退出。
#include <pthread.h>
void pthread_exit(void *retval);
  • retval --> 当前线程的退出信息,也可以是NULL值
    当主线程调用pthread_exit退出的时候,进程是不会退出的,但是我们调用后就会发现主线程的状态变成了Z,我们在进程中了解到Z就是僵尸进程的状态,这里可以通俗的理解为,主线程变成了“僵尸进程”,工作线程的状态还是R/S
top -H -p [pid]  	// -H 是查看线程执行  -p是线程pid

在这里插入图片描述
3. pthread_cancel(pthread_t thread);

  • thread 是线程标识符
    调用这个函数可以结束传入线程标识的线程–>可以结束任一的一个线程,前提是知道这个线程的标识符
//获取当前线程的线程标识符(共享区中创建线程的首地址) 
pthread_t pthread_self();

线程在默认创建的时候,默认属性当中认为线程是joinable的。

  • joinable:就是指当前线程在退出的时候,需要其他线程来回收该线程的资源,如果没有线程回收,则共享区当中对于该线程开辟的空间还是存在的。由于退出的线程没有得到及时的释放,就相当于造成了内存泄漏

线程等待

线程等待跟进程的等待作用相同,是为了回收对应线程的资源,防止线程退出的时候产生的内存泄漏的问题。

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//这是一个阻塞接口,如果当前线程没有退出,就会一直进入等待状态
  • thread:需要等待的那一个线程的线程标识符
  • retval:获取线程退出时的状态
    • return :获取入口函数return 返回的void*的内容
    • pthread_exit(void*):void*对应的线程退出信息,可以使用pthread_join的void**来获取
    • pthread_cancel(pthread_t thread):void**对应保存的一个常数,PTHREAD_CANCELED
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thread_start(void* arg)
{
  (void)arg;
  printf("i am new thread\n");
  sleep(5);
  
  //while(1);//此处是为了主线程退出情况而设置

  //printf("pthread_exit 鍑芥暟\n");
  //谁调用谁退出
  //pthread_exit(NULL);
 
  printf("pthread_concel 函数 id = [%p]\n",pthread_self());
  pthread_cancel(pthread_self());
  
  while(1)
  {
    printf("我退出了,打印到我就说明出错了!\n");
  }
  return NULL;
}

int main()
{
  pthread_t tid;
  pthread_create(&tid,NULL,thread_start,NULL);

  //主线程任务
  //看看主线程退出会是什么情况
  //sleep(5);
  //pthread_exit(NULL);
  
  pthread_join(tid,NULL);//主线程等待
  while(1)
  {
    printf("i am main thread\n");
    sleep(1);
  }
  return 0;
}

线程分离

线程分离就是设置线程的joinable属性为detach属性,从而使程序不用关心线程退出时由于joinable属性带来的内存泄漏的问题。这是因为线程的detach属性,他可以使退出的线程不用其他线程回收自己的资源,使得退出的线程资源可以被操作系统所回收。

#include <pthread.h>
int pthread_detach(pthread_t thread);
  • thread:线程标识符,这个线程标识符是指要分离的哪一个线程,从而设置这个线程的joinable属性为detach属性
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值