欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

Linux多线程

时间:2023-06-11
参考链接: Linux多线程Linux线程详解并发与并行的区别是什么? 并行和并发 并发(concurrency)

在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

同一个时刻只有一个指令运行,但多个进程指令被快速轮换执行,使得宏观上有多个进程被同时执行的效果(针对单核处理器)。

并发强调的是一起出发。

与并发相对的是不可以一起出发的顺序。

并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行;顺序:上一个开始执行的任务完成后,当前任务才可以开始执行。 并行(parallelism)

在单处理器多道程序设计系统中,进程被交替执行,表现出一种并发的外部特征;

在多处理器系统中,进程不仅可以交替执行,而且可以重叠执行。在多处理器上的程序才可实现并行处理。

并行是针对多处理器而言的,并行是同时发生的多个并发事件,具有并发的含义,但并发不一定并行,即并发事件不一定要同一时刻发生。

并行强调的是一起执行。

与并行相对的是串行。

串行:有一个任务执行单元,从物理上就只能一个任务、一个任务的执行(执行什么任务无所谓,反正只能执行一个任务);
并行:有多个任务执行单元,从物理上就可以多个任务一起执行。

即,在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。

综上,单核CPU多任务:并发(不必等上一个任务完成才开始下一个任务)、串行(只有一个实际执行任务的CPU核);多核CPU多任务:并发、串行(所有线程都在同一个核心上执行);并发、并行(不同线程在不同核心上执行)。

线程的优势

使用多进程时,程序每处理一个任务,都需要创建一个进程来处理,而每个进程在创建时都需要复制父进程的进程上下文,且有自己独立的地址空间。如果需要并发处理很小的任务时(如服务器并发处理客户端的请求),这种开销是很不划算的,并且每个进程之间的数据并不共享,这使得进程间通信也十分麻烦。

相比之下,线程的优势便体现出来了。线程有自己的上下文(thread context),虽然只是一些如线程ID、栈、栈指针、程序计数器、通用寄存器之类的东西,所以线程的开销较小。一般都是在线程运行的函数中指定,因此每个线程都是独立的。而所有线程依旧在这个进程内共享一片地址空间,包括代码、堆、共享库和打开的文件等。

线程的创建

关键头文件 ,编译时还需链接 pthread 库文件。pthread 即遵守 Posix 标准的多线程,它是 C 语言处理线程的一个标准接口。

创建线程的函数:

int pthread_create(pthread_t* tidp, const pthread_attr_t* attr, (void*)(*start_run)(void*), void* arg);

对于开发者来说,创建线程无非做了两件事(对系统来说是比较复杂的),让操作系统在当前进程创建一个线程,将线程的相关信息写入第一个参数指向的 pthread_t 类型的线程类型中,这个类型在 Linux 内部的定义是 unsigned long int 类型,代表线程 ID;

attr 参数用于指定线程的一些属性;有 joinable 和 unjoinable 状态,默认为 joinable。

start_run 对应线程要做的事情的函数指针;

arg 则是需要传入的参数;

注意:函数指针的参数和返回值都必须定义为 void * 类型。

线程的退出 线程状态

Linux 的线程状态分为 joinable 和 unjoinable 状态。

如果是 joinable 状态的线程,则线程所占用的资源即使在线程 return 或者 pthread_exit 时都不会释放,只有在其他线程中调用 pthread_join 函数才会释放。

而 unjoinable 状态的线程的资源会在线程退出时自动释放,不需要 pthread_join 函数进行手动释放。

因此 pthread_join 操作的线程必须是 joinable 的。

线程退出函数 pthread_exit 函数

函数原型为:void pthread_exit(void* retval);

在需要退出的线程中使用,使得线程显式地退出。当然线程也可以通过函数的 return 退出。

线程退出时允许携带一个返回值,这个指将被调用 pthread_join 的线程接收到。

pthread_join 函数

函数原型为:int pthread_join(pthread_t* thread, void** retval);

在等待线程退出的线程中使用,thread 参数代表等待的线程 ID。

