小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

栏目: Python · 发布时间: 6年前

内容简介:小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

引言:

从本节开始的连续几节我们都会围绕着 Python并发 进行学习, 本节学习的是 threading 这个 线程相关模块 ,附上官方文档: docs.python.org/3/library/t… 跟官方文档走最稳健 ,网上的文章都是某一时期的产物,IT更新 换代那么快,过了一段时间可能就改得面目全非了,然后你看了 小猪现在的文章然后写代码,这不行那不行就开始喷起我来了,我表示

小猪的 <a href='https://www.codercto.com/topics/20097.html'>Python</a> 学习之旅 —— 7.Python并发之threading模块(1)

另外,在查阅相关资料的时候发现很多文章还是用的 thread模块 , 在高版本中已经使用 threading 来替代thread了!!!如果你在 Python 2.x版本想使用threading的话,可以使用 dummy_threading 话不多说开始本节内容~

1.threaing模块提供的可直接调用函数

  • active_count ():获取当前活跃(alive)线程的个数;
  • current_thread ():获取当前的线程对象;
  • get_ident ():返回当前线程的索引,一个非零的整数;(3.3新增)
  • enumerate ():获取当前所有活跃线程的列表;
  • main_thread ():返回主线程对象,(3.4新增);
  • settrace (func):设置一个回调函数,在run()执行之前被调用;
  • setprofile (func):设置一个回调函数,在run()执行完毕之后调用;
  • stack_size ():返回创建新线程时使用的线程堆栈大小;
  • threading.TIMEOUT_MAX :堵塞线程时间最大值,超过这个值会栈溢出!

2.线程局部变量(Thread-Local Data)

先说个知识点:

在一个进程内所有的 线程共享进程的全局变量 ,线程间共享数据很方便 但是每个线程都可以 随意修改全局变量 ,可能会引起 线程安全问题 , 这个时候,可以对全局变量进行 加锁 来解决。对于 线程私有数据 可以 通过使用 局部变量 ,只有线程自身可以访问,其他线程无法访问, 除此之外,Python还给我们提供了 ThreadLocal变量 ,本身是一个全局 变量,但是线程们却可以使用它来 保存私有数据

用法也很简单,定义一个全局变量: data = thread.local() ,然后就可以 往里面存数据啦,比如data.num = xxx,写个简单例子来验证下: :如果data没有设置对应的属性,直接取会报 AttributeError 异常, 使用时可以捕获这个异常,或者先调用**hasattr(对象,属性)**判断对象中 是否有该属性!

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

输出结果:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

厉害了,不同线程访问果然是返回的不同值,小猪这种求知欲 旺盛的人肯定是要扒一波看看是怎么实现的啦,跟源码会比较 枯燥,先简单说下实现套路:

threading.local()实例化一个 全局对象 ,这个全局对象里有 一个 大字典 ,键值为两个 弱引用对象 {线程对象,字典对象}, 然后可以通过 current_thread() 获得当前的线程对象,然后根据 这个对象可以拿到对应的 字典对象 ,然后进行参数的读或者写。

是的大概套路就是这样,接下来就是剖析源码环节了,挺枯燥的, 可以不看,看的话,相信你会收获非常多,小猪昨天下午开始看 _threading_local.py 这个模块的源码,仅仅246行,却看到了晚上 十点才舍得回家,收益颇丰,Get了N多知识点,至少在那些什么 Python教程里没看到过,每弄懂一个都会忍不出发出:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

这样的感叹!快上老司机小猪的车吧,上车只需五个滑稽币:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

*3._threading_local源码解析

按住ctrl点local()方法,会进到threading.py模块,会定位到这一行:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

_thread模块上节也说了threading模块的基础模块,应该尽量使用 threading 模块替代,而我们代码里也没导入这个模块,所以会走 _threading_local ,点进去看下这个模块,246行代码,不多,嘿嘿, 点击PyCharm左侧的 Structure看看代码结构

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

关注点在**_localimpl local**两个类上,我们先把这个模块的源码 全选,然后新建一个Python文件,把内容粘贴到里面,为什么要 这样做呢?

答:因为这样方便我们进行代码执行跟踪啊,Debug调试 或打Log跟踪方法运行顺序,或者查看某个时刻某些变量的值!

很多小伙伴可能只会print不会使用Debug调试,这里顺道简单 介绍下怎么用,掌握这个对跟源码非常有用,务必掌握!!!

1.PyCharm调试速成

点击左侧边栏可以 下断点 ,在调试模式下运行的话,运行到 这一行的时候会暂时挂起,并激活调试器窗口:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

