目的
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,多线程就是在一个进程中的多个线程,如果使用多线程默认开启一个主线程,按照程序需求自动开启多个线程(也可以自己定义线程数)。
多线程知识点
- Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
- 多线程共享主进程的资源,所以可能还会改变其中的变量,这个时候就要加上线程锁,每次执行完一个线程在执行下一个线程。
- 因为每次只能有一个线程运行,多线程怎么实现的呢?Python解释器中一个线程做完了任务然后做IO(文件读写)操作的时候,这个线程就退出,然后下一个线程开始运行,循环之。
- 当你读完上面三点你就知道多线程如何运行起来,并且知道多线程常用在那些需要等待然后执行的应用程序上(比如爬虫读取到数据,然后保存的时候下一个线程开始启动)也就是说多线程适用于IO密集型的任务量(文件存储,网络通信)。
- 注意一点,定义多线程,然后传递参数的时候,如果是有一个参数就是用args=(i,)一定要加上逗号,如果有两个或者以上的参数就不用这样。
代码实例
案例一 多线程核心用法
1 | import sys |
运行结果:
1 | thread MainThread is running... |
案例二 线程锁
前面有说到过,多线程是共享内存的,所以其中的变量如果发生了改变的话就会改变后边的变量,导致异常,这个时候可以加上线程锁。线程锁的概念就是主要这个线程运行完后再运行下一个线程。
1 | import sys |
使用线程锁后,程序按照一个一个有序执行。其中lock还有Rlock的方法,RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。否则会出现死循环,程序不知道解哪一把锁。注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的锁
案例三 join()方法的使用
在多线程中,每个线程自顾执行自己的任务,当最后一个线程运行完毕后再退出,所以这个时候如果你要打印信息的话,会看到打印出来的信息错乱无章,有的时候希望主线程能够等子线程执行完毕后在继续执行,就是用join()方法。
1 | import sys |
关于setDaemon()的概念就是:主线程A中,创建了子线程B,并且在主线程A中调用了B.setDaemon(),这个的意思是,把主线程A设置为守护线程,这时候,要是主线程A执行结束了,就不管子线程B是否完成,一并和主线程A退出.这就是setDaemon方法的含义,这基本和join是相反的。此外,还有个要特别注意的:必须在start() 方法调用之前设置,如果不设置为守护线程,程序会被无限挂起。
案例四 线程锁之信号Semaphore
类名:BoundedSemaphore。这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队。
1 | import time |
运行后,可以看到5个一批的线程被放行。
案例五 线程锁之事件Event
事件线程锁的运行机制:
全局定义了一个Flag,如果Flag的值为False,那么当程序执行wait()方法时就会阻塞,如果Flag值为True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。
事件主要提供了四个方法set()、wait()、clear()和is_set()。
1 | 调用clear()方法会将事件的Flag设置为False。 |
下面是一个模拟红绿灯,然后汽车通行的例子:
1 | #利用Event类模拟红绿灯 |
运行结果:
1 | 绿灯亮... |
案例六 线程锁之条件Condition
Condition称作条件锁,依然是通过acquire()/release()加锁解锁。
1 | wait([timeout])方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。 |
实际案例
1 | import threading |
如果不强制停止,程序会一直执行下去,并循环下面的结果:
1 | 线程A开始执行... |
案例 七定时器
定时器Timer类是threading模块中的一个小工具,用于指定n秒后执行某操作。一个简单但很实用的东西。
1 | from threading import Timer |
案例八 通过with语句使用线程锁
类似于上下文管理器,所有的线程锁都有一个加锁和释放锁的动作,非常类似文件的打开和关闭。在加锁后,如果线程执行过程中出现异常或者错误,没有正常的释放锁,那么其他的线程会造到致命性的影响。通过with上下文管理器,可以确保锁被正常释放。其格式如下:
1 | with some_lock: |
这相当于:
1 | some_lock.acquire() |
threading 的常用属性
1 | current_thread() 返回当前线程 |
线程池 threadingpool
在使用多线程处理任务时也不是线程越多越好。因为在切换线程的时候,需要切换上下文环境,线程很多的时候,依然会造成CPU的大量开销。为解决这个问题,线程池的概念被提出来了。
预先创建好一个数量较为优化的线程组,在需要的时候立刻能够使用,就形成了线程池。在Python中,没有内置的较好的线程池模块,需要自己实现或使用第三方模块。
需要注意的是,线程池的整体构造需要自己精心设计,比如某个函数定义存在多少个线程,某个函数定义什么时候运行这个线程,某个函数定义去获取线程获取任务,某个线程设置线程守护(线程锁之类的),等等…
在网上找了几个案例,供大家学习参考。
下面是一个简单的线程池:
1 | import queue |
分析一下上面的代码:
- 实例化一个MyThreadPool的对象,在其内部建立了一个最多包含5个元素的阻塞队列,并一次性将5个Thread类型添加进去。
- 循环100次,每次从pool中获取一个thread类,利用该类,传递参数,实例化线程对象。
- 在run()方法中,每当任务完成后,又为pool添加一个thread类,保持队列中始终有5个thread类。
- 一定要分清楚,代码里各个变量表示的内容。t表示的是一个线程类,也就是threading.Thread,而obj才是正真的线程对象。
上面的例子是把线程类当做元素添加到队列内,从而实现的线程池。这种方法比较糙,每个线程使用后就被抛弃,并且一开始就将线程开到满,因此性能较差。下面是一个相对好一点的例子,在这个例子中,队列里存放的不再是线程类,而是任务,线程池也不是一开始就直接开辟所有线程,而是根据需要,逐步建立,直至池满。
1 | # -*- coding:utf-8 -*- |
关于线程池其实涉及到工程设计,需要自己很熟练的运行面向对象程序设计。
生产者和消费者模式
生产者就是生成任务,消费者就是解决处理任务。比如在一个程序中,代码是按照重上往下执行,有的时候做等待的时间完全可以用来做任务处理或者做别的事情,为了节省时间,可以借助多线程的功能(自顾自完成自己线程任务)加上Queue队列特性(管道模式。里面存储数据,然后提供给线程处理)完成生产者和消费者模式。关于Queue的用法参考我之前的文章。
案例一
1 | import sys |
上面这个代码主要是针对骨架进行拆分解说,一般的生产者消费者模式都是这种构架,下面用一个更加清晰的案例来帮助理解。
案例二
1 | # -*- coding:utf-8 -*- |
案例三
使用生产者消费者模式实现代理IP扫描并且同步扫描代理IP是否可用,如果不适用生产者消费者模式的话,首先要获取代理IP,然后把获取到的IP放在一个列表,然后在扫描列表的IP,扫描过程为—->获取IP—->IP保存—->IP存活扫描。过程是单向的,也就是说没办法同步一边获取IP然后马上验证。
下面的代码是用生产者消费者模式实现代理IP的获取与存活扫描。
1 | # -*- coding: utf-8 -*- |
运行结果:
1 | 177.132.249.127:20183无法连接到代理服务器 |
Python3中的线程池方法
虽然在2版本中并没有线程池,但是在3版本中有相关线程池的使用方法。
1 | from concurrent.futures import ThreadPoolExecutor |