《操作系统导论》第三部分 并发 P1 并发简介,线程API

C1 并发

我们将为单个运行的进程提供新的抽象:线程

经典观点是一个程序只有一个执行点(一个程序计数器,用来存放要执行的指令),但多线程程序会有多个执行点(多个程序计数器,每个都用于取指令和执行),每个线程类似于独立的进程,但它们共享地址空间,从而能访问相同的数据

单个线程的状态与进程十分相似,但如果有多个线程,从一个线程切换到另一个线程必然要发生上下文切换,以切换存储该线程状态的线程控制块TCB,但与进程相比,线程间进行上下文切换时,地址空间不变,即不需要更换当前使用的页表

线程与进程另一个主要区别在于,传统进程(单线程)只有一个栈,而多线程中,在地址空间内,每个线程都有一个栈

在这里插入图片描述

1.1 创建线程

通过Java创建两个线程,分别打印各自的线程名和一个字符A或B
在这里插入图片描述
测试类:
在这里插入图片描述
上述的结果并不是唯一的,虽然在程序内先创建的是线程A,但是仍然有可能先运行线程B

在这里插入图片描述
线程的运行顺序并不是由创建的顺序决定,而是由调度程序决定,线程创建有点像进行函数调用,然而并不是首先执行函数然后返回给调用者,而是为被调用的例程创建一个新的执行线程,它可以独立于调用者运行,可能在创建者返回前运行,但也许会晚的多

上述的例子可以看到,线程让整个程序复杂,并发让结果难以确定

1.2 共享数据

再看一个简单的例子,两个线程希望能更新一个全局变量,每个线程每次对该变量加1,每个线程循环1000次,最终的期望的输出是2000

在这里插入图片描述
结果1:
在这里插入图片描述
但是多运行几次,可能会有下面的结果:
在这里插入图片描述
在这里插入图片描述
除了偶尔能获得预期的结果外,很多次的输出结果都是不确定且无序的

1.3 不可控的调度

上述示例结果的不可控性,其根本原因是竞争条件

竞争条件:结果取决于代码的执行时间,每个线程运行时间有固定的配额,当它的时间片被用尽后,调度程序会将它放入就绪态,将别的线程移至运行态,如果该线程运气不好(此刻正在更新变量的值却发生了上下文切换),那么就会得到错误的结果,因此结果是不确定的

由于执行这段代码的多个线程可能导致竞争状态,因此将此段代码称为临界资源临界资源就是访问共享变量的代码片段不能不设限制的让多个线程同时执行

为了得到期望的结果,需要互斥互斥保证了如果一个线程在临界区内执行,那么其它线程会被阻止进入临界区

1.4 原子性问题

为了解决整个问题,有一种途径是利用更强大的指令,消除不合时宜的中断

假设这条指令将一个值添加到内存位置,并且硬件保证它以原子方式执行,当指令执行时,它会像期望那样执行更新,它不能再指令中间中断

因此,需要使用同步原语,通过使用这些硬件的同步原语,加上操作系统的一些帮助,多线程将以同步和受控的方式访问临界区

1.5 小结

线程间的交互除了访问共享变量外,还有一种常见的交互,即多线程程序间中常见的睡眠/唤醒交互的机制

操作系统是第一个并发程序,因此页表,进程列表,文件系统结构及几乎每个内核数据结构都必须小心地访问,并使用正确的同步原语才能正常工作

C2 线程API

2.1 创建线程

在POSIX中创建线程的接口:

#include <pthread.h>

int pthread_create(
	pthread_t* thread,
	const pthread_attr_t* attr,
	void* (*start_routine)(void*),
	void* arg);

该函数有四个参数:
1,thread,指向了pthrad_t结构类型的参数,将利用这个结构与线程交互
2,attr,用于指定该线程可能具有的任何属性
3,指明该线程应该在哪个函数中运行
4,arg,传递给线程开始执行的函数的参数

当创建好一个线程后,它有自己的调用栈,与程序中所有当前存在的线程在相同的地址空间内运行

2.2 完成线程

创建好一个线程后,想等待该线程完成,必须调用函数

pthread_join()

该函数有两个参数:
1,第一个参数是phread_t类型,用于指定待完成的线程
2,第二个参数是一个指针,指向你希望得到的返回值

并非所有的多线程代码都使用join函数,如多线程Web服务器会创建大量工作线程,然后使用主线程接受请求,并将其无期限地传递给工作线程,这样地长期程序可能不需要使用join,然而,创建多线程执行特定任务的并行程序,很可能会使用join来确保在退出或进入下一阶段前完成这些工作

2.3 锁

除了creat和join外,POSIX线程库提供的最有用的函数集可能是通过锁来提供互斥进入临界区的一些函数,最基本的一对是:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread mutex_t *mutex);

如果有一段带代码是临界区,就需要意识到需要用锁保护:

pthread_mutex_t lock;
pthread_mutex_lock(&lock);
x = x + 1; //临界区代码
pthread_mutex_unlock(&lock);

这段代码的意思是:如果在调用pthread_mutex_lock()时没有其它线程持有锁,线程将获取该锁进入临界区,如果另一个线程已经持有了该锁,那么当前视图获取该锁的线程阻塞,即在获取锁的函数内部等待,直到持有锁的线程解锁后,这些被阻塞的线程才有可能获得锁

还有一对主动获取锁的函数:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex, struct timespec *abs_timeout);

这两个函数用于获取锁,如果锁已经被占用,则trylock失败,获取锁的timedlock会在超时或获取锁后返回,通常应该避免使用这两个函数,但有些情况下,避免卡在获取锁(无限期的)获取锁的函数中会很有用

2.4 条件变量

当线程之间必须发生某种信号时,如果一个线程在等待另一个线程继续执行某些操作时,条件变量就会很有用

int pthread_cond_wait(pthread_cont_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);

要使用条件变量,必须另有一个与此条件相关的锁,在调用上述任何一个函数时,调用线程应该持有这个锁

第一个函数pthread_cond_wait()使调用线程进入休眠状态,第二个函数pthread_cond_signal()唤醒阻塞的线程

2.5 小结

想要写出健壮高效的多线程代码,需要耐心和小心,多线程程序,难的不是API,而是如何构建并发程序的逻辑

猜你喜欢

转载自blog.csdn.net/weixin_43541094/article/details/110541892
今日推荐