Python 多线程、多进程 (一)之 源码执行流程、GIL

Python 多线程、多进程 (二)之 多线程、同步、通信

Python 多线程、多进程 (三)之 线程进程对比、多线程

一、python程序的运行原理

许多时候,在执行一个python文件的时候,会发现在同一目录下会出现一个__pyc__文件夹(python3)或者.pyc后缀(python2)的文件

Python在执行时,首先会将.py文件中的源代码编译成Python的byte code(字节码),然后再由Python Virtual Machine(Python虚拟机)来执行这些编译好的byte code。

1、执行流程

源代码.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 对象还会存储这些字节码指令与原始代码行号的对应关系,这样当出现异常时,就能指明位于哪一行的代码。

3、pyc文件

一个 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.py

import 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_VALUE

5、python虚拟机

demo.py 被编译后,接下来的工作就交由 Python 虚拟机来执行字节码指令了。Python 虚拟机会从编译得到的 PyCodeObject 对象中依次读入每一条字节码指令,并在当前的上下文环境中执行这条字节码指令。我们的程序就是通过这样循环往复的过程才得以执行。

二、进程线程

1、进程

程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。需要强调的是:同一个程序执行两次,那也是两个进程。

进程:资源管理单位(容器)。

线程:最小执行单位,管理线程的是进程。

进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

2、线程

线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。

线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。

3、线程与进程关系

在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程。

多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,控制该进程的地址空间。

进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

进程和线程的关系:

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。

(3)CPU分给线程,即真正在CPU上运行的是线程。

4、串行,并行与并发

比较重要的就是,无论是并行还是并发,在用户看来都是'同时'运行的,而一个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去掉不太可能

1、GIL优缺点

缺点:多处理器退化为单处理器;

优点:避免大量的加锁解锁操作

2、GIL释放

要实现python的多线程就需要借助标准库threading

# test2.py

import threading

total = 0

def add():
# 连续执行total的加操作
global total
for i in range(1000000):
total += 1 def 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=0

add:
x1 = total +1 # x1 = 1
total = x1
total = 1 reduce:
x2 = total-1 # x2 = 0
total = x2
total = 0 最终循环一次结果0
正常应该是无论多少次循环结果total都是0

多线程共享变量,两个线程交替占用cpu,:

total=0

add:

x1 = total + 1  # x1 = 1

reduce:
x2 = total - 1 # x2 = -1
total = x2 # total = -1 add:
total = x1
total =1 最终循环结果为1
只要进行足够多的循环,total的值就会出现不可预计的结果

所以,在修改total值的时候,需要多条语句。所以我觉得上面的例子可以这么理解:就是当一个线程在执行的时候也就是PVM在执行字节码指令,当字节码指令到达一定数目(ticks专门计数),此线程不再拥有GIL(释放GIL,release)并且释放CPU资源,但是其他的线程又过来抢,这个线程没抢过它,GIL就这样别抢走了,CPU资源就暂时交给其他的线程了(嗯,天道有轮回,下次我还会抢回来的)。因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。所以在进行python多线程变成的时候,一般会进行细粒度的自定义加锁,以保证安全性。

问题:GIL什么时候会释放?

  • 执行的字节码行数到达一定阈值
  • 通过时间片划分,到达一定时间阈值
  • 在遇到IO操作时,主动释放

关于GIL,强烈推荐参阅Understand GIL:[传送门],在Understand中作者在Python2.X的环境中队多核CPU,单核CPU上,多线程以及单线程做了详细对比,并且对CPython的线程执行做了详细的跟踪,从根本上解释了GIL对python 多线程编程的影响和GIL的趋势。虽然英文原版,但是除了一些英文术语词汇,没有太难的句子,对英语渣还是很友好的。

有了预备知识来看下一篇吧,Python 多线程、多进程 (二)之 多线程、同步、通信

