多线程概念与常用接口
多线程概念与相对于线程的区别
什么是多线程(概念)
多线程是指在同一个进程内同时执行多个线程,每个线程都是独立的执行流,都有自己的程序计数器、堆栈、寄存器和状态等信息,但共享同一个进程的地址空间和资源(在进程的共享区)。多线程可以提高程序的并发性和响应速度,使得程序能够更加有效地利用计算机的多核心和多任务处理能力。
补充:什么是高并发:
高并发性是指系统能够同时处理大量的并发请求,保持系统的高效性和可用性。 在计算机领域,高并发性通常指在单位时间内处理的请求量非常大,例如每秒处理几百万甚至几千万个请求。高并发性通常是对于网络服务、Web应用程序、数据库系统、操作系统等需要处理大量请求的系统而言的。
进程和线程的区别
进程是计算机中正在执行的程序实例,是资源分配的单位。当程序被启动时,操作系统为该程序分配一段内存空间,这段内存空间包含了程序的代码、数据和堆栈等信息,并为该程序创建一个进程实例。进程包括了程序的所有状态,例如程序计数器、寄存器、堆栈指针和打开的文件等。操作系统利用进程的信息来控制程序的执行和资源的分配,使多个程序可以同时在计算机上运行。
进程是计算机中最基本的执行单元,每个进程都拥有自己的内存空间、资源和执行上下文,这使得多个进程可以同时在计算机上运行,而不会相互干扰。进程之间可以通过进程间通信来进行数据交换和协作,
线程是进程中的执行单元,每个进程可以包含多个线程。 线程共享进程的内存空间和资源,每个线程拥有自己的执行上下文和栈。线程可以协作完成一个任务,也可以同时执行多个任务,这使得多线程程序可以充分利用计算机的多核处理能力,提高程序的性能和响应速度。
因此,进程和线程都是计算机中的执行单元,进程是操作系统中资源分配的最小单位,线程是进程中的执行单元,一个进程可以包含多个线程。两者的区别在于进程是操作系统中的基本单位,而线程是进程中的执行单元,进程之间相互独立,线程之间共享进程的资源和上下文。
在Linux系统下,进程和线程的区别如下:
1.资源占用:每个进程都会占用一定的系统资源,包括独立的地址空间、文件描述符、内存和系统调用等,而线程则共享进程的资源,包括地址空间、文件描述符、内存和系统调用等。
2.上下文切换:进程之间的切换需要进行完整的上下文切换,包括寄存器、内存和I/O等,而线程之间的切换只需要切换线程的上下文和栈,比进程切换更快。
3.调度:进程之间的调度由操作系统负责,每个进程有自己的调度策略和优先级,而线程之间的调度由进程内的线程库负责,根据线程的优先级和调度算法来调度线程。
4.通信:进程之间的通信需要使用进程间通信机制(IPC),例如管道、信号、共享内存和消息队列等,而线程之间的通信可以直接通过共享内存和进程内部的信号量、互斥锁和条件变量等进行通信。
5.安全性:进程之间的数据隔离更加彻底,可以提供更高的安全性,而线程之间共享同一个地址空间,可能会出现数据互相干扰的问题,需要进行同步和互斥等操作来保证数据的一致性和安全性。
因此,在Linux系统下,进程和线程都是执行单元,进程是资源分配的基本单位,线程是进程中的执行单元。 进程之间的隔离更加彻底,线程之间的切换更加快速,但需要考虑线程间的同步和互斥问题。
在Linux下,进程和线程都是通过pcb实现的,进程和线程都是以任务(task)的形式存在的,每个任务都对应着一个PCB,任务的切换实际上就是PCB的切换。由于线程共享同一个进程的地址空间和资源,因此Linux中线程的PCB与进程的PCB在结构上是有所区别的,线程的PCB通常称为线程描述符(Thread Descriptor)或轻量级进程(LWP,Light-weight Process)。Linux中线程的PCB与进程的PCB在结构上的区别在于,线程的PCB只记录了线程的部分状态,如堆栈指针、寄存器值、调度策略等,而没有单独的地址空间和文件描述符等资源,这些资源是共享给整个进程的。因此,线程切换的代价相对进程切换要小得多,这也是多线程应用程序的一个重要优势。
多进程和多线程优缺点比较:
多进程和多线程都是用来实现并发的技术,但它们各自具有一些优缺点,下面对它们进行比较:
多进程的优点:
稳定性(健壮性强):每个进程都是独立的,一个进程崩溃不会影响其他进程,因此系统的稳定性更高。
安全性:进程间的内存空间是独立的,不同进程之间的数据不会互相干扰,因此系统的安全性更高。
可扩展性:可以利用多台计算机的资源,将多个进程分配到不同的计算机上运行,从而实现更高的可扩展性。
所以在例如网络服务器,shell程序这种对于主程序要求高的情况,多使用多进程,其他情况尝尝使用多线程提高效率。
多进程的缺点:
资源消耗:每个进程都需要独立的地址空间、文件描述符、信号处理等资源,因此创建和维护多个进程的开销相对较大。
进程间通信:进程间通信需要额外的开销,而且往往需要使用复杂的通信机制来实现。
上下文切换:进程切换的开销相对较大,需要保存和恢复更多的状态信息,因此系统的性能可能受到影响。
多线程的优点:
资源节约:多个线程可以共享同一个进程的地址空间、文件描述符、信号处理等资源,因此创建和维护多个线程的开销相对较小
响应速度:线程切换的开销相对较小,可以更快地响应用户的请求,提高系统的响应速度。
简单易用:线程的创建,销毁和管理相对较简单,程序员可以更方便地利用多线程实现并发操作。
多线程的缺点:
稳定性:多个线程共享同一个地址空间,一个线程的错误可能会影响其他线程或整个进程,因此系统的稳定性可能会受到影响。
安全性:多个线程共享同一个内存空间,数据竞争和死锁等问题可能会出现,程序员需要使用同步机制来保证线程的安全性。
调试困难:多个线程同时运行,程序的执行流程可能会变得复杂和难以调试,特别是在多线程并发环境下,程序员需要花费更多的时间和精力来调试程序。
在多任务处理中,启动多少执行流比较合适
在多任务处理中,使用多执行流(多线程)可以更加充分的利用计算机资源,提高执行效率
但是执行流越多,cpu切换调度就越频繁,如果执行流太多,返回会造成切换调度消耗了大部分的资源。在任务处理中,程序一般分为俩种程序:
cpu密集型程序:一段程序中几乎都是数据的运算(对cpu的使用率非常高)
IO密集型程序:一段程序中大部分是IO操作(大部分时间都是在进行IO操作以及等待,因此对CPU使用率并不高)
因此,不同的程序对于CPU的要求是不一样的,因此多执行流没有什么固定的数量,最好是经过压力测试找到最合适的数量
线程控制
主要介绍的是线程的接口,其实都是大佬们封装的库函数,因为linux操作系统并没有直接向上层提供线程系统调用接口。
在linux下创建一个线程,其实就是创建一个pcb,多个pcb指向同一个虚拟空间,因此上层接口就要实现找到pcb并对其进行一些控制
线程的创建
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数thread是一个指向pthread_t类型变量的指针,用于存储新线程的ID(线程的操作句柄);
参数attr是一个指向pthread_attr_t类型变量的指针,用于指定新线程的属性,如果为NULL,则使用默认属性;
参数start_routine是一个指向函数的指针(函数指针),该函数是新线程的启动函数,即线程的入口函数,这个线程调度运行的就是这个函数,我们知道线程就是控制进程中的一部分,所以线程就是控制调度这一部分函数;
参数arg是传递给启动函数(rounite)的参数。
功能即:创建一个线程,指定这个线程运行的函数routine,并且给这个函数传入一个数据arg,成功返回0,不成功返回非0值。
操作演示:
#include<stdio.h> #include<unistd.h> #include<pthread.h> void *thread_entry(void* arg){ while (1) { printf("i am ordinary thread:%s\n",arg); } } int main() { pthread_t tid; void * arg = "hello"; int ret = pthread_create(&tid,NULL,thread_entry,arg); if(ret!=0){ printf("pthread error\n"); return -1; } while(1){ printf("i am main thread\n"); } return 0; }
普通线程一旦创建出来,创建出来的这个线程调度的是传入线程的入口函数,因此主进程也可以走下来,不等普通线程结束,并发进行。创建一个线程,其实就是让操作系统提供一个执行流。至于让线程做什么,取决于它的入口函数。
一个执行流在cpu运行的时间是一个时间片,谁先运行不一定,取决于操作系统的调度。主进程和子线程是并发执行的,它们的执行顺序是由系统调度器决定的。在多线程程序中,所有线程都可以并行运行,因此不能保证主进程先于所有子线程执行完毕。如果需要等待所有子线程执行完毕后再退出主进程,可以使用pthread_join()等待线程退出。
线程的终止
线程其实调度运行的是线程入口函数传入的入口函数,因此其实程序线程入口函数退出了,线程也就退出了。
在线程入口函数中return
注意:main中return退出的不仅仅是主线程,而是整个线程(退出了进程)
调用pthread_exit(void *retval):接口退出,retval是线程退出的返回值
上边两个是主动退出,在任意地方主动调用,主动调用,谁调用谁退出
调用void pthread_cancel(pthread_t thread)接口,该函数用于向指定线程发送取消请求,如果一个线程被取消,则不能调用其返回值进行操作(比如获取退出码进行线程等待,没有实际的处理意义)。假设我现在在主线程运行三秒后让普通线程退出,我就可以使用一个计时器,在主线程运行三秒后,调用接口让普通线程取消。
线程的等待
主线程退出,其实并不会影响其他线程的运行,一个线程被释放之后,它所占用的资源并不会立即释放。相反,线程的资源会在该线程结束后被释放。
当一个线程调用pthread_exit()函数或者线程函数执行完毕退出时,该线程会被标记为“已终止”,但是它所占用的资源并不会立即释放。线程的资源包括堆栈、寄存器、线程控制块等等。这些资源会被操作系统回收,但是具体的时间是不确定的,取决于操作系统的实现。通常,操作系统会在一段时间后回收线程资源,以确保资源能够被充分利用。
需要注意的是,如果一个线程没有被正确地销毁,即没有调用pthread_join()或pthread_detach()函数来回收线程资源,那么线程的资源将一直存在,直到进程结束。这会导致内存泄漏等问题,因此在编写多线程程序时需要特别注意线程的正确销毁和资源回收。
如果主线程调用pthread_exit()或者return语句退出,那么它所创建的其他线程将会继续运行,直到它们自己结束或被取消。但是如果主线程通过调用exit()函数、abort()函数或者收到信号等方式退出,那么整个进程将会被终止,所有线程都将被强制结束,即使它们没有执行完毕。这种情况下,其他线程的状态和数据都将丢失,可能会导致数据不一致或资源泄漏等问题。
等待:等待指定线程退出,获取退出的线程返回值,回收退出线程的所有资源
pthread_join(pthread_t thread, void **retval):该函数用于等待指定线程的结束,并获取线程的返回值。调用线程会一直阻塞,直到指定线程退出为止。如果不需要获取线程的返回值,返回的是一个地址,所以传入二级指针。可以将retval参数设置为NULL。thread是获取线程的tid。被等待的线程在退出时必须通过pthread_exit()函数返回一个值,否则无法获取到有效的返回值。成功返回0,失败返回一个错误编号。
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex):该函数用于等待条件变量的触发,并在等待期间释放互斥锁。在调用该函数之前,必须先获得互斥锁,否则会导致未定义的行为。当条件变量被触发时,该函数会重新获得互斥锁,并继续执行。
注意:线程的传递需要额外的注意数据的常量。(static,malloc创建的等),如果线程被取消了,将会返回一个宏。
线程的分离
线程的分离(Detached Thread)是指将一个线程设置为分离状态,使得该线程结束后,它所占用的资源会自动被系统回收,而无需其他线程调用pthread_join()函数来获取其退出状态。在线程属性中,有一个属性叫做分离属性,默认是joinable状态,表示线程退出后,不会被自动释放资源,需要被其他线程等待,但是我们有时候并不关心一个线程的返回值,也不想等待他的退出,则这个时候会将这个分离属性设置为detach状态,表示线程退出后,会自动释放所有资源,不需要被等待(等待会出错)
int pthread_detach(pthread_t tid);//设置指定线程的分离属性为detach