Linux——多线程

Linux——多线程

线程概述

线程是程序中完成一个独立任务的完整执行序列,也就是一个可调度的实体
根据运行环境和调度这的身份,线程可分为内核线程和用户线程
内核线程,在有的系统上也称为轻量级进程,运行在内核空间,由内核来调度
用户线程运行在用户空间,由线程库来调度
当进程的一个内核线程获得CPU的使用权时,他就加载并运行一个用户线程。所以,内核线程相当于用户线程运行的“容器”。一个进程可以拥有M个内核线程和N个用户线程,其中M<=N。并且在一个系统的所有进程中,M和N的比值都是固定的。按照 M:N 的取值,现成的实现方式可分为三种模式:完全在用户空间实现、完全由内核调度和双层调度。
完全在用户空间实现的线程不需要内核的支持,内核甚至根本不知道这些线程的存在。线程库负责管理所有执行线程,比如线程的优先级、时间片等。线程库利用longjmp来切换线程的执行,使它们看起来像是“并发”执行的。但实际上内核仍然是把整个进程作为最小单位来调度的。换句话说,一个进程的所有执行线程共享这个进程的时间片,它们对外表现出相同的优先级。因此,对这种实现方式而言,N=1,即M个用户空间线程对应1个内核线程,而该内核线程实际上就是进程本身。完全在用户空间实现的线程的优点是:创建和调度线程都无须内核的干预,因此速度相当快。并且由于它不占用额外的内核资源,所以即使一个进程创建了很多线程,也不会对系统性能造成明显的影响。缺点是:对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上,因为内核是按照其最小调度单位来分配CPU的。此外,线程的优先级只对同一个进程中的线程有效,比较不同进程中的线程的优先级没有意义。
完全由内核调度的模式将创建、调度线程的任务都交给了内核,运行在用户空间的线程库无须执行管理任务,这与完全在用户空间实现的线程恰恰相反。二者的优缺点也正好互换。较早的Linux内核对内核线程的控制能力有限,线程库通常还要提供额外的控制能力,尤其是线程同步机制,不过现代Linux内核已经大大增强了对线程的支持。完全由内核调度的这种线程实现方式满足M:N=1:1,即1个用户空间线程被映射为1个内核线程。
双层调度模式是前两种实现模式的混合体:内核调度M个内核线程,线程库调度N个用户线程。这种线程实现方式结合了前两种方式的优点:不但不会消耗过多的内核资源,而且线程切换速度也较快,同时它可以充分利用多处理器的优势

Linux线程库

Linux内核从2.6版本开始,提供了真正的内核线程。使用NPTL线程库,它的主要优势在于:

1、内核线程不再是一个进程,因此避免了很多用进程模拟内核线程导致的语义问题
2、摒弃了管理线程,终止线程、回收线程堆栈等工作都可以由内核来完成
3、由于不存在管理线程,所以一个进程的线程可以运行在不同的CPU上,从而充分利用了多处理器系统的优势
4、线程的同步由内核来完成。隶属于不同进程的线程之间也能共享互斥锁,因此可以实现跨进程的线程同步

线程的创建和结束

1、创建一个线程:pthread_create

#include<pthread.h>
int pthread_create(pthread_t* thread,				//新线程的标识符,即线程ID
					const pthread_attr_t* attr,		//用于设置线程属性
					void* (*start_routinue)(void*),	//线程入口函数
					void* arg);						//传递给线程入口函数的参数

attr参数用于设置新线程的属性,通常给它传递NULL表示使用默认线程属性。
返回值:pthread_create函数成功时返回0,失败时返回错误码。
一个用户打开的线程数量不能超过RLIMIT_NPROC软资源限制。
系统上所有用户能创建的线程总数也不能超过/proc/sys/kernel/threads-max内核参数所定义的值

2、退出线程pthread_exit

线程一旦被创建好,内核就可以调度内核线程来执行 start_routine 函数指针所指向的函数了。线程函数在结束时最好调用 pthread_exit 函数,以确保安全、干净地退出:

#include<pthread.h>
void pthread_exit(void* retval);

pthread_exit 函数通过retval参数向线程的回收者传递它的退出信息。它执行完后不会返回到调用者,而且永远不会失败。

3、回收线程pthread_join

一个进程中的所有线程都可以调用pthread_join函数来回收其他线程(前提是目标线程是可回收的,见后文),即等待其他线程结束,这类似于回收进程的wait和 waitpid系统调用。
pthread_join的定义:

