聚沙成塔--爬虫系列(十四)(群架要怎么打)

2017-11-13

版权声明:本文为作者原创文章,可以随意转载,但必须在明确位置标明出处!!!

tips:本基础系列旨在以爬虫带大家入门Python语言

本章并不是要教你如何去打群架,本篇文章将主要介绍如何使用多线程来帮我们的提高程序的执行效率,「群架」只是为了更加生动的去描述多线程的现象,相信大部分读者都看过《叶问》,叶问为了在日本军那里拿到粮食它决定一次要打10个,这种模式就是单线程模式,他必须把10个人一个个打败了才能赢得粮食,这种模式也是我们前面章节用到的模式,虽然最终能够赢得胜利,但用时就比较长了。假设叶问有多只手,每次最多只能用两只手,那么他在和人对战的时候就可以出其不意的将对手干翻掉,这肯定不两只手解决敌人的速度要块。这就是多线程模式,由于Python解释器GIL(Global Interpreter Lock)的存在,同一时刻只能有一个线程在执行,所以对于现在的多核CPU而言,Python中的多线程并没提高程序的处理能力,反而有可能降低程序的处理能力,如果你开启的线程足够多的话,线程的上下文切换将会造成程序的执行效率变低。

那么有的读者就会问既然Python的多线程设计并不能有效的提高程序的处理能力,那为什么还要有这个模块呢,说到这里就需要给读者普及一下概念了,I/O密集型、CPU密集型

I/O密集型

什么是I/O密集型呢,I/O的是输入/输出(input/output)的意思,就是我们程序大部分的时间都是用来等待I/O操作,想网络的请求、文件的读写、数据库的读写都属于I/O操作,前面的章节说过I/O操作是很费时的。所以Python中的多线程适用于I/O密集型的任务,因为它不需要用到CPU的计算能力。

CPU密集型

CPU密集型的意思就是计算密集型,如果你的程序中有大量复杂的计算逻辑那么就选择CPU密集型,因为复杂计算是十分耗CPU资源的,想圆周率的计算,高清视频数据的处理等等。那么Python中怎么设计CPU密集型呢,当然是多进程的使用,什么意思呢,就拿上面叶问的例子,若是叶问有10个分身,二期10个分身都是独立有思想的,那么打10个还不是分分钟的事吗。

threading模块

多线程有两个模块一个是thread模块、一个就是threading模块,这里推荐大家使用threading模块,不推荐使用thread模块有两个原因,一个是它对于进程何时退出没有控制,当主线程结束时子线程也会被强制性的结束也不会发出警告或者进行适当的清理和释放工作。另一个原因是该模块不支持守护线程这个概念。这里普及一个概念,线程是不能单独执行的,它必须依赖进程才能存活。进程至少包含一个线程也就是上面说道的主线程,只要进程启动,那么主线程也就启动了。

threading模块对象

threading模块提供了如下对象:
hreading模块对象
这些对象都包含在threading模块里。没个类对象都提供了哪些方法接口都可以在开放这文档查看到。

创建线程