Python 多线程、多进程 (一)之 源码执行流程、GIL的更多相关文章

  1. mybatis源码专题(2)--------一起来看下使用mybatis框架的insert语句的源码执行流程吧

    本文是作者原创,版权归作者所有.若要转载,请注明出处.本文以简单的insert语句为例 1.mybatis的底层是jdbc操作,我们先来回顾一下insert语句的执行流程,如下 执行完后,我们看下数据 ...

  2. java架构之路-(SpringMVC篇)SpringMVC主要流程源码解析(上)源码执行流程

    做过web项目的小伙伴,对于SpringMVC,Struts2都是在熟悉不过了,再就是我们比较古老的servlet,我们先来复习一下我们的servlet生命周期. servlet生命周期 1)初始化阶 ...

  3. (3)一起来看下使用mybatis框架的select语句的源码执行流程吧

    本文是作者原创,版权归作者所有.若要转载,请注明出处.本文以简单的select语句为例,只贴我觉得比较重要的源码,其他不重要非关键的就不贴了 主流程和insert语句差不多,这里主要讲不同的流程,前面 ...

  4. 深入Mybatis源码——执行流程

    前言 上一篇分析Mybatis是如何加载解析XML文件的,本篇紧接上文,分析Mybatis的剩余两个阶段:代理封装和SQL执行. 正文 代理封装 Mybatis有两种方式调用Mapper接口: pri ...

  5. Mybatis底层源码执行流程

    1.通过类加载器,加载了config.xml文件 2.通过SqlSessionFactoryBuilder.build(resource)这个方法进行了config.xml的解析,解析为Configu ...

  6. java多线程系列(九)---ArrayBlockingQueue源码分析

    java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...

  7. Python之Django rest_Framework框架源码分析

    #!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import APIView from rest_fram ...

  8. Python多线程多进程那些事儿看这篇就够了~~

    自己以前也写过多线程,发现都是零零碎碎,这篇写写详细点,填一下GIL和Python多线程多进程的坑~ 总结下GIL的坑和python多线程多进程分别应用场景(IO密集.计算密集)以及具体实现的代码模块 ...

  9. 详细解析Thinkphp5.1源码执行入口文件index.php运行过程

    详细解析Thinkphp5.1源码执行入口文件index.php运行过程 运行了public目录下的index.php文件后,tp的运行整个运行过程的解析 入口文件index.php代码如下: < ...

随机推荐

  1. cad.net之ACAD移植到GCAD的自动加载问题

    将acad.pgp,lsp,fas,vlx,名称增加一份gcad.pgp,lsp,fas,vlx.涉及系统加载用. Lisp的拖拉加载在gcad无法通过lastprompt获取命令历史栏最后一行(含路 ...

  2. Spring Boot中使用Flyway来管理数据库版本

    flyway是一个开源的数据库迁移工具.类似于数据库的版本控制工具.flyway的数据库修改文件默认放在resource下的db.migration文件夹中,以V{version_number}__{ ...

  3. CSS之浏览器默认样式问题

    今天自己写css样式时,其中用到了<ul>标签,设置了一系列效果后运行,发现位置与设置有出入.chrome上打开检查项,发现<ul>标签的styles底部多了以下一段: ul, ...

  4. WebSocket集成XMPP网页即时通讯3:二进制文件收发

    WebSocket支持二进制的发送,见jetty官网: http://www.eclipse.org/jetty/documentation/current/jetty-websocket-api-s ...

  5. Python程序的打包-上传到pypi

    pypi注册与配置 在pypi的官网:https://pypi.python.org/pypi 注册自己的账号激活账号之后,我们还需要将在本地配置一份文件 在用户的根目录创建文件 : .pypirc在 ...

  6. Shell 流程控制 if while for

    if else if if 语句语法格式: if condition then command1 command2 ... commandN fi 写成一行(适用于终端命令提示符): if [ $(p ...

  7. Javac常量池的解读

    interface IA{ public void md(); } public class Test07 implements IA{ final double d = 2.0d; final fl ...

  8. sql的存储过程实例--循环动态创建表

    创建一个存储过程,动态添加100张track表表名track_0 ~~ track_99注:sql的拼接只能用 CONCAT()函数 -- 创建一个存储过程 CREATE PROCEDURE crea ...

  9. 分布式理论(八)—— Consistent Hash(一致性哈希算法)

    前言 在分布式系统中,常常需要使用缓存,而且通常是集群,访问缓存和添加缓存都需要一个 hash 算法来寻找到合适的 Cache 节点.但,通常不是用取余hash,而是使用我们今天的主角-- 一致性 h ...

  10. 记一次Java AES 加解密 对应C# AES加解密 的一波三折

    最近在跟三方对接 对方采用AES加解密 作为一个资深neter Ctrl CV 是我最大的优点 所以我义正言辞的问他们要了demo java demo代码: public class EncryptD ...