点击顶部的小虫子标记即可进入调试模式:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

运行到我们埋下断点的这一行后,就会挂起并激活下面这个 调试器窗口:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

MainThread这个表示 当前断点上的线程 ,下面是 该线程的堆栈帧 右侧Variables是变量调试窗口 ,可以查看此时的变量情况! 接着就来一一说下一些调试技巧吧:

单步调试,

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
Step Over(F8)

,程序向下执行一行,如果该行 函数被调用,直接执行完返回,然后执行下一行;

当单步调试执行到某一个函数,如果你不想直接运行完,切到下 一行而是想看进去这个函数的运行过程的话,可以点击

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
Step Into(F7)

上面这一步,遇到官方类库的函数也会进去,如果只想在碰到 自己定义函数才进去的话,可以点击

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
Step Into My Code(Alt + Shift + F7)

进入函数后确定没什么问题了,可以点击

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
Step Out(Shift + F8)

跳出这个函数,返回该函数被调用处的下一行语句。

如果想快速执行到下一个断点的位置,可以点击

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
Run to Cursor(Alt + F9)

跨断点调试,点击左侧栏的:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

,直接跳过当前断点, 进入下一个断点。

监视变量,有时右侧 Variables ,显示的变量有很多时,而你 想关注某一个变量而已,可以点击这个小眼镜:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
,然后 输入你想监视的变量名,如果名字太长或者懒,可以直接右键 变量, Add To Watches 即可!不想监视时可右键 Remove Watch

停止调试,点击左侧红色按钮即可跳过调试,不是停止程序!:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

断点设置,点击左侧:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

,可以打开断点设置窗口,可以在此 看到所有的断点,设置条件断点(满足某个条件时,暂停程序执行), 删除断点,或者临时禁用断点等。

好的,关于PyCharm调试就先说这么多,基本够用了, 回到我们的源码,我们使用了threading.local()初始化了实例, 按照我们第一节学的类内容,类会走 构造函数 __init__() 对吧? 然而,在local类里,并没有发现这个函数,只有一个 __new__(cls, *args, **kw) , 这又是一个新的知识点了!

2.Python中的经典类和新式类

在Python 2.x中默认都是经典类,除非显式继承object才是新式类; 而在Python 3.x中默认都是新式类,不用显式继承object; 新式类相比经典类增加了很多内置属性,比如** __class__ ** 获得自身类型(type),** __slots__ **内置属性,还有这里的 new ()函数等。

3. __new__() 函数

在调用** init ()方法前, new (cls, args, kw) 可决定是否使用该 init ()方法,可以 调用其他类的构造方法或者直接返回别的对象 来作为 本类的实例cls表示需要实例化的类 ,该参数在实例化时由 Python解释器自动提供。另外还要注意一点, new 必须有返回值, 可以返回 父类__new__()出来的实例 object的__new__()出来的实例 如果__new__()没有成功返回 cls类型的对象 ,是不会调用 * init **() 来对对象进行初始化的!!!

卧槽,骚气,代码里也刚好这样做了,返回的是一个**_localimpl()**对象:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

直接实例化的**_localimpl() ,然后设置了 localargs**, locallock 以及调用了 create_dict() 方法。先定位到_localimpl类的 localargs

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

又触发新知识点: 黑魔法__slots__

4.Python黑魔法 __slots__ 内置属性

作用是 阻止在实例化类时为实例分配dict ,使用这个东西会带来: 更快的属性访问速度减少内存消耗 。此话怎么说?

默认情况下,Python的类实例都会有一个** dict 来存储实例的属性, 注意: 只保存实例的变量,不会保存类属性 !!! 可以调用内置属性 dict **进行访问,比如下面的例子:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

输出结果:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

看上去是 挺灵活的 ,在程序里可以随意 设置新属性 ,只是每次 实例化类对象的时候,都需要去分配一个 新的dict ,如果是对于 拥有固定属性的class 来说,这就有点浪费内存了,特别是在需要 创建大量实例的时候,这个问题就尤为突出了。Python在新式类中给 我们提供了** slots 属性来帮助我们解决这个问题。 slots 是一个 元组 ,包括了当前能访问到的属性,定义后 slots中定义的变量变成了 类的描述符**,相当于 java 里的成员变量 声明, 不能再增加新的变量 。还有一点要注意: 定义定义了__slots__后,就不再有__dict__ !!!可以写个例子验证下:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

输出结果:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