使用Thread类来创建线程,创建线程有有几种方式,你可以选择你最舒服最中意的方式去使用。

  • 创建Thread的实例,传给它一个参数

    1
    2
    3
    4
    5
    6
    7
    8
    import threading
    def fun():
    count = 0
    for index in range(10000000):
    count = count + 1
    th = threading.Thread(target=fun, args=())
    th.start()
    th.join()
  • 创建Thread 的实例,传给它一个可调用的类实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import threading
    class ThreadFunc(object):
    def __init__(self, func, args):
    self.func = func
    self.args = args
    def __call__(self):
    self.res = self.func(*self.args)
    def fun():
    count = 0
    for index in range(10000000):
    count = count + 1
    th = threading.Thread(target = ThreadFunc(fun, ())
    th.start()
    th.join()
  • 派生Thread 的子类,并创建子类的实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import threading
    class myThread(threading.Thread):
    def __init__(self, func, args):
    super(myThread, self).__init__(self)
    self.args = args
    self.func = func
    def run(self):
    self.res = self.func(*self.args)
    def fun():
    count = 0
    for index in range(10000000):
    count = count + 1
    th= myThread(fun, ())
    th.start()
    th.join()

推荐使用第三种方式,第三种方式对于灵活行和未来的扩展性更好。

同步

涉及到多线程编程必定要涉及到一个主题就是数据同步,这里普及一下多线程为什么能提高程序的处理能力,这个跟CPU的工作原理有关,只有拿到CPU分给线程执行的时间片,我们的代码才能够被CPU执行,所以如果我们有多个线程去执行同一个任务那么每个线程有是有机会得到CPU时间片,那对于处理同一个人才得到的时间片总和就比单个线程大太多了,所以多线程能够提高程序的处理能力。但是多线程的处理会遇到数据同步的问题,为什么会出现这种问题呢,我们知道了线程只有得到CPU时间片才能执行,那么当A线程正在执行还没有执行完,这个时候CPU时间片到了,那么A线程就会暂停在此刻并进入「休眠」状态,B线程拿到时间片也执行同样的动作,这个时候B就有可能把A、B线程共同拥有的数据给破坏掉,举个例子,张三、李四都喜欢吃热狗,他们一起走进店里买热狗,张三刚刚想把热狗拿起来,这个时候由于CPU时间到了,张三就进入休眠状态了,李四刚好拿到CPU时间片他就把热狗拿走了,等到下一次张三拿到CPU时间片从休眠状态中唤醒,他认为自已已经拿到热狗了,但真实的情况是这根热狗已经被李四拿走了所以这就造成了数据的不同步了。讲了这么多我们来看一个卖票的例子,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import threading
tickets = 10000000
listdata = []
def sale_ticket():
global tickets
while tickets:
tickets = tickets - 1
# print(tickets)
listdata.append(tickets)
threads = []
for index in range(10):
thread = threading.Thread(target=sale_ticket, args=())
thread.start()
threads.append(thread)
for t in threads:
t.join()
print(len(listdata))
print('thread exit')
#执行结果:33934146

从结果中我们可以看出这个结构肯定是不对的,因为我们只有10000000这么多张票,而结果是我们总票数的3倍还多,这就是因为有很多人都拿到了相同的票。那么如何解决数据同步呢,解决数据同步的很多总方法,章节前面threading模块对象已经列出来了,Lock、RLock等,下面我们主要讲一下Lock的用法,

Lock类

Lock类提供了两个方法

  • acquire(blocking=True, timeout=-1)
    获取一个锁 blocking参数默认为True,即阻塞模式,什么意思呢,就是电话亭一样,如果电话亭里有人在打电话那么下一个人只有等电话亭里的人打完电话你才能打,如果设置为False,既不阻塞,如果电话亭里有人打电话那我就干其它事去了,等会儿再来看看,若是电话亭里这个时候没有人我就把电话亭外挂个请勿打扰的牌子,timeout超时时间,只有blocking为True有用。
  • released()
    该函数没有参数,释放一个锁

加锁改写后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import threading
tickets = 10000000
listdata = []
mutex = threading.Lock()
def sale_ticket():
global tickets
while tickets:
if mutex.acquire():
tickets = tickets - 1
# print(tickets)
listdata.append(tickets)
mutex.release()
threads = []
for index in range(10):
thread = threading.Thread(target=sale_ticket, args=())
thread.start()
threads.append(thread)
for t in threads:
t.join()
print(len(listdata))
print('thread exit')
# 执行结果:10000000

从结果可以看出我们保证了数据的同步,也就是保证了每个人都买到的票都是唯一的。

okay,本章就讲到这里就结束了, 因为我们的爬虫程序正好是一个I/O密集型程序,所以使用多线程设计正好合适,如何设计多线程我将放到后面的章节将,读者有兴趣的可以去我的git查看源码,地址https://github.com/Gavinxyj/Python/tree/master/python_study/Scrapy/modules欢迎大家fork、star。

PS:成为牛人你只需要保持每天进步一点点


欢迎关注我的公众号:「爱做饭的老谢」,老谢一直在努力…

上一篇:聚沙成塔–爬虫系列(十三)(如何正确的使用数据库三)
下一篇:聚沙成塔–爬虫系列(十五)(如何,成为设计师)