[后台开发工程师总结系列] 3.操作系统之线程

多线程

为了更好的理解线程的概念,先对进程、线程的背景做介绍。

早期的计算机都只允许一个程序独占系统资源,一次只能执行一个程序。

这种背景下,一台计算机支持多个程序并发执行的需求就变得迫切,由此产生了进程的概念。进程在多数早期多任务操作系统中是执行工作的基本单元。进程是包含程序指令和相关资源的集合,每个进程和其他进程一起参与调度,竞争CPU、内存。每次进程的切换都存在进程资源的保护和恢复动作,这称为上下文切换。进程的引入可以解决多用户支持的问题,但也引入了新的问题:进程频繁切换可能严重影响性能。

同一个进程内部可能有多个线程,共享同一个京城的所有资源。通过线程支持了同一应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务也更简单。

网络具有天生的并发性,比如数据库可能同时需要处理数以千计的请求。而由于网络连接的不确定性和不可靠性,等待网络交互时,可以让当前的线程进入睡眠、退出调度,处理其他线程,这样能够充分利用系统资源,发挥系统实时处理能力。

多线程是什么

一个程序运行的过程中,只有一个控制权存在,当函数调用时,函数获得控制权,成为激活函数;同时其他函数不运行。

多线程程序就是允许一个进程内存在多个控制权,以便让多个函数处于激活状态,让多个函数同时运行,即便是单核计算机也可以通过指令切换,实现多个线程同时运行的效果。

1551767091258

回忆栈的 功能和用途:一个栈中只有最下方的栈可被读写,相应的也只有这个栈对应的函数被激活,处于工作状态。为了实现多线程,则必须绕开栈的限制。这样多个线程就有了多个函数栈,每个栈对应一个线程。

多线程的创建与结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <pthread.h>

void* say_hello(void* args){
printf("hello from thread\n");
pthread_exit((void*)1);
}
int main(){
pthread_t tid;
int iRet = pthread_create(&tid, NULL, sayhello, NULL);
if(iRet){
printf("create error");
return iRet;
}
void* retval;
iRet = pthread_join(tid, &reval);
if(iRet){
printf("join error");
return iRet;
}
printf("retval %ld", retval);
return 0;
}

线程的属性

线程有一组属性是可以在创建时被指定的。该属性被封装在一个对象中,该对象可以用来设置一个或一组线程。线程属性对象类型为 pthread_t .

默认属性为 : 非绑定、非分离、默认1MB大小的栈

1551767866351

  1. 分离状态 若线程终止时,线程处于分离状态,系统将不保留线程的终止状态;不需要这个状态可一直设置
  2. 栈地址 可以设置线程的栈地址
  3. 栈大小 系统中有很多线程时,可能需要减小每个线程栈的大小,防止空间不够;如果调用函数很深时,需要加大线程栈的大小。
  4. 栈保护区大小 (防止栈溢出,进入错误提示)
  5. 线程优先级
  6. 继承父进程优先级
  7. 调度策略; 新线程的调度时一旦开始运行,直到被抢占或调度停止
  8. 争用范围
  9. 并发级别

多线程同步

互斥锁

互斥锁是一个特殊的变量,它有上锁和解锁两个状态。互斥锁一般被设置为全局变量。打开互斥锁可以由某个线程获得,一旦获得这个互斥锁就会被锁上,只有该线程有权打开这个,其他线程需要等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER
int total_ticket_num = 20;
void *sell_ticket(void *arg){
for(int i=0; i<20; i++){
pthread_mutex_lock(&mutex);
if(total_ticket_num>0){
sleep(1);
printf("sell ticket %d", 20 - total_ticket_num);
total_ticket_num--;
}
pthread_mutex_unlock(&mutex);
}
reutrn 0;
}

int main(){
int iRet;
pthread_t tids[4];
int i = 0;
for(int i=0; i<4; i++){
int iRet = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
if(iRet){
printf("Error");
return iRet;
}
}
sleep(30);
void* retval;
for(int i=0; i<4; i++){
iRet = pthread_join(tids[i], &retval);
if(iRet){
printf("Error");
return iRet;
}
}
return 0;
}

条件变量

条件变量允许线程阻塞和等待另一线程发送信号的方法弥补互斥锁的不足,他常常和互斥锁一起使用。使用时,条件变量用来阻塞一个线程,当条件不满足时,线程往往解开互斥锁等待条件变化。而当其他的线程改变了条件变量时,它将通知相应被这个条件变量阻塞的一个或多个线程,对这些线程获得锁并测试是否瞒住。

读写锁

一些程序中存在读者写着问题,某些资源的访问可能出现两种情况,一种是排他性的(独占),另一种操作访问是可以共享的,可以有多个线程同时去访问某个资源。这就是读操作,这个模型从读写操作中引申出来。

读写锁也叫共享-独占锁。处理读者写者的常见策略是强读者同步或强写者同步,即总是有限读和优先写

信号量

线程可以通过信号量实现通信,信号量和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区。要使用信号量同步,需要包含头文件semaphore.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sem_t t;
void get_service(void *thread_id){
int customer_id = *((int*)thread_id);
if(sem_wait(&sem) == 0){
usleep(100);
printf("%d customer service", customer_id);
sem_post(&sem);
}
}
int main(){
sem_init(&sem,0, 2);
pthread_t customers[10];
int i, iRet;
for(int =0; i<10; i++){
int customer_id = i;
iRet = pthread_create(&customers[i],NULL, get_service, &customer_id);
if(iRet){...}
}
int j;
for(int j=0; j<10;i++){
pthread_join(customers[i], NULL);
}
sem_destory(&sem);
return 0;
}

多线程重入

前面介绍了很多方式都是为了解决 “函数不可重入的问题”。

所谓“可重入函数”是指可以由多个函数并发使用而不担心错误的函数。相反,不可重入函数是指只能由一个任务占有,除非能确保函数互斥。可重入函数可以在任意时刻被中断,稍后在继续运行,且不会丢失数据。

  • 可重入函数
  1. 不为连续调用持有静态数据
  2. 不返回指向静态数据的指针
  3. 所有数据由函数调用者提供
  4. 使用本地数据、制作全局数据副本来保护全局数据
  5. 如果必须访问全局数据,利用互斥锁、信号量
  6. 不调用任何不可重用函数
  • 不可重入函数
  1. 使用静态变量
  2. 返回静态变量
  3. 调用了不可重入函数
  4. 使用静态数据结构
  5. 调用了malloc或free函数
  6. 调用了其他IO函数