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

引言html

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

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


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)

先说个知识点:segmentfault

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

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

输出结果并发

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

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

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

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


*3._threading_local源码解析

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

_thread 模块上节也说了threading模块的基础模块,应该尽可能使用 threading 模块替代,而咱们代码里也没导入这个模块,因此会走 _threading_local ,点进去看下这个模块,246行代码,很少,嘿嘿, 点击PyCharm左侧的Structure看看代码结构

关注点在**_localimpllocal**两个类上,咱们先把这个模块的源码 全选,而后新建一个Python文件,把内容粘贴到里面,为何要 这样作呢?

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

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

1.PyCharm调试速成

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

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

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

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

单步调试

Step Over(F8),程序向下执行一行,若是该行 函数被调用,直接执行完返回,而后执行下一行;

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

Step Into(F7)

上面这一步,遇到官方类库的函数也会进去,若是只想在碰到 本身定义函数才进去的话,能够点击

Step Into My Code(Alt + Shift + F7)

进入函数后肯定没什么问题了,能够点击

Step Out(Shift + F8) 跳出这个函数,返回该函数被调用处的下一行语句。

若是想快速执行到下一个断点的位置,能够点击

Run to Cursor(Alt + F9)

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

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

监视变量,有时右侧Variables,显示的变量有不少时,而你 想关注某一个变量而已,能够点击这个小眼镜:

,而后 输入你想监视的变量名,若是名字太长或者懒,能够直接右键 变量, Add To Watches便可!不想监视时可右键 Remove Watch

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

断点设置,点击左侧:

,能够打开断点设置窗口,能够在此 看到全部的断点,设置条件断点(知足某个条件时,暂停程序执行), 删除断点,或者临时禁用断点等。

好的,关于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()**对象:

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

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


4.Python黑魔法__slots__内置属性

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

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

输出结果

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

输出结果

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

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

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

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

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

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

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

这里前面判断name是否为__dict__,猜想是权限控制,不容许 外部经过**setattrdelattr**来操做字典,只容许经过 **_patch()**方法来修改操做字典!

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

@contextmanager 又是什么东西???

又是新的知识点~


5.@contextmanager

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

对于相似于文件关闭这种不想遗忘的重要操做,咱们能够本身封装 一个with结构来进行处理,封装也很简单,再定义你那个类的时候 重写**enter方法和exit**方法,好比文件关闭那个能够自定义 成这样的:

若是以为上面这种实现起来比较麻烦的话,就能够用 @contextmanager啦,直接就一个方法,比定义类简单多了~

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

current_thread()得到当前线程,而后得到线程的内存地址,查找dicts里 此线程对应的字典,此时,若是dicts里没有这个线程对应的数据字典, 会引起KeyError异常,执行:

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

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

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


6.Python弱引用函数ref()

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

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

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

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

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

又回到**_patch()方法,拿到参数,而后又调用init函数 而后调用了init函数,这里不是很明白动机,猜想是若是 另外重写了local的init**函数,能够调用一些其余的操做吧。

再接着又有一个知识点了,操做数据字典时的加锁,正常来讲 私用Lock或RLock,须要本身去调用acquire()和release(), 而使用with关键字,就无需你本身去操心了,缘由是RLock 类里重写了**enterexit**函数。

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

到此,_threading_local模块的完整的源码实现套路就浮出水面了, 不错,Get了不少新的姿式,若是你还有些疑惑的话,能够本身Debug, 跟跟方法的调用顺序,慢慢体会。


4.线程对象(threading.Thread)

使用threading.Thread建立线程

能够经过下面两种方法建立新线程:

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

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

两次运行结果采集:

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

Thread类构造函数

参数依次是

  • 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(可重入锁)

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

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

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

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

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

RLock可重入锁,和Lock相似,但RLock却能够被同一个线程请求屡次! 好比在一个线程里调用Lock对象的acquire方法两次:

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

     输出结果:

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

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

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

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


小结

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


参考文献


来啊,Py交易啊

想加群一块儿学习Py的能够加下,智障机器人小Pig,验证信息里包含: PythonpythonpyPy加群交易屁眼 中的一个关键词便可经过;

验证经过后回复 加群 便可得到加群连接(不要把机器人玩坏了!!!)~~~ 欢迎各类像我同样的Py初学者,Py大神加入,一块儿愉快地交流学♂习,van♂转py。

相关文章
相关标签/搜索