Python内置的 dict(字典) 本质是一个哈希表, 通过空间换时间 , 在实例化对象达到万级,和** slots 元组**对比耗费的内存就不是 一点半点了!另外属性访问速度也是比dict快的,相关对比以及 更多内容可见: www.cnblogs.com/rainfd/p/sl… 和: Saving 9 GB of RAM with Python’s slots

了解完** slots 后,我们回到我们的源码,回到_localimpl的 init ()**

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

设置了一个key,规则是: _threading_local._localimpl. 拼接上 对象所在的内存地址 这里的 id()函数作用是获得对象的内存地址 。接着初始化了一个 dicts大词典 , 拿来存放键值对的: (弱引用的线程对象,该线程在_localimpl对象里对应的数据字典) 就是每个线程对象,对应_localimp里不同的字典对象,这些字典对象都放在 大字典里。

接着回到 local类 的** new ()** 函数,这里是一个设置属性的方法:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

_local__impl属性在上面通过** slots **定义了

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

简单点理解就是为local设置了一个**_localimpl 对象,后面 可以根据根据这个name = _local__impl 拿到对应的 _localimpl**对象!

而且这里没那么简单,local类里对这个函数进行了重写:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

这里前面判断name是否为__dict__,猜测是权限控制,不允许 外部通过** setattr delattr **来操作字典,只允许通过 **_patch()**方法来修改操作字典!

接着继续来跟下**_patch()**方法:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

@contextmanager又是什么东西???

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

又是新的知识点~

5.@contextmanager

这就涉及到我们以前学习的 with结构 了,在爬虫写入文件那里用过, 不用自己写finally,然后在里面去close()文件,以避免不必要的错误, 不知道你还记不记得,不记得的话回头翻翻吧。

对于类似于文件关闭这种不想遗忘的重要操作,我们可以自己封装 一个with结构来进行处理,封装也很简单,再定义你那个类的时候 重写** enter 方法和 exit **方法,比如文件关闭那个可以自定义 成这样的:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

如果觉得上面这种实现起来比较麻烦的话,就可以用 @contextmanager 啦,直接就一个方法,比定义类简单多了~

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

知道@contextmanager之后,继续来分析**_patch( )方法,先根据 _local__impl 这个值拿到了local里的 _localimpl 对象,然后 调用impl的 get_dict()**想获得一个数据字典:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

current_thread()获得当前线程,然后获得线程的内存地址,查找dicts里 此线程对应的字典,此时,如果dicts里没有这个线程对应的数据字典, 会引发 KeyError异常 ,执行:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

调用create_dict()方法创建字典:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

创建空字典,设置key,获得当前线程,获得当前线程的内存地址; 就是做一些准备工作,接着看到定义了两个方法,先跳过,往下看:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

然后又是新的知识点: Python弱引用函数ref()

6.Python弱引用函数ref()

ref() 这个函数是 weakref模块 提供的用于创建一个 弱引用 的函数, 参数异常是想 建立弱引用的对象当弱引用的对象被删除后的回调函数 为什么要用弱引用?

Python和其他高级语言一样,使用垃圾回收器来自动销毁不再使用的对象, 每个对象都有一个引用计数, 当这个计数为0时 ,Python才能够安全地销毁 这个对象,当对象 只剩下弱引用 时也会回收!

这里的 local_deleted() thread_deleted() 这两个回调参数 就是在**_localimpl对象 线程对象**被回收时触发:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

localimpl对象被回收时把线程里持有localimpl对象的弱引用删除掉, 线程对象对象被回收时 ,弹出大字典中该线程对应的数据字典;

剩下的三句就是保存_localimpl对象的弱引用到thread的** dict 里, localimpl对象 添加键值对 (线程弱引用,线程对应的数据字典) 到 大字典中,然后 返回线程对应的数据字典**。

又回到**_patch() 方法,拿到参数,然后又调用 init 函数 然后调用了 init 函数,这里不是很明白动机,猜测是如果 另外重写了local的 init **函数,可以调用一些其他的操作吧。

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

再接着又有一个知识点了,操作数据字典时的加锁,正常来说 私用Lock或RLock,需要自己去调用 acquire () 和release (), 而使用with关键字,就无需你自己去操心了,原因是RLock 类里重写了** enter exit **函数。

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

最后yield返回一个生成器对象。

到此, _threading_local 模块的完整的源码实现套路就浮出水面了, 不错,Get了很多新的姿势,如果你还有些疑惑的话,可以自己Debug, 跟跟方法的调用顺序,慢慢体会。

4.线程对象(threading.Thread)