#include<pthread.h>
int pthread_join(pthread_t thread,	//表示等待那个线程退出
				void** retval		//是一个void*空间的地址,用于接收线程返回值
				);

这个函数会一直阻塞,直到被回收的线程结束为止。
返回值:成功时返回0,失败时返回错误码。
在这里插入图片描述

4、取消线程pthread_cancel

有时候,我们希望异常终止一个线程,即取消线程:

#include<pthread.h>
int pthread_cancel(pthread_t thread);

线程属性

pthread_attr_t 结构体定义了一套完整的线程属性:

#include<bits/pathreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
	char __size[__SIZEOF_PTHREAD_ATTR_T];
	long int __align;
} pthread_attr_t;

各种线程属性全部包含在一个字符数组中。线程库定义了一系列函数来操作pthread_attr_t类型的变量,以方便我们来获取和设置线程属性:

#include<pthread.h>

/*初始化线程属性对象 */
int pthread_attr_init(pthread_attr_t* attr);

/*销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用 */
int pthread_attr_destroy(pthread_attr_t* attr);

/*下面这些函数用于获取和设置线程属性对象的某个属性 */
int pthread_attr_getdetachstate ( const pthread_attr_t* attr,int* detachstate );

int pthread_attr_setdetachstate ( pthread_attr_t* attr, int detachstate );

int pthread_attr_getstackaddr (const pthread_attr_t* attr,void ** stackaddr );

int pthread_attr_setstackaddr ( pthread_attr_t* attr,void* stackaddr );

int pthread_attr_getstacksize ( const pthread_attr_t* attr,size_t*stacksize );

int pthread_attr_setstacksize ( pthread_attr_t* attr. size_t stacksize);

int pthread_attr_getstack ( const pthread_attr_t* attr,void** stackaddr,size_t* stacksize) ;

int pthread_attr_setstack ( pthread_attr_t* attr,void* stackaddr,size_t stacksize ) ;

int pthread_attr_getguardsize ( const pthread_attr_t *_attr,size_t* guardsize );

int pthread_attr_setguardsize (pthread _attr_t* attr,size_t guardsize );

int pthread_attr_getschedparam ( const pthread_attr_t* attr,struct sched_param*param ) ;

int pthread_attr_setschedparam ( pthread_attr_t* attr,const struct sched_param* param );

int pthread_attr_getschedpolicy ( const pthread_attr_t* attr,int* policy );

int pthread_attr_setschedpolicy ( pthread_attr_t* attr,int policy );

int pthread_attr_getinheritsched ( const pthread_attr_t* attr,int* inherit);

int pthread_attr_setinheritsched (pthread_attr_t* attr, int inherit );

int pthread_attr_getscope ( const pthread_attr_t* attr,int* scope );

int pthread_attr_setscope ( pthread_attr_t* attr, int scope ) ;

下面我们详细讨论每个线程属性的含义:

  • detachstate,线程的分离状态。它有PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACH两个可选值。前者指定线程是可以被回收的,后者使调用线程分离与进程中其他线程的同步。脱离了与其他线程同步的线程称为“分离线程”。分离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE。此外,我们也可以使用pthread_detach函数直接将线程设置为分离线程。
  • stackaddr 和 stacksize,线程堆栈的起始地址和大小。一般来说,我们不需要自己来管理线程堆栈,因为Linux默认为每个线程分配了足够的堆栈空间(一般是8 MB)。我们可以使用ulimt -s命令来查看或修改这个默认值。
  • guardsize,保护区域大小。如果guardsize大于0,则系统创建线程的时候会在其堆栈的尾部额外分配guardsize字节的空间,作为保护堆栈不被错误地覆盖的区域。如果guardsize等于0,则系统不为新创建的线程设置堆栈保护区。如果使用者通过pthread_attr_setstackaddr或pthread_attr_setstack函数手动设置线程的堆栈,则guardsize属性将被忽略。
  • schedparam,线程调度参数。其类型是sched_param结构体。该结构体目前还只有一个整型类型的成员——sched_priority,该成员表示线程的运行优先级。
  • schedpolicy,线程调度策略。该属性有SCHED_FIFO、SCHED_RR和SCHED_OTHER三个可选值,其中SCHED_OTHER是默认值。SCHED_RR表示采用轮转算法(round-robin)调度,SCHED_FIFO表示使用先进先出的方法调度,这两种调度方法都具备实时调度功能,但只能用于以超级用户身份运行的进程。
  • inheritsched,是否继承调用线程的调度属性。该属性有PTHREAD_INHERIT_SCHED和PTHREAD_EXPLICIT_SCHED两个可选值。前者表示新线程沿用其创建者的线程调度参数,这种情况下再设置新线程的调度参数属性将没有任何效果。后者表示调用者要明确地指定新线程的调度参数。
  • scope,线程间竞争CPU的范围,即线程优先级的有效范围。POSIX标准定义了该属性的PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS两个可选值,前者表示目标线程与系统中所有线程起竞争CPU的使用,后者表示目标线程仅与其他隶属于同一进程的线程竞争CPU的使用。目前Linux只支持PTHREAD_SCOPE_SYSTEM这一种取值。

