许多时候,在执行一个python文件的时候,会发现在同一目录下会出现一个__pyc__文件夹(python3)或者.pyc后缀(python2)的文件
Python在执行时,首先会将.py文件中的源代码编译成Python的byte code(字节码),然后再由Python Virtual Machine(Python虚拟机)来执行这些编译好的byte code。
源代码.py ——(编译处理)——>字节码.pyc ———>python虚拟机——(编译)——>程序2、编译
执行 python demo.py 后,将会启动 Python 的解释器,然后将 demo.py 编译成一个字节码对象 PyCodeObject。
在 Python 的世界中,一切都是对象,函数也是对象,类型也是对象,类也是对象(类属于自定义的类型,在 Python 2.2 之前,int, dict 这些内置类型与类是存在不同的,在之后才统一起来,全部继承自 object),甚至连编译出来的字节码也是对象,.pyc 文件是字节码对象(PyCodeObject)在硬盘上的表现形式。
在运行期间,编译结果也就是 PyCodeObject 对象,只会存在于内存中,而当这个模块的 Python 代码执行完后,就会将编译结果保存到了 pyc 文件中,这样下次就不用编译,直接加载到内存中。pyc 文件只是 PyCodeObject 对象在硬盘上的表现形式。
这个 PyCodeObject 对象包含了 Python 源代码中的字符串,常量值,以及通过语法解析后编译生成的字节码指令。PyCodeObject 对象还会存储这些字节码指令与原始代码行号的对应关系,这样当出现异常时,就能指明位于哪一行的代码。
一个 pyc 文件包含了三部分信息:Python 的 magic number、pyc 文件创建的时间信息,以及 PyCodeObject 对象。
magic number 是 Python 定义的一个整数值。一般来说,不同版本的 Python 实现都会定义不同的 magic number,这个值是用来保证 Python 兼容性的。比如要限制由低版本编译的 pyc 文件不能让高版本的 Python 程序来执行,只需要检查 magic number 不同就可以了。由于不同版本的 Python 定义的字节码指令可能会不同,如果不做检查,执行的时候就可能出错。
4、字节码指令为什么 pyc 文件也称作字节码文件?因为这些文件存储的都是一些二进制的字节数据,而不是能让人直观查看的文本数据。
Python 标准库提供了用来生成代码对应字节码的工具 dis。dis 提供一个名为 dis 的方法,这个方法接收一个 code 对象,然后会输出 code 对象里的字节码指令信息。
# test1.pyimport dis def add(a): a = a+1 return a print(dis.dis(add))# 输出 10 0 LOAD_FAST 0 (a) 3 LOAD_CONST 1 (1) 6 BINARY_ADD 7 STORE_FAST 0 (a) 11 10 LOAD_FAST 0 (a) 13 RETURN_VALUE5、python虚拟机
demo.py 被编译后,接下来的工作就交由 Python 虚拟机来执行字节码指令了。Python 虚拟机会从编译得到的 PyCodeObject 对象中依次读入每一条字节码指令,并在当前的上下文环境中执行这条字节码指令。我们的程序就是通过这样循环往复的过程才得以执行。
更多请参考谈谈 Python 程序的运行原理
推荐python虚拟机与java虚拟机
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。需要强调的是:同一个程序执行两次,那也是两个进程。
进程:资源管理单位(容器)。
线程:最小执行单位,管理线程的是进程。
进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
2、线程线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。
线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程。
多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,控制该进程的地址空间。
进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)CPU分给线程,即真正在CPU上运行的是线程。
比较重要的就是,无论是并行还是并发,在用户看来都是'同时'运行的,而一个cpu同一时刻只能执行一个任务。
并行:同时运行,只有具备多个cpu才能实现并行。
并发:是伪并行,即看起来是同时运行,单个cpu+多道技术。
多道技术:内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,并且切换时间十分短暂,所以给人的感觉是我可以边打游戏边听歌。多个程序并行执行,其实是伪并行即并发。
阮一峰关于线程进程更形象介绍传送门:
5、同步与异步同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
打电话时就是同步通信,发短息时就是异步通信。
6、生产者与消费者生产者消费者模式:
某些模块负责生产数据,这些数据由其他模块来负责处理(此处的模块可能是:函数、线程、进程等)。产生数据的模块称为生产者,而处理数据的模块称为消费者。在生产者与消费者之间的缓冲区称之为仓库。生产者负责往仓库运输>商品,而消费者负责从仓库里取出商品,这就构成了生产者消费者模式。
比如在网络I/O的时候,一个对象负责请求数据,另一个对象负责处理数据,中间就需要一个容器来负责数据的缓冲,平衡两个对象之间的处理速度的协调。
优点:
解耦:由于两个对象之间的方法独立,数据的获取只需要通过接口的调用,所以两者的依赖性低,可重用性高
平衡了生产力与消费力,就是生产者一直不停的生产,消费者可以不停的消费,因为二者不再是直接沟通的,而是通过数据缓冲区沟通的。生产者的数据直接丢入缓冲区,消费者直接从缓冲区那数据,就不会造成因为数据因为过剩造成生产者阻塞,或者数据过少消费者阻塞的问题
举例
男生:我负责挣钱养家,你呢?
女生:我负责貌美如花。
男生:那如果钱不够?
女生:那就等钱够了再娶我,我等着!
男生:如果钱太多呢?
女生:那就存着,我慢慢花!
从上面可以抽象出三个对象,生产者(男生),消费者(女生),数据(钱),而数据暂存到哪,一般是为了解决加锁问题,放到队列而不是简单的容器类型。
三、全局解释器锁全局解释器锁(Global Interpreter Lock):简称GIL,多进程(mutilprocess) 和 多线程(threading)的目的是用来被多颗CPU进行访问, 提高程序的执行效率。 但是多线程之间数据完整性和状态同步是一个很大的问题,所以在python内部存在一种机制(GIL),在多线程时同一时刻只允许一个线程来访问CPU,也就是不同线程对共享资源的互斥。 在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。GIL 并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。因为CPython是大部分环境下默认的Python执行环境。所以在把GIL之殇归结给Python是不对的。GIL并不是Python的特性,Python完全可以不依赖于GIL。例如Jython(java编写的python解释器)就不会存在GIL。
python中一个线程对应于c语言中的一个线程
GIL使得同一个时刻只有一个线程在一个CPU上执行字节码, 无法将多个线程映射到多个cpu上执行,因此python是无法利用多核CPU实现多线程的
大量的第三方包都是基于CPython编写的,所以短期内想把GIL去掉不太可能
缺点:多处理器退化为单处理器;
优点:避免大量的加锁解锁操作
要实现python的多线程就需要借助标准库threading
# test2.pyimport threading total = 0def add(): # 连续执行total的加操作 global total for i in range(1000000): total += 1def reduce(): # 连续执行total的减操作 global total for i in range(1000000): total -= 1# 创建两个线程thread1 = threading.Thread(target=add) thread2 = threading.Thread(target=desc)# 线程开始thread1.start() thread2.start()# 线程结束thread1.join() thread2.join() print(total)
使用total作为标志,通过total的值判断线程的实现。
如果实现GIL没有释放的的话,那么两个线程先后完成,打印结果应该是0,而实际打印结果却不是0,并且每次打印结果也都不一致,说明实现了GIL主动释放掉了。
total变量是一个全局变量,其实在add与reduce内部的赋值语句total+=1与total-=1时,高级语言每一条语句在CPU上执行的时候又被对应成许多语句,比如total+=1对应成x1=total+1,total=x1,而total-=1被对应成x2=toal-1,total=x2,每一个x都是函数内部的局部变量。
注:可以对应字节码指令来理解,可以参照上面GIL中的实例使用dis模块获取字节码查看,PVM(python虚拟机)其实执行的也就是字节码指令。。
正常执行:
初始total=0add:x1 = total +1 # x1 = 1total = x1 total = 1reduce:x2 = total-1 # x2 = 0total = x2 total = 0 最终循环一次结果0 正常应该是无论多少次循环结果total都是0
多线程共享变量,两个线程交替占用cpu,:
total=0add:x1 = total + 1 # x1 = 1reduce:x2 = total - 1 # x2 = -1total = x2 # total = -1add:total = x1 total =1 最终循环结果为1 只要进行足够多的循环,total的值就会出现不可预计的结果
所以,在修改total值的时候,需要多条语句。所以我觉得上面的例子可以这么理解:就是当一个线程在执行的时候也就是PVM在执行字节码指令,当字节码指令到达一定数目(ticks专门计数),此线程不再拥有GIL(释放GIL,release)并且释放CPU资源,但是其他的线程又过来抢,这个线程没抢过它,GIL就这样别抢走了,CPU资源就暂时交给其他的线程了(嗯,天道有轮回,下次我还会抢回来的)。因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。所以在进行python多线程变成的时候,一般会进行细粒度的自定义加锁,以保证安全性。
问题:GIL什么时候会释放?
执行的字节码行数到达一定阈值
通过时间片划分,到达一定时间阈值
在遇到IO操作时,主动释放
更多请参考python GIL详讲:传送门
原文出处:https://www.cnblogs.com/welan/p/10009627.html



