天天看点

【Linux】多线程初识

一、线程概念

1、线程引入

假设有一个mp3程序,他的主要任务是从内存中读取文件,对文件进行解压,最后再将这个文件播放出来,因此他需要三个步骤,才能完成这一个任务,如果将其放在一个进程中,有可能造成在读以及解压过程比较慢,当这边的文件已经播放完成,但是那边还没有读取或者解压完成,将会造成CPU的长时间等待,或者 有可能造成文件播放杂乱,因此有的人就提出以下两种方法:

方法一:分别由三个进程,控制着这三个动作,让每一个进程控制着不同的工作,这个方法乍一看还不错,但是当我们仔细想来,还是存在一定的问题,例如,当读进程正在干工作之时,但是由于另一个进程的优先级较高,因此将其进程切换出去去执行另一个进程,从而使得数据出现二义性,其次,每个进程的切换,还会占用系统资源,从而延长时间,导致大量的时间在进程切换中被浪费,如下图所示:

【Linux】多线程初识

方法二:在一个进程中,创建多个线程,每个线程分配不同的工作,从而协同完成这个任务。

2、线程概念

  • 在一个程序⾥的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列(表示执行流)”
  • 一切进程至少都有一个执行线程
  • 通俗的讲,线程在进程内部运行—–>所有线程共享进程的地址空间(包括进程的代码与数据)

3、进程与线程的比较

  • 进程是资源竞争的基本单位(即,进程是承担系统分配资源的最小单位)
  • 线程是程序执行的最小单位(即调度的基本单位是线程)
  • 线程共享进程数据,但也拥有自己的一部分数据: >* 线程ID(LWP) >* 一组寄存器(保存上下文结构 ) >* 栈 (每个线程都有自己的私有栈)>* errno >* 信号屏蔽字 >*调度优先级
  • 总结上面的可以得出,Linux下的进程(PCB)称为轻量级进程(有可能是线程)
  • 线程也具有就绪,运行,等待三种基本状态以及状态之间的转换
  • 线程能减少并发执行的时间和空间开销

    (1)线程的创建时间比进程短

    (2)线程的终止时间比进程短

    (3)同一进程内的线程切换时间比进程短

    (4)由于线程之间的资源是共享,因此可不通过内核进行通信

3、线程的优缺点

优点

- 创建一个新线程的代价要比创建一个新进程小得多

- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

- 线程占用的资源要比进程少很多

- 能充分利用多处理器的可并行数量

- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

- I/O密集型应⽤用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点

  • 性能损失

    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同⼀个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低

    编写多线程需要更全⾯更深入的考虑,在一个多线程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很⼤大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制

    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高 编写与调试一个多线程程序比单线程程序困难得多

    二、线程控制

    目前,我们在给一个进程中创建多个线程,主要是利用用户级线程库,从而达到我们的请求。

    POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文件
#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;失败返回错误码
           

实例如下所示:

#include<stdio.h>
   #include<pthread.h>
   #include<unistd.h>
   #include<string.h>
   #include<stdlib.h>
   void* rount(void * arg)
   {
       while()
      {
          printf("i am new pthread!!!tid=%x\n",pthread_self());
          sleep();
      }
  }
  
  
  int main()
  {
      pthread_t tid;
      pthread_t ret;
      pthread_create(&tid,NULL,rount,NULL);
      while()
      {
          printf("i am main pthread!!!tid=%x\n",pthread_self());//获取线程的pid
          sleep();
      }
      printf("hello world\n");
      return ;
  }
           
【Linux】多线程初识

结果如下所示:

【Linux】多线程初识

有关线程ID

【Linux】多线程初识

在这里,我们看到了有关线程ID与进程ID,他们分别是操作系统中对进程与线程的描述,现在,让我们来了解一下这些。

  • 在Linux中,目前的线程实现是Native POSIX Thread

    Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted

    Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。

  • 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系(拥有多个执行流的进程,每个执行流叫做线程),POSIX标准⼜又要求进程内的所有线程调用getpid函数时返回相同的进程ID。

在线程中,也是利用其数据结构将进程中的每一个线程串接起来,数据结构如下所示:

线程组

struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;//线程组组长,即主线程
...
struct list_head thread_group;//其中list_head利用链表,将其每一个线程连接起来,从而形成一个数据链
...
};
           

对其做如下的解释

多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。

【Linux】多线程初识

