文章目录
前言
进程是操作系统(Operating System)结构的基础。
一、进程(Process)
操作系统执行main函数的程序,机器指令需要加载到内存中执行,因此需要记录下内存的起始地址和长度;同时要找到main函数的入口地址写到PC寄存器中。
- 与协程的区别
当普通函数返回后,进程的地址空间中不会再保存该函数运行时的任何信息,而协程返回后,函数的运行时信息是需要保存下来的
二、线程
PC寄存器指向的非main函数,存在互斥和同步问题
- 线存共享进程的内存地址空间
- 操作系统要为每个线程在进程的地址空间中分配一个栈(stack),消耗进程内存空间
1.线程池
一批创建后不再释放的线程,复用,可控,包括:
- 处理数据的函数;
- 需要处理的数据;
代码如图
Struct task{
Data; //数据
Handler;//方法
}
- 线程池中的线程会阻塞在队列上。
- 当生产者向队列中写入数据后,线程池中的某个线程会被唤醒,该线程从队列中取出上述结构体执行。
2.线程安全
2.1.共享资源有哪些?
- 用于动态分配内存的堆区,我们用C/C++中的malloc或者new就是在堆区上申请的内存。
- 全局区(数据区),这里存放的就是全局变量。
- 文件,我们知道线程是共享进程打开的文件。
- 代码区和动态链接库是只读的,不能被修改。
2.2.识别线程核心资源
- 线程私有资源,没有线程安全问题
- 线程间以某种秩序使用共享资源也能实现线程安全
2.3.什么是线程安全的代码?
线程安全的代码:
- 无状态函数
- 当我们在多个线程中同时且多次调用的这段代码,都能给出正确的结果
- 按值传参,参数是线程的私有资源
线程不安全的代码:
- 按引用传参,参数在数据区或者是全局资源
2.4.线程(进程)的同步、异步
-
什么是回调函数?
在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。 -
案例:
假如有一个主线程,一个副线程。- 同步:主线程等待副线程执行完毕后,才继续执行。
- 异步:将主线程在副线程执行完后才执行的部分,封装为一个函数,将函数传入副线程。如果主线程不关心副线程的执行情况,副线程就会一直执行下去。如果主线程关心副线程的执行情况,主线程在接收到消息后,继续处理上一个请求的后半部分。
三.协程
函数只是协程的一种特例,函数其实只是没有挂起点的协程。
与普通函数只有一个返回点(yield)不同,协程可以有多个返回点。
协程返回后还能继续调用该协程,并且从该协程的上一个返回点后继续执行。
代码:
void func(){
print("a")
yield
print("b")
yield
print("c")
}
def A():
co = func() # 得到该协程
next(co) # 调用该协程
print("in function A") # do something
next(co) # 再次调用该协程
协程会在函数被暂停运行时,保存函数的运行状态,并可以从保存的状态中恢复并继续运行,类似操作系统对线程的调度。
3.1.怎么实现协程?
假设进程中只有一个线程:
栈区中有四个栈帧,main函数调用A函数,A函数调用B函数,B函数调用C函数,函数的运行时状态就保存在栈区的栈帧中(上下文),两个协程的栈区都是在堆上分配的,这样我们就可以随时中断或者恢复协程的执行了。
一个线程中,即使你创建了N多协程,但在操作系统看来依然只有一个线程,协程对操作系统来说是不可见的。
四.数据结构
4.1.树的递归遍历
函数的调用过程具有数据结构中栈的性质,也就是先进后出。
void C() {
}
void A() {
B();
}
void B() {
C();
}
void main() {
A();
}
A()会调用B(),B()会调用C(),那么函数调用过程如图所示:
4.2.链表
链表是计算机科学中极其经典的一种数据结构,货车就好比数组,火车就好比链表。
struct node {
struct node* next; // 下一节车厢是谁 (内存地址)
int value; // 装载的货物
};
五.CPU执行函数指令
假设函数A调用了函数B,函数A将一些参数写入相应的寄存器,当CPU执行函数B时就可以从这些寄存器中获取参数了,当参数个数多于寄存器数量时剩下的参数直接放到栈帧中,这样被调函数就可以从前一个函数的栈帧中获取到参数了
5.1.栈溢出
栈区是有大小限制的,当超过限制后就会出现栈溢出问题。
- 不要创建过大的局部变量(函数内部变量)
- 函数栈帧,也就是调用层次不能太多
总结
了解到进程相关的概念后,下一章讲讲操作系统及内核。