线程分离pthread_detach:将线程属性设置为detach状态
在设计线程的时候,线程有很多属性,其中有一种为分离属性,线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE,表示线程退出后不会自动释放资源,需要被等待。如果设置现成的分离属性为PTHREAD_CREATE_DETACH,这时线程退出后不在需要被等待,而是直接释放资源

此外,我们也可以使用pthread_detach函数直接将线程设置为分离线程:

#include<pthread.h>
int pthread_detach(pthread_t thread);

一个线程,既不关心返回值,也不想等待,这种线程就适合被分离。

POSIX信号量

和多进程程序一样,多线程程序也必须考虑同步问题。pthread_join可以看作一种简单的线程同步方式,不过很显然,它无法高效地实现复杂的同步需求,比如控制对共享资源的独占式访问,又或是在某个条件满足之后唤醒一个线程。
有3种专门用于线程同步的机制:POSIX信号量、互斥量和条件变量。
线程的同步互斥:

同步:通过一些条件判断实现对资源获取的合理操作
互斥:保证执行流在同一时间对临界资源的访问是唯一的

POSIX信号量:
本质:就是一个计数器,用于实现进程或线程之间的同步与互斥
操作

P操作:计数-1,判断技术是否大于等于0,成立则返回,否则阻塞
V操作:计数+1,唤醒一个阻塞的进程或线程

同步的实现
通过计数器对资源数量进行计数,获取资源之前进行P操作,产生资源之后进行V操作。通过这种方式实现对资源的合理获取
互斥的实现
计数器初始值为1(资源只有一个),访问资源前进行P操作,访问完毕进行V操作,实现类似于加锁和解锁的操作
实现流程

#include < semaphore.h>
//1、定义
sem_t sem;
//2、初始化
int sem_init(sem_t* sem, int pshared,unsigned int value);	
//3、P操作
int sem_wait( sem_t*sem);		//阻塞
int sem_trywait(sem_t*sem) ;	//非阻塞
//4、V操作
int sem_post(sem_t* sem);
//5、销毁
int sem_destroy(sem_t* sem) ;
  • 这些函数的第一个参数sem指向被操作的信号量
  • sem_init函数用于初始化一个未命名的信号量(POSIX信号量API支持命名信号量,不过本书不讨论它)。pshared参数指定信号量的类型。如果其值为0,就表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的结果。
  • sem_destroy函数用于销毁信号量,以释放其占用的内核资源。如果销毁一个正被其他线程等待的信号量,则将导致不可预期的结果。
  • sem_wait函数以原子操作的方式将信号量的值减1。如果信号量的值为0,则sem_wait将被阻塞,直到这个信号量具有非О值。
  • sem_trywait与sem_wait函数相似,不过它始终立即返回,而不论被操作的信号量是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值非0时,sem_trywait对信号量执行减1操作。当信号量的值为0时,它将返回-1并设置errno为EAGAIN.
  • sem _post函数以原子操作的方式将信号量的值加1。当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
  • 返回值:上面这些函数成功时返回o,失败则返回-1并设置errno

互斥锁

本质:就是一个0/1的计数器,主要用于标记资源的访问状态;0——不可访问,1——可访问
操作:加锁,解锁

加锁:将状态置为不可访问状态
解锁:将状态置为可访问状态

一个执行流在访问资源之前进行加锁操作,如果不能加锁则阻塞,在访问资源完毕之后解锁
互斥锁实现互斥,本质上自己也是个临界资源(同一个资源所有线程在访问的时候必须加同一把锁)。因此互斥锁必须先保证自己是安全的——即互斥锁的操作是一个原子操作
实现流程:

