线程与子线程

时间:2022-08-18 01:10:00

目录

一、简介

线程        

主线程与子线程

线程与进程

并发和并行

二、代码编写

1、创建线程之每个线程有独立的线程函数

验证

2、创建线程 每个线程共用的同一线程函数

验证

3、回收线程资源(阻塞)

验证

 4、分离线程(不阻塞)

验证

5、线程的取消和退出

验证

 6、线程的取消

验证​编辑

三、线程的属性

1、设置线程分离、修改线程的栈的地址和大小

验证

 2、创建多线程

验证


 

一、简介

线程        

        线程是参与系统调度的最小单位。 它被包含在进程之中, 是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流), 一个进程中可以创建多个线程, 多个线程实现并发运行, 每个线程执行不同的任务。 譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将两个不同的任务分别放置在两个线程中

        当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器, 它包含了线程运行所需的数据结构、环境变量等信息。同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context) 、 自己的线程本地存储(thread-local storage)。线程不单独存在、而是包含在进程中,是参与系统调度的基本单位,是可并发执行,同一进程中的各个线程,可以共享该进程所拥有的资源。

线程与子线程

主线程与子线程

        当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以 main()做为入口开始运行的,所以 main()函数就是主线程的入口函数, main()函数所执行的任务就是主线程需要执行的任务

        所以由此可知,任何一个进程都包含一个主线程, 只有主线程的进程称为单线程进程,譬如前面章节内容中所编写的所有应用程序都是单线程程序,它们只有主线程;既然有单线程进程,那自然就存在多线程进程,所谓多线程指的是除了主线程以外, 还包含其它的线程,其它线程通常由主线程来创建( 调用pthread_create 创建一个新的线程,那么创建的新线程就是主线程的子线程
        其它新的线程(也就是子线程)是由主线程创建的;
        主线程通常会在最后结束运行, 执行各种清理工作,譬如回收各个子线程

线程与进程

        进程创建多个子进程可以实现并发处理多任务(本质上便是多个单线程进程),多线程同样也可以实现(一个多线程进程) 并发处理多任务的需求,怎么选择需要分清优劣势

        多进程编程的劣势:多个进程同时运行(指宏观上同时运行),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算;进程间通信较为麻烦。 每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦
        多线程能够弥补上面的问题:同一进程的多个线程间切换开销比较小; 同一进程的多个线程间通信容易。 它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易;线程创建的速度远大于进程创建的速度; 多线程在多核处理器上更有优势

        多线程编程相比于多进程编程的优势是比较明显的,在实际的应用当中多线程远比多进程应
用更为广泛。那既然如此,为何还存在多进程编程模型呢?当然不是,多线程也有它的缺点、劣势, 譬如多线程编程难度高,对程序员的编程功底要求比较高,因为在多线程环境下需要考虑很多的问题, 例如线程安全问题、信号处理的问题等, 编写与调试一个多线程程序比单线程程序困
难得多等等缺点,多进程编程通常会用在一些大型应用程序项目中,譬如网络服务器应用程序,在中小型应用程序中用的比较少

并发和并行

        串行:指的是一种顺序执行,譬如先完成 task1,接着做 task2、直到完成 task2,然后做 task3、直到完成 task3……依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行
 

线程与子线程
串行运行

         并行与串行则截然不同,并行指的是可以并排/并列执行多个任务, 这样的系统,它通常有多个执行单元, 所以可以实现并行运行,譬如并行运行 task1、 task2、 task3

线程与子线程
并行运行

         并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着

线程与子线程
并行运行2

         相比于串行和并行,并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。 在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行) ,这就是并发运行

线程与子线程
并发运行

 网络上看到形象生动的比喻,用来说明串行、并行以及并发这三个概念的区别

⚫ 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并行,仅仅只是串行。【一件事、一件事接着做】
⚫ 你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发。

        【交替做不同的事】
⚫ 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行.【同时做不同的事】

二、代码编写

1、创建线程之每个线程有独立的线程函数

线程与子线程

         用pthread_create函数创建两个进程,线程标识符地址分别为tid1和tid2,线程属性结构体地址为NULL(通常设置为 NULL),线程函数的入口地址分别为pthread_fun1和fun2,这俩个函数功能都是打印i++,一个输出数字,一个输出ascll码,最后是传给线程函数的参数,最后按下回车键之后就会退出程序。

        当调用pthread_create()函数后,该函数会立即返回并使线程在后台运行。因此,在主线程中调用pthread_create()函数并传递参数后,主线程会继续往下执行(在这里是调用getchar()函数等待用户输入),而新创建的线程会在后台运行。如果不调用getchar()函数或者程序没有遇到其他阻塞语句,那么程序将一直运行直到被手动停止。因此,尽管新创建的线程进入了死循环,但是主线程仍然会继续执行下去。

验证

线程与子线程

        此时会不断循环往下增加,按下回车键就会停止 

2、创建线程 每个线程共用的同一线程函数

线程与子线程

        代码和上面几乎一样,就共用一个线程函数 

验证

线程与子线程

         每个线程都有自己的计数器。在这段代码中,每个线程都有一个独立的局部变量i,并且它们各自独立地递增它们自己的计数器。因此,当第一个线程打印出i=1时,它只会影响该线程的局部变量i,而不会影响另一个线程的局部变量i。当第二个线程开始运行时,它的局部变量i将从0开始,而不会从1开始。

3、回收线程资源(阻塞)

线程与子线程

         pthread_join函数,等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。

验证

线程与子线程

 4、分离线程(不阻塞)

        将线程的回收工作 分离出去 线程结束时,线程由系统回收资源。使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞

线程与子线程          线程分离之后的代码,是主线程和线程2运行的,它们在同一个进程中运行的。因此,在这个程序中,主线程和线程2可以访问相同的变量和资源,并且可以互相通信。由于它们运行的代码是相同的,所以它们打印输出的内容也是相同的,不会像父子进程那样分别打印。

验证

线程与子线程

         在这段代码中,子线程和主线程/线程2是同时运行的,输出的结果是不确定的。可能会出现主线程/线程2先于子线程执行,也可以出现子线程先于主线程/线程2执行。因此,在测试结果中,可能是子线程在主线程/线程2打印"A"之前就已经完成了一些循环迭代,导致输出的数字跳过一些值,比如从0直接到2或者从2直接到4。

在多线程编程中,由于线程在执行时是并发的,它们的执行顺序是不确定的。因此,在编写多线程程序时需要考虑到线程间通信、竞态条件等问题,以避免出现不可预测的错误。

5、线程的取消和退出

        退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放

线程与子线程

         创建了一个子线程,并在其中不断循环打印输出数据。当计数器i等于5时,子线程通过调用pthread_exit函数正常退出。在主函数中,使用pthread_join函数回收子线程资源并等待子线程完成,以保证子线程的运行完全结束后再结束主线程。最后,主函数打印输出一条“线程结束”的消息

        通常情况下,在一个线程正常退出时,它所占用的资源(例如栈、寄存器等)并不会立即释放,而是被保留在进程中。这些资源会一直存在,直到进程本身结束或者其他线程调用相关的系统函数来释放这些资源。

        在这段代码中,子线程占用了一定的资源空间,但是在子线程正常退出后,并没有调用任何系统函数来释放这些资源。因此,在主线程中等待子线程完成后,这些资源仍然会一直保留在进程中,直到进程结束。 需要注意的是,虽然线程退出时,其资源可能并不会立即释放,但是操作系统会负责管理这些资源,以确保它们不会对系统造成过多的影响。

验证

线程与子线程

 6、线程的取消

        杀死(取消)线程,取消自己、也可以取消当前进程的其他线程。杀死线程也不是立刻就能完成,必须要到达取消点。 取消点是线程检查是否被取消,并按请求进行动作的一个位置

        在多线程编程中,如果一个线程正在执行某些阻塞操作(例如I/O操作、等待锁、睡眠等),则该线程可能无法响应取消请求。为了避免这种情况发生,我们可以将一些阻塞操作称作“取消点”,并在这些取消点上设置取消状态。

线程与子线程

         在这段代码中取消点是“sleep(1)”,子线程通过调用sleep函数进行睡眠,每隔1秒钟输出一次数据。由于sleep函数是一个可取消的阻塞操作,因此在子线程睡眠时,如果主线程调用pthread_cancel函数来取消子线程,则可以在下一个取消点上将子线程终止。如果没有设置取消点,那么即使主线程调用了pthread_cancel函数,子线程也可能继续运行,直到睡眠时间结束后才会退出。需要注意的是,虽然sleep函数是一个取消点,但并不是所有的阻塞操作都是可取消的。在实际编程中,我们需要根据具体的场景选择合适的阻塞操作,并在必要的地方增加取消点和取消状态的处理。

验证
线程与子线程

         当主线程睡眠5秒钟时间结束后,会调用pthread_cancel函数来向子线程发送取消请求。此时子线程正在执行sleep(1)操作,对应的取消点是下一次调用sleep函数之前。因此,当子线程完成第四次循环迭代并打印出i=3的数据之后,它进入了下一次的睡眠状态,在那一瞬间被主线程发送的取消请求所终止。因此,最终i的值为4,即子线程正常输出了4次数据

三、线程的属性

        pthread_attr_t结构体是用于设置线程属性的数据类型,通常使用它来定义和初始化线程的各种性。通过设置不同的线程属性,我们可以控制线程的优先级、栈大小、线程调度策略等参数,以满足程序对线程执行的不同需求。

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

利用pthread_attr_t数据类型,我们可以通过以下函数对线程的属性进行设置:

  • pthread_attr_init:对线程属性进行初始化,以便后续的设置操作。
  • pthread_attr_setdetachstate:设置线程的分离状态,以决定线程是否需要被回收资源。
  • pthread_attr_setschedpolicy:设置线程的调度策略,包括SCHED_OTHER、SCHED_FIFO、SCHED_RR等多种选项。
  • pthread_attr_setschedparam:设置线程的调度参数,包括线程的优先级和时间片大小等
  • pthread_attr_setstacksize:设置线程的栈大小,以确保线程能够正常运行

 创建线程的时候就通过线程属性 设置线程分离,就定义保存,先分离,后执行线程

1、设置线程分离、修改线程的栈的地址和大小

线程与子线程

         调用getchar函数后会阻塞主线程,子线程仍然在执行并打印输出。当用户输入一个字符并按下Enter键后,getchar函数返回该字符并解除主线程的阻塞状态,程序继续往下执行,并执行return 0语句退出主函数。

验证

线程与子线程

 2、创建多线程

体验一下

线程与子线程

         读取用户输入的任务名和运行时间,然后通过创建新线程来运行fun()函数,并传递用户输入作为参数。在每次循环中,该程序会请求用户输入新增的任务名称和运行时间,并将其存储在MSG结构体中。之后,它会创建一个新的线程tid,调用pthread_create()函数来启动线程,并将MSG结构体指针传递给线程。

        在fun()函数中,将MSG结构体解引用成MSG类型的变量,并进行相应的处理。fun()函数包含一个循环,打印任务名称和剩余时间,并每秒钟休眠一次

验证

线程与子线程

         因为这只是体验,代码没优化的,在输入第一个任务名和执行时间之后,就会不断打印,此时显示出来的信息会看起来不好友好,但是依旧可以继续输入任务2、3....,及其时间