使用threading.Thread创建线程:

可以通过下面两种方法创建新线程:

  • 1.直接创建 threading.Thread 对象,并把 调用对象 作为 参数传入
  • 2.继承 threading.Thread类 ,**重写run()**方法;

这里写代码测试个东西:到底使用多线程快还是单线程快~

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

两次运行结果采集:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

测试环境:Ubuntu 14.04 为了尽量公平,把单线程运行那个也另外放到 一个线程中,结果发现,多线程并没有比单线程快,反而还慢了一些。 出现这个原因是以为Python中的: 全局解释器锁(GIL) ,上一节已经 介绍过了,这里就不再复述了。

Thread类构造函数

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

参数依次是:

  • group :线程组
  • target :要执行的函数
  • name :线程名字
  • args/kwargs :要传入的函数的参数
  • daemon :是否为守护线程

相关属性与函数:

  • start ():启动线程,只能调用一次;
  • run ():线程执行的操作,可继承Thread重写,参数可从args和kwargs获取;
  • join ([timeout]): 堵塞调用线程 ,直到被调用线程运行结束或超时;如果 没设置超时时间会一直堵塞到被调用线程结束。
  • name/getName() :获得线程名;
  • setName ():设置线程名;
  • ident :线程是已经启动,未启动会返回一个非零整数;
  • is_alive ():判断是否在运行,启动后,终止前;
  • daemon/isDaemon() :线程是否为守护线程;
  • setDaemon ():设置线程为守护线程;

3.Lock(指令锁)与RLock(可重入锁)

上节就说过了,多个线程并发地去访问 临界资源 可能会引起线程同步 安全问题,这里写个简单的例子, 多线程写入同一个文件

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

打开 test.txt ,发现结果并没有按照我们预想的1-20那样顺序打印,而是乱的。

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

threading模块 中提供了两个类来确保多线程共享资源的访问: LockRLock

Lock指令锁 ,有两种状态( 锁定与非锁定 ),以及两个基本函数: 使用** acquire ()设置为 locked 状态,使用 release ()设置为 unlocked**状态。 acquire()有两个可选参数: blocking =True:是否堵塞当前线程等待; timeout =None:堵塞等待时间。如果成功获得lock,acquire返回True, 否则返回False,超时也是返回False。 使用起来也很简单,在访问共享资源的地方acquire一下,用完release就好:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

这里把循环次数改成了100,test.txt中写入顺序也是正确的,有效~ 另外需要注意:如果锁的状态是unlocked,此时调用release会 抛出 RuntimeError 异常!

RLock可重入锁 ,和Lock类似,但RLock却可以被 同一个线程请求多次 ! 比如在一个线程里调用Lock对象的acquire方法两次:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

你会发现程序卡住不动,因为已经发生了 死锁 ...但是在都在同一个主线程里, 这样不就很搞笑吗?这个时候就可以引入RLock了,使用RLock编写一样代码:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

输出结果:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

并没有出现Lock那样死锁的情况,但是要注意使用 RLockacquire与release 需要 成对出现 ,就是有多少个acquire,就要有多少个release,才能真正释放锁!

有点意思,点进去看看源码是怎么实现的,显示acquire方法:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

如果调用acquire方法是同一线程的话,计数器_count加1;在看下release:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

哈哈,一样的套路,_count减1。

小结

本节我们开始来啃Python并发里的threading,在学习线程局部变量的时候, 顺道把模块源码撸了一遍,而且还Get了很多以前没学过的东西,开森, 本节要消化的内容已经挺多的了,就先写那么多吧~

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

参考文献:

来啊,Py交易啊

欢迎各种像我一样的Py初学者,或者Py大神加入, 一起愉快地交流学♂习:

小猪的Python学习之旅 —— 7.Python并发之threading模块(1)

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机和难解性

计算机和难解性

M.R 加里、D.S. 约翰逊 / 张立昂、沈泓 / 科学出版社 / 1987年 / 4.50

本书系统地介绍了NP完全性理论的概念和方法,全书共分为7章和两个附录。第一章粗略地介绍了计算复杂性的一些基本概念和NP完全性理论的意义。第二章至第五章介绍了NP完全性的基本理论和证明的方法。第六章集中研究NP难问题的近似算法。第七章概述了大量计算复杂性中的有关理论课题。 附录A收集了范围广泛、内容丰富的NP完全性和NP难的问题、附录B补充了NP问题的一些最新的进展,既有理论方面的,又有关于具体问题......一起来看看 《计算机和难解性》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码