线程

最后他的身躯一点点消散,留下了苍老的浮云和涛声,而他的血管,业已成为那条河的支流。

基本介绍

定义

线程是CPU进行调度和分配的基本方式,一个CPU在一个时间只能运行一个线程。我们要注意程序为何要设计成多线程的,在一开始CPU都是单核的,其实这种情况下程序执行的最快的方式就是单核单线程,但是这样会带来并发问题,例如当处理IO时,如果只是单线程的,就会导致阻塞,此时CPU无事可做,所以我们开始将程序设计为多线程,从而实现并发。给我们一种错觉,就是同一时间有多个程序在执行。

线程的特点及优点

特点

线程是轻量化的进程,其特点为:

  • 共享代码、数据、文件和内存地址
  • 独有寄存器(程序计数器)和栈空间,因为每个线程执行速度是不一样的

优点

单CPU

充分利用CPU,令$t_i$为线程1的空闲时间,$t_s$为上下文切换时间,那么当满足下面公式时,进行多线程可以提高CPU利用率。一般来说,线程切换比进程切换开销小很多。

多CPU
  1. 加速
  2. 专业化,每个线程做特定的工作,也可以保证hot cache

支持线程机制的要素

  • thread data struct:和PCB一样,用于对线程进行描述。
  • 创建和管理线程的机制
  • 多线程合作的机制:由于多线程共享内存地址,所以会导致同步问题

Thread data structure

一个完善的线程数据结构应包括:线程ID、程序计数器、栈顶指针(SP)、栈段以及其他的一些关键属性。

线程创建和管理

fork

fork,创建一个线程而非进程,线程创建的同时,还会初始化线程数据结构。

join

当父线程t0对子线程t1调用join时,join(t1),那么父线程会被阻塞,直到子线程返回。同时,join将会返回子线程的计算结果。

detach

一个detach的线程被创建后,父子线程是相互独立的。

线程创建的例子(伪代码)

1
2
3
4
5
Thread thread1;
Shared_list list;
thread1 = fork(safe_insert,4);
safe_insert(6);
join(thread1); //这里join是一个非必须的语句,因为很有可能执行到这一步时,thread1已经执行完了,那么join会立刻返回

这个代码没有考虑线程调用顺序,因此会导致结果的不确定性。

内核态和用户态线程

内核态和用户态中都可以有线程。内核态的线程可以由内核访问和管理。调度器会决定哪个线程运行在哪个CPU上。一些内核线程可能在支持用户态线程的工作,另一些可能在执行一些内核的工作。一个用户态线程必须和一个内核线程关联后,才能被正确调度和运行。本节将针对内核态和用户态线程的调度模型进行总结。

内核线程与用户线程的区别

  1. 内核线程运行在内核态,而用户线程可以运行在用户态,也可以运行在内核态(系统调用时)
  2. 内核线程一般作为守护线程,执行一些诸如页表换入换出、脏页回写、软中断处理等操作
  3. 内核线程也会参与调度,每个CPU上有一个IDLE线程,实际是一个优先级最低的内核线程,实在没有任务了就执行这个
  4. init是所有用户进程的祖先,而kthreadd是所有内核线程的祖先

用户态多线程模型

一一对应模型

在该模型中,一个用户态线程和一个内核态线程是一一对应的关系。当一个用户态线程被创建后,就会有一个新创建或已经存在的内核线程和其一一对应。

优点

并发性好,内核可以知道用户到底有多少个线程,从而可以动态调整内核线程数目和用户态进程中的多线程相匹配

缺点
  • 必须在操作系统中完成所有操作,而且要频繁在用户态和内核态切换,代价大。
  • 只能依赖内核态的线程调度策略,这也限制了可移植性

多对一模型

在该模型中,多个用户态线程和一个内核态线程对应,由用户指定调度策略,指定规定时间哪个线程和内核态线程对应

优点

可移植性强,不依赖内核态线程调度策略

缺点
  • 由于没有用到内核线程的特性,所以操作系统对于用户的多线程程序基本上没太多帮助,操作系统也不知道用户到底有几个线程
  • 一个用户线程的阻塞将会导致整个进程的阻塞

多对多模型

该模型是上面两种模型的综合,一些线程一一对应,另一些和内核多对一对应

优点

综合了上面两种的优点

缺点

对于内核和线程的管理更加复杂,需要一个专门的用户-内核态线程管理器

用户态和内核态线程调度

在用户态中,由一个用户层面的库对于一个进程中的线程进行管理,而在内核态中,由CPU调度器对内核态线程的调度和资源分配进行管理

