程序员基础,性能测试需要知道的一些底层概念(一)
进程、线程、程序
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元
,线程和进程都是由系统内核管理。
进程
定义
程序被加载到内存中并准备执行,就是一个进程,它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
关于进程有两点需要注意:
一、进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量;
二、进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
进程状态
进程有三个状态,就绪-运行-阻塞
。就绪状态其实就是获取除CPU之外的所有资源,只要处理器分配资源就可以马上执行;运行状态就是获取了处理器分配的资源,开始运行程序;阻塞状态,当程序条件不够时,需要等待条件满足才能继续执行,例如I/O。
程序
程序其实本身没有任何运行的含义,程序是指令和数据的有序集合。
线程
定义
单个进程中执行中的每个任务就是一个线程,线程是进程中执行运算的最小单位。一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
进程与线程的区别
1,线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程的文本片段中。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,寄存器的内容(栈段又叫运行时段,用来存放所有局部变量和临时变量)。
2,线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
3,从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
4,线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
(进程就像火车,线程就是火车的车厢)
多进程与多线程
多线程
多线程,顾名思义,同时运行多个线程;多任务可以由多进程完成,也可以由一个进程内的多线程完成。
线程状态
首先,线程大致有五种状态:初始化,就绪,运行,阻塞,死亡;
线程锁
在多线程中,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。例如:有两个线程,同时操作一个列表, 列表中数值都是0,一个线程是更新(update),操作为从前往后顺序更新值为1;线程二为打印(print),操作为从后往前倒序打印列表,当两个线程同时操作列表时就会出现打印时一半0,一半1,出现了数据不同步,为了避免这样的情况,就引进了线程锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如”update”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”update”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”update”继续。经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的混乱数据。
线程通信(条件变量)
列表并不是一开始就有的;而是通过线程”create”创建的。如果”update”或者”print” 在”create”还没有运行的时候就访问列表,将会出现一个异常。使用锁可以解决这个问题,但是”update”和”print”将需要一个无限循环——他们不知道”create”什么时候会运行,让”create”在运行后通知”update”和”print”显然是一个更好的解决方案。于是,引入了条件变量。
条件变量允许线程比如”update”和”print”在条件不满足的时候(列表为None时)等待,等到条件满足的时候(列表已经创建)发出一个通知,告诉”update” 和”print”条件已经有了,你们该起床干活了;然后”update”和”print”才继续运行。
线程运行和阻塞状态转换
阻塞有三种状态:
同步阻塞(锁定池)是指处于竞争锁定的状态,线程请求锁定时将进入这个状态,一旦成功获得锁定又恢复到运行状态;
等待阻塞(等待池)是指等待其他线程通知的状态,线程获得条件锁定后,调用“等待”将进入这个状态,一旦其他线程发出通知,线程将进入同步阻塞状态,再次竞争条件锁定;
而其他阻塞是指调用time.sleep()、anotherthread.join()或等待IO时的阻塞,这个状态下线程不会释放已获得的锁定。
多进程
为啥会想看多进程这么个东西,那是因为,python说实话,有点坑的地方是,实质上python没有多线程。python虽然可以通过threading
模块来实现多线程编程,但是Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。而通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。所以使用python实现并发多线程,不太现实。。
不过即使如此,我们也可以使用多进程来实现多核任务,毕竟当前多核处理器满大街都是。
实现多进程
首先,unix中有个fork()函数,这玩意就可以实现多进程,这函数产生的效果就是把当前进程复制一份,普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
python的os模块封装了fork(),可以很简单就实现创建子进程;但是由于windows系统没有fork(),在windows上就不能执行包含fork()调用的脚本,在windows上就用multiprocessing
替代。
multiprocessing 示例
使用multiprocessing
模块进行多进程编程,可以支持跨平台。
1 | #!/usr/bin/env python3 |
执行结果:
1 | Parent process 1692 |
进程间通信
multiprocessing提供多种方法来交换数据:Queue
,Pipes
等。
代码示例:
1 | #!/usr/bin/env python3 |
运行结果:
1 | Process to read: 7616 |
以上代码均来自于网络~
协程
协程,又称微线程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。(关于协程,真的不太懂,之后有空再研究,看到一句话:子程序就是协程的一种特例)多线程编程中,为了防止数据不同步,需要添加锁机制,但是协程应为就用了一个线程,在协程之间共享资源是不用加锁机制的,只需要判断状态。