#include <pthread.h>
//1、定义互斥锁变量
pthread_mutex_t mutex;
//2、初始化互斥锁
int pthread_mutex_init ( pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);
//3、加锁
int pthread_mutex_lock ( pthread_mutex_t* mutex);		//阻塞
int pthread_mutex_trylock ( pthread_mutex_t* mutex);	//非阻塞
//4、解锁
int pthread_mutex_unlocki (pthread_mutex_t* mutex);
//5、释放销毁
int pthread_mutex_destroy ( pthread_mutex_t* mutex);
  • 这些函数的第一个参数mutex指向要操作的目标互斥锁,互斥锁的类型是 pthread_mutex_t 结构体
  • pthread_mutex_init函数用于初始化互斥锁。mutexattr参数指定互斥锁的属性。如果将它设置为NULL,则表示使用默认属性。我们将在下一小节讨论互斥锁的属性。除了这个函数外,我们还可以使用如下方式来初始化一个互斥锁: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 宏PTHREAD_MUTEX_INITIALIZER实际上只是把互斥锁的各个字段都初始化为0
  • pthread_mutex_destroy 函数用于销毁互斥锁,以释放其占用的内核资源。销毁一个已经加锁的互斥锁将导致不可预期的后果。
  • pthread_mutex_lock 函数以原子操作的方式给一个互斥锁加锁。如果目标互斥锁已经被锁上,则pthread_mutex_lock 调用将阻塞,直到该互斥锁的占有者将其解锁。
  • pthread_mutex_trylock 与 pthread_mutex_lock 函数类似,不过它始终立即返回,而不论被操作的互斥锁是否已经被加锁,相当于pthread_mutex_lock的非阻塞版本。当目标互斥锁未被加锁时,pthread_mutex_trylock对互斥锁执行加锁操作。当互斥锁已经被加锁时,pthread_mutex_trylock将返回错误码EBUSY。需要注意的是,这里讨论的pthread_mutex.lock 和 pthread_mutex_trylock 的行为是针对普通锁而言的。后面我们将看到,对于其他类型的锁而言,这两个加锁函数会有不同的行为。
  • pthread_mutex_unlock函数以原子操作的方式给一个互斥锁解锁。如果此时有其他线程正在等待这个互斥锁,则这些线程中的某一个将获得它。
  • 返回值:上面这些函数成功时返回0,失败则返回错误码

死锁
概念:如果一组进程中的每一个进程都在等待仅有这组进程中的其他进程才能引发的事件,那么这组进程是死锁的。即程序流程无法推进,卡死的情况叫做死锁
产生:由于对锁资源的争抢不当所导致
四个必要条件

  • 互斥条件:进程对所分配道德资源进行排他性使用,即在一段时间内,某资源只能被一个进程占用。如果此时还有其他进程请求这个资源,则请求进程只能等待,直至占有这个资源的进程用毕释放。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而这个资源已经被其他进程占有,此时请求进程被阻塞,但对自己已获得得资源保持不放。
  • 不可抢占条件:进程以获得得资源在未使用完之前不能被抢占,只能在进程使用完时由自己释放
  • 循环等待条件:在发生死锁时,必然存在一个进程—资源的循环链,即进程集合{P0,P1,P2,…,Pn}中的P0正在等待一个P1占用的资源,P1正在等待P2占用的资源,…,Pn正在等待已被P0占用的资源。

预防死锁:通过一些限制条件,去破坏死锁产生的四个必要条件中的一个或几个来预防死锁
避免死锁:避免产生死锁的一些具体方案——银行家算法
检测思索:通过检测机构及时的检测出死锁的发生,然后采取适当的措施,把进程从死锁中解脱出来。
解除死锁:当检测到系统中已经发生死锁时,就采取相应的措施,将他们分配给已处于阻塞状态中解脱出来。

条件变量

如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量则是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通知机制;当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。
条件变量的相关函数主要有如下5个:
实现流程:

#include <pthread.h>
//1、定义条件变量
pthread_cond_t cond;
//2、初始化条件变量
int pthread_cond_init(pthread_cond_t* cond, const pthread_eondattr_t* cond_attr);
//3、使线程阻塞
int pthread_cond_wait(pthread_cond_t cond, pthread_mutex_t* mutex) ;
int pthread_cond_timedwait();
//4、唤醒阻塞的线程
int pthread_cond_signal (pthread_cond_t* cond);		//将cond的pcb队列中的线程至少唤醒一个
int pthread_cond_broadcast (pthread_cond_t* cond);	//将cond的队列中的线程全部唤醒
//5、释放销毁
int pthread_cond_destroy (pthread_cond_t* cond) ;
  • 这些函数的第一个参数cond指向要操作的目标条件变量,条件变量的类型是pthread_cond_t结构体。
  • pthread_cond_init函数用于初始化条件变量。cond_attr参数指定条件变量的属性。如果将它设置为NULL,则表示使用默认属性。条件变量的属性不多,而且和互斥锁的属性类型相似,所以我们不再赘述。除了pthread_cond_init函数外,我们还可以使用如下方式来初始化一个条件变量:
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 宏PTHREAD_COND_INITIALIZER实际上只是把条件变量的各个字段都初始化为0
  • pthread_cond_destroy函数用于销毁条件变量,以释放其占用的内核资源。销毁一个正在被等待的条件变量将失败并返回EBUSY。
  • pthread_cond_broadcast函数以广播的方式唤醒所有等待目标条件变量的线程。thread_cond_signal函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。有时候我们可能想唤醒一个指定的线程,但pthread没有对该需求提供解决方法。不过我们可以间接地实现该需求:定义一个能够唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后采用广播方式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己,如果是就开始执行后续代码,如果不是则返回继续等待。
  • pthread_cond_wait 函数用于等待目标条件变量。mutex参数是用于保护条件变量的互斥锁,以确保pthread_cond_wait操作的原子性。在调用pthread_cond_wait前,必须确保互斥锁mutex已经加锁,否则将导致不可预期的结果。pthread_cond_wait函数执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex解锁。可见,从pthread_cond_wait开始执行到其调用线程被放入条件变量的等待队列之间的这段时间内,pthread_cond_signal和pthread_cond_broadcast等函数不会修改条件变量。换言之,pthread_cond_wait 函数不会错过目标条件变量的任何变化。当pthread_cond_wait 函数成功返回时,互斥锁mutex将再次被锁上。
  • 返回值:上面这些函数成功时返回0,失败则返回错误码。

概念对比

进程与线程的区别

1、同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间 2、同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的
3、一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程崩溃,所以多进程比多线程健壮
4、进程切换,消耗的资源大,所以涉及到频繁的切换,使用线程要好于进程 5、两者均可并发执行
6、每个独立的进程有一个程序的入口,程序出口,但是线程不能独立执行,必须存在应用程序中,由应用程序提供多个线程执行控制

并行与并发

  • 并行性:是指两个或多个事件在同一时刻发生。
  • 并发性:是指两个或多个事件在同一时间间隔发生。

条件变量与信号量实现同步上的区别

1、本质不同,信号量是个计数器,而条件变量没有计数器,因此条件变量的资源访问合理性需要用户自己进行,但是信号量可以通过自身计数完成。
2、条件变量需要搭配互斥锁一起使用,而信号量不需要

POSIX标准信号量
计数器用于线程可以是局部变量通过传参使用同一个,或者全局变量
计数器用于进程间,这个计数器是通过共享内存来实现的
systemV标准信号量:Linux内核提供的一个计数器。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Linux中的多线程实际上是通过进程来模拟实现的。在Linux中,多个线程是通过共享父进程的资源来实现的,而不是像其他操作系统那样拥有自己独立的线程管理模块。因此,在Linux中所谓的“线程”其实是通过克隆父进程的资源而形成的“线程”。这也是为什么在Linux中所说的“线程”概念需要加上引号的原因。 对于Linux中的线程,需要使用线程库来进行管理。具体来说,Linux中的线程ID(pthread_t类型)实质上是进程地址空间上的一个地址。因此,要管理这些线程,需要在线程库中进行描述和组织。 由于Linux中没有真正意义上的线程,因此线程的管理和调度都是由线程库来完成的。线程库负责创建线程、终止线程、调度线程、切换线程,以及为线程分配资源、释放资源和回收资源等任务。需要注意的是,线程的具体实现取决于Linux的实现,目前Linux使用的是NPTL(Native POSIX Thread Library)。 总结来说,Linux中的多线程是通过进程来模拟实现的,线程共享父进程的资源。线程的管理和调度由线程库完成。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Linux —— 多线程](https://blog.csdn.net/sjsjnsjnn/article/details/126062127)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灯火不熄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值