最近在复习这块内容,随时整理一下,未完。
一、多线程
线程概念
- 程序(program):为完成特定任务、用某种语言编写的一组指令。即指一段静态的代码
- 进程(process):进程是动态的,是程序的一次执行过程,或是正在运行的一个程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
- 线程(thread):进程可进一步细化为线程,线程是进程中的一个执行单元,是一个程序内部的一条执行路径。一个进程中至少有一个线程。
- 多线程:同一个进程内部有多个线程。
- 所有的线程共享同一个进程的内存空间(同样的动态内存、映射文件、目标代码……),进程中定义的全局变量会被所有的线程共享,比如有全局变量int i = 10,这一进程中所有并发运行的线程都可以读取和修改这个i的值。
- 除了标识线程的tid,每个线程还有自己独立的栈空间,线程彼此之间是无法访问其他线程栈上内容的。每个线程独自占用一个虚拟处理器
- 线程调度只需要保存线程栈、寄存器数据和指令指针(PC)即可,相比进程切换开销要小很多,线程是处理机调度的最小单位,是程序执行流的最小单元。
- 线程包括两个方面:第一:线程内核对象(OS用来存放统计信息的地方);第二:线程堆栈(函数参数和局部变量)
- 线程在它的进程的地址空间执行代码。内核对象句柄依赖于进程而存在。
什么时候使用多线程?
多线程模型主要优势为线程间切换代价较小,因此适合I/O密集型工作场景,所以在编写程序时,遇到了阻塞过程而不想使整个程序停止响应时,应使用多线程。同时,多线程模型也适用于单机多核分布式场景。
为啥使用多线程?
- 提高cpu资源利用率(执行主线程任务时,还可以同时执行其他的)
- 创建线程开销比较小(比创建进程要小)
- 线程之间可以共享数据(进程之间不行)
线程基本状态
就绪、阻塞和运行三种基本状态。
- 就绪状态,指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;
- 运行状态,指线程占有处理机正在运行;
- 阻塞状态,指线程在等待一个事件(如信号量),逻辑上不可执行。
并发并行
- 一个逻辑流的执行在时间上与另一个流重叠,称为并发流,它们并发地执行的一般现象就叫做并发(concurrency),并发流的思想与流运行的处理器核数或者计算机数无关。
- 如果两个流并发地运行在不同的处理器核或计算机上,就称它们为并行流(parallel flow)
- 历史上最开始什么样,不代表固定不变。多进程有多进程的缺点,所以提出了多线程。并发、并行和多进程、多线程并没有严格的对应关系。
- 并发更多的是针对逻辑结构,不同任务可以无需等待上一个任务完成才开始,多个任务可以同时开始。多个任务即可以以多进程方式开始,也可以以多线程方式开始。并行是多个任务可以同时在CPU上执行,并行更关注执行状态。
线程的创建和结束
- 创建线程:
int pthread_create( pthread_t *pthread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *agr
);
| 参数 | 说明 |
|---|---|
| pthread_t *pthread, | pthread:用来返回线程的tid,标识线程,*pthread值即为tid,类型pthread_t == unsigned long int。 |
| const pthread_attr_t *attr, | attr:指向线程属性结构体的指针,用于改变所创线程的属性,填NULL使用默认值。 |
| void *(*start_routine)(void *), | 定义了一个名字为start_routine的函数指针,指向返回类型是void* 类型(指针)且形参为void* 类型的函数。即:线程执行函数的首地址,传入函数指针。 |
| void *arg); | arg:通过地址传递来传递函数参数,这里是无符号类型指针,可以传任意类型变量的地址,在被传入函数中先强制类型转换成所需类型即可。 |
- 获得线程ID:
pthread_t pthread_self();调用时,会打印线程ID。 - 等待线程结束:
int pthread_join(pthread_t tid, void** retval);- 主线程调用:等待子线程退出并回收其资源,类似于进程中wait/ waitpid回收僵尸进程,调用pthread_join的线程会被阻塞。
- pthread_t tid,tid:创建线程时通过指针得到tid值。
- void** retval,retval:指向返回值的指针。
- 结束线程:
pthread_exit( void *retval );- 子线程执行,用来结束当前线程;并通过retval传递返回值,该返回值可通过pthread_join获得。
- 分离线程:
int pthread_detach(pthread_t tid);- 主线程可以调用:
pthread_detach(tid); - 子线程可以调用:
pthread_detach(pthread_self());调用后和主线程分离,子线程结束时自己立即回收资源。
- 主线程可以调用:
线程属性值修改
线程属性对象类型为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_arrt_t;
对上述结构体中各参数大多有:pthread_attr_get***()和pthread_attr_set***()系统调用函数来设置和获取。这里不一一罗列。
多线程的同步互斥
在多任务操作系统中,同时运行的多个任务可能:
- 都需要访问/使用同一种资源;
- 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。
- 同步:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。
- 比如:A 任务的运行依赖于 B 任务产生的数据。
- 互斥:当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。
- 一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
用户模式:不需要切换内核态,只在用户态完成操作。
临界区CriticalSection:
互斥only、线程所有权:串行到谁谁可、不可跨进程
- 适合一个进程内的多线程访问公共区域或代码段时使用。 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
内核模式:利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。
互斥量(互斥锁)Mutex:
-
综述:原子操作。采用对象互斥锁的概念,保证数据同一时间唯一访问。互斥量有两个状态,解锁、加锁。(线程所有权:拥有互斥对象的可)可跨进程。
-
过程:
- 线程在访问共享资源后、临界区域前,对互斥锁进行加锁。线程访问完成后释放该锁。
- 其他企图加锁的其他线程,将会被挂起。直到该锁被释放,被挂起的线程被唤醒并继续执行、锁定该互斥量。
-
实现:
- 创建和销毁:
(1)静态方式创建:pthread_mutex_t:结构,右值:结构常量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;(2)动态方式创建:mutexattr用于指定互斥锁属性,如果为NULL则使用缺省属性。
int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr )(3)注销互斥锁:释放锁所占用的资源,且要求锁当前处于开放状态。
int pthread_mutex_destroy( pthread_mutex_t *mutex )注:在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
- 互斥锁属性: 互斥锁的属性在创建锁的时候指定。
- 锁的操作:
int pthread_mutex_lock(pthread_mutex_t *mutex) // 加锁(在锁已经被占据时挂起) int pthread_mutex_unlock(pthread_mutex_t *mutex) // 解锁 int pthread_mutex_trylock(pthread_mutex_t *mutex) // 测试加锁(在锁已经被占据时返回EBUSY)
自旋锁
- 和互斥量阻塞了线程不同,在获得锁之前一直处于忙等(自旋)阻塞状态。
- 场景:自旋锁只能被短时间持有。
读写锁
- 顾名思义,将对共享资源的访问者分为读者、写者。读写锁有三种状态:读模式下加锁,写模式下加锁,不加锁装填。
- 一个读写锁同时只能有一个写者or多个读者(与CPU数相关),但不能同时既有读者又有写者。
- 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。
- 如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
- 场景:读写锁适用于对数据结构的读次数远大于写次数的情况。
pthread_rwlock_t myrw; pthread_rwlock_init(&myrw, NULL); pthread_rwlock_rdlock(&myrw);//只读模式去加锁 pthread_rwlock_wrlock(&myrw);//只写模式去加锁 pthread_rwlock_unlock(&myrw); pthread_rwlock_destroy(&myrw);
屏障
- 让多个线程并行工作,然后所有参与的线程都到达一个屏障后,从该点继续执行。比如:主线程中设一个屏障,它一直等待:6个工作线程并行处理数据,当6组都处理完成后,主线程对这些所有的数据继续操作。
- 场景:适用于并发完成同一项任务。
信号量Semaphore:(同步or互斥、可跨进程)
- 它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
- 与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据。先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。
事件(信号)Event:(同步or互斥、可跨进程)
- 通过线程间触发事件实现同步互斥。
- 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。
Linux下的线程同步:互斥锁,自旋锁,读写锁,屏障。 Windows下的线程同步:互斥量,信号量,事件,关键代码段
参考资料: 操作系统技能树; 互斥锁、条件变量、读写锁、自旋锁、信号量;多线程全面总结;互斥量,读写锁,自旋锁,条件变量,屏障