有关线程ID以及地址空间的布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前⾯面说的线程ID不是一回事。
  • 前⾯讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_

    create函数产生并标记在⼀一个参数指向的地址中的线程ID中,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID

    有关pthread_ self函数

//获取线程的tid
#include <pthread.h>
 pthread_t pthread_self(void);
//只有成功返回值,没有失败返回值,成功返回线程id即可
           

(二)线程终止

1、线程终止的情况

如果进程中的任意线程调用了exit _Exit _exit 函数,那么整个线程就会退出。

单个线程退出的3种方式

a线程可以简单的从启动例程中返回,返回值是线程的退出码( 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。)

b线程可以被同一进程中的其他线程取消,利用系统接口pthread_cancel

c线程调用pthread_exit

2、系统调用接口

pthread_exit接口

//线程终止
 #include <pthread.h>
  void pthread_exit(void *retval);
//参数
//retval:retval不要指向一个局部变量。(可以利用函数的参数传递)
//返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
           

注:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel接口

//取消一个执行中的线程
#include <pthread.h>
 int pthread_cancel(pthread_t thread);
//thread:线程ID
//返回值:成功返回0;失败返回错误码
           
【Linux】多线程初识

(三)线程等待(阻塞式等待)

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
  • 线程退出,其资源必须被回收,否则会存在内存泄漏
#include <pthread.h>
 int pthread_join(pthread_t thread, void **retval);
//参数
//thread:线程ID
//value_ptr:它指向一个指针,后者指向线程的返回值,即获取线程的退出状态
//返回值:成功返回0;失败返回错误码
           

如果线程已经处于分离状态,pthread_join就会调用失败,因为对分离的线程调用pthread_join会产生未定义的行为,即已经分离的线程是不需要等待的。

1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。

2. 如果thread线程被别的线程调⽤用pthread_ cancel异常终掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED(此处为宏,值为-1)。

3. 如果thread线程是自己调用pthread_exit 终止的 ,retval所指向的单元存放的是传给pthread_exit的参数。

4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

实例如下所示,展示如何获取已经终止进程的退出码

#include<sys/types.h>
   #include<stdio.h>
   #include<pthread.h>
   #include<unistd.h>
   #include<string.h>
   #include<stdlib.h>
   void* rount(void * arg)
   {
       while()
      {
          printf("i am new pthread!!!tid=%x\n",pthread_self());
          sleep();
          //pthread_exit(arg);
      }
  }
  
  
  int main()
  {
      pthread_t tid;
      pthread_t ret;
      pthread_create(&tid,NULL,rount,NULL);
      pthread_cancel(tid);
      while()
      {
          printf("i am main pthread!!!tid=%x\n",pthread_self());
          sleep();
      }
      printf("hello world\n");
      return ;
  }
           

结果如下图所示:

【Linux】多线程初识

(四)线程分离

理解线程分离,好比就是在一个家庭中,兄弟两个人由于矛盾要进行分家,但是这种分家并不从家中搬出去,假如家中有两层楼,弟弟只是搬到二层上,实际上还是属于这个家中的,这就是线程分离。

线程分离只是处于资源层面上的分离,但还是属于这个线程中的。

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

    现在让我们来了解一下有关线程分离的系统调用接口

//分离指定线程
#include <pthread.h>
 int pthread_detach(pthread_t thread);
 //分离成功返回0,失败返回错误码
           
//分离自己
pthread_detach(pthread_self());
           
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
  • 一个线程的默认状态是可结合的
  • 如果被分离的线程出现异常,则此进程也就终止了(原因:即使线程分离了,但还是属于这个进程的,因此如果线程出现异常,则整个进程就退出)
  • 被分离的线程在执行完成自己的工作时,自动被清除

实例如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )
{
    //pthread_detach(pthread_self());//两种分离方式,方式一
    printf("%s\n", (char*)arg);
    return NULL;
}
int main( void )
{
    pthread_t tid;
    if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") !=  ) 
    {
        printf("create thread error\n");
        return ;
    }
    pthread_detach(tid);//方式二
    int ret = ;
    sleep();
    if ( pthread_join(tid, NULL ) ==  )
    {
        printf("pthread wait success\n");
    ret = ;
    } 
    else 
    {
        printf("pthread wait failed\n");
        ret = ;
    }
    return ret;
}
           

结果如下所示:

【Linux】多线程初识

有关线程的基础,大概就这么多的知识,一起共勉!!!

只有不停的奔跑,才能不停留在原地!!!

继续阅读