注意,这个函数执行后是阻塞的,目的是等待 thread 的退出,如果指定的线程已经退出了会立刻返回。

如果是 joinable 状态的线程,则线程所占用的资源即使在线程 return 或 pthread_exit 时都不会释放,只有在其它线程中调用 pthread_join 函数才会释放。

而 unjoinable 状态的线程的资源在线程退出时就会自动释放,不需要 pthread_join 函数进行手动释放。

因此,pthread_join 操作的线程必须是 joinable 的。

pthread_join 执行的其实就是线程合并,将调用函数的线程与指定的线程进行合并,然后调用线程会等待被指定的线程的退出,退出后进行资源的释放。

pthread_detach 函数

函数原型:int pthread_detach(pthread* thread);

该函数不会将调用线程与退出线程合并,也不会阻塞调用线程。只是在等待退出线程退出后将资源回收,效果与将线程设置为 unjoinable 相同。

线程同步

在同一个进程中,不同线程的运行先后顺序对开发者来说是不确定的。在操作系统内部,内核在短时间内不断地在不同的线程上下文间进行切换。因此,当有多个线程操作一个临界资源时,需要程序员进行线程的同步。

互斥锁

Mutex,互斥锁允许一个或多个线程同时拥有它(一般是一个),当多个线程需要同时操作某临界资源时,它们需要先抢占互斥锁,抢到了互斥锁的线程可以继续运行,没抢到的则被阻塞,直到有线程释放了互斥锁,则继续抢占。所以在线程抢占了互斥锁操作完临界资源应该尽早释放互斥锁,避免其他线程阻塞过久。

互斥锁实现方式有多种,这里主要讲 pthread.h 中提供的互斥锁接口。

互斥锁的主要操作函数有三个:

pthread_mutex_t mutex;//定义一个互斥锁变量pthread_mutex_init(pthread_mutex_t* mutex); //初始化互斥锁pthread_mutex_lock(pthread_mutex_t* mutex); //上锁pthread_mutex_unlock(pthread_mutex_t* mutex);//操作结束释放锁

条件变量

假如某一个线程需要临界资源满足某一种条件才进行操作,如抢票系统要在票全部卖完后才进行加票。加票的线程需要一直进行互斥锁的抢占,抢占后只是检查票是否卖完,但大部分时间是没有卖完,因此造成了很多不必要的抢占。在当票真正卖完时,加票线程又可能抢占多次才真正抢到互斥锁,导致程序延误。为了解决这种问题,Linux 提供了条件变量接口。

条件变量(condition variable),系统会将使用条件变量的线程阻塞,直到满足条件时才唤醒,唤醒后互斥锁将被当前线程锁定。

定义条件变量:pthread_cond_t cond;

初始化条件变量:int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* cond_attr);。其中,cond_attr 表示需要赋予条件变量的属性,Linux man pages 是这么描述的:“The LinuxThreads implementation supports no attributes for conditions, hence the cond_attr parameter is actually ignored.”,通常直接设置为 NULL。

唤醒使用了条件变量同时正在阻塞的线程:int pthread_cond_signal(pthread_cond_t* cond);。如果有多个正在阻塞的线程,依旧只唤醒一个线程,但无法确定是哪个线程。

唤醒所有正在阻塞的线程:int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);。如果有多个正在阻塞的线程,则这些线程需要再次抢占互斥锁。

阻塞线程,等待条件变量响应后唤醒:int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);。唤醒后得到响应的互斥锁 mutex(如果有多个阻塞线程,需要抢占)。值得注意的是,线程离开阻塞状态不一定是因为条件变量满足,也有可能遇到了中断信号或出现错误。

加上阻塞时限:int pthread_cond_timewait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);

释放条件变量:int pthread_cond_destroy(pthread_cond_t* cond);

线程池

由于线程的创建和销毁都需要消耗时间,在需要持续处理高并发任务时,通常线程在处理一个任务后并不会直接退出,而是阻塞或接着执行下一个任务,这就是线程池。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。