Pthreads(Posix thread)

pthreads(Portable operating system interface)是Unix系统下常用的线程API,本节将针对pthreads的使用进行介绍。

创建

常用数据结构及API

创建thread对象
1
2
//-----------------------thread对象
pthread_t aThread; //type of thread
创建线程
1
2
3
4
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr, //设置线程属性,例如栈大小、优先级、调度策略、是否可join
void * (*start_routine)(void *),
void *arg); //函数输入参数

返回值表示是否创建成功,可以根据attr设置创建线程的属性。

join
1
2
3
4
int pthread_join(pthread_t thread, 
void **status);

// status将会捕获子进程返回信息和结果

返回值表示join是否成功

detach
1
2
3
4
5
6
// 方式1
int pthread_detach();

//方式2
pthread_attr_setdetachstate(attr, PTHREAD_CREATE_DETACHED);
pthread_create(..., attr, ...);
exit
1
int pthread_exit();
设置属性
1
2
3
4
//----------------------- attr 处理
int pthread_attr_init(pthread_attr_t* atrr); //创建attr
int pthread_attr_destory(pthread_attr_t* atrr); //销毁attr
int pthread_attr_{get/set}(); //获得/设置

例子:创建多线程程序

无参数线程函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <pthread.h>

void *foo (void ar){
pthread_exit(NULL);
}

int main(void){
int i;
pthread_t tid;

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(attr, PTHREAD_CREATE_DETACHED);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); // 设置调度方式和系统相同
pthread_create(NULL, &attr, foo, NULL);

return 0;
}

编译过程为:gcc -o main main.c -lpthread

有参数线程函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <pthread.h>

void *threadFun (void *pArg){
int *p = (int*) pArg;
int myNum = *p;
printf("%d", myNum);
return 0;
}

int main(void){
int i = 5;
pthread_t tid;
pthread_create(&tid, NULL, threadFun, &i); // Input parameter

return 0;
}

Mutex

pthreads 对于锁的使用如下所示:

1
2
3
4
5
6
7
8
9
10
pthread_mutex_t aMutex;   //创建锁

int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t *attr); //初始化

int pthread_mutex_lock(pthread_mutex_t* mutex); //上锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);//解锁

int pthread_mutex_trylock(pthread_mutex_t* mutex); //尝试上锁,如果不成功,不会阻塞当前线程

int pthread_mutex_destory(pthread_mutex_t* mutex);//销毁锁

注意:Mutex不能用在中断场景下,因为会引起睡眠

条件变量

1
2
3
4
5
6
7
8
9
10
11
pthread_cond_t aCond; //创建

//--------------------------- wait
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t *mutex);

//--------------------------- notify
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);

int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t *attr); //初始化
int pthread_cond_destory(pthread_cond_t* mutex);//销毁锁

Linux下线程的实现

共享与独占资源

在linux下的线程中,会有自身独有和共享的资源,这里对这些资源进行一个总结,其中,共享的资源包括:

  • 虚拟内存映射
  • 进程基础信息
  • 一些数据
  • 打开的文件
  • 信号处理
  • 当前工作目录
  • 用户和用户组属性
  • ……

独占的资源包括:

  • 线程ID
  • 一系列寄存器
  • 错误码
  • 信号掩码
  • 线程优先级
  • ……

进程与线程的联系与区别

联系

线程实际上是将进程的指令执行流部分进行了剥离,进程负责资源调度,而具体指令执行交给了线程,两者的关系可以用下面的图进行概括:

图片名称

可以看的线程是有自己独立的堆栈和SP指针,以及相应的寄存器等。所以线程实际就是进程减去资源共享

区别

  1. 进程是操作系统进行资源分配的最小单位,而线程是CPU进行任务调度的最小单位。一个进程至少包含一个线程
  2. 进程有独立的地址空间,每次启动一个进程,系统会分配地址空间,同时建立数据表维护代码、数据、堆栈段,开销很大。而线程共享代码段(代码与常量)、数据段(全局变量与静态变量)和堆段,开销小很多,但是每个线程有自己的栈段(局部变量和临时变量)。创建与切换线程的开销比创建切换线程大很多。
  3. 线程间通信更方便,同一进程下的线程共享全局变量、静态变量等数据,进程间通信需要以通信方式进行,但是多线程需要处理好同步与互斥。
  4. 多进程程序更健壮,一个进程的消亡不会影响另外的进程;而多线程的程序有一个线程死掉,整个进程就会死掉。

参考文献

0%