Python爬虫-进阶篇之多线程爬虫

1、多线程描述

   多线程是为了同步完成多项任务,通过提高资源使用效率来提高系统的效率。线程是在同一个时间需要完成多项任务的时候实现的。
   最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理,火车也可以有多节车厢。
   多线程的出现就是为了提高效率,但同时也会带来一些问题。

2、threading模块

   threading模块是Python中专门提供用来做多线程编程的模块,threading模块中最常用的类是Thread

   A)单线程示例代码如下:

# 单线程
import time

def coding():
    for x in range(3):
        print('%s正在写代码'%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在画图'%x)
        time.sleep(1)

def single_thread():
    coding()
    drawing()

if __name__ == '__main__':
    single_thread()

   运行结果:

0正在写代码
1正在写代码
2正在写代码
0正在画图
1正在画图
2正在画图

   B)多线程示例代码如下:

# 多线程
import threading
import time

def coding():
    for x in range(3):
        print('%s正在写代码'%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在画图'%x)
        time.sleep(1)

def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)
    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()

   运行结果:

0正在写代码
0正在画图
1正在写代码
1正在画图
2正在写代码
2正在画图

3、多线程相关知识点

   A)查看线程数:使用threading.enumerate()函数查看当前线程的数量。

import threading
import time

def coding():
    for x in range(3):
        print('%s正在写代码'%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在画图'%x)
        time.sleep(1)

def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)

    t1.start()
    t2.start()
    print(threading.enumerate())

if __name__ == '__main__':
    multi_thread()

   运行结果:

0正在写代码
[<_MainThread(MainThread, started 31204)>, , ]
0正在画图
1正在写代码
1正在画图
2正在写代码
2正在画图

   B)查看当前线程的名字:使用threading.current_thread()函数查看当前线程的信息。

import threading
import time

def coding():
    for x in range(3):
        print('%s正在写代码'%threading.current_thread())
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在画图'%threading.current_thread())
        time.sleep(1)

def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)

    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()

   运行结果:

正在写代码
正在画图
正在画图
正在写代码
正在写代码
正在画图

   C)继承自threading.Thread
   为了让线程代码更好的封装,可以使用threading模块下的Thread类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码,示例代码如下:

import threading
import time

class CodingThread(threading.Thread):
   def run(self):
       for x in range(3):
           print('%s正在写代码'%threading.current_thread())
           time.sleep(1)

class DrawingThread(threading.Thread):
   def run(self):
       for x in range(3):
           print('%s正在画图'%threading.current_thread())
           time.sleep(1)

def multi_thread():
   t1 = CodingThread()
   t2 = DrawingThread()

   t1.start()
   t2.start()

if __name__ == '__main__':
   multi_thread()

   运行结果:

正在写代码
正在画图
正在写代码
正在画图
正在画图
正在写代码

   D)多线程共享全局变量的问题
   多线程都是在同一个进程中运行的,因此在进程中的全局变量所有线程都是可共享的,这就造成了一个问题,因为线程执行的顺序是无序的,有可能会造成数据错误,示例代码如下:

import threading
value = 0

def add_value():
    global value
    for i in range(1000000):
        value += 1
    print('value: %d'%value)

def main():
    for i in range(2):
        t = threading.Thread(target=add_value)
        t.start()

if __name__ == '__main__':
    main()

   运行结果:

value: 1205450
value: 1253480

   以上代码运行的结果应该是value: 1000000value: 2000000,但因为多线程运行的不确定性,因此最后的结果可能是随机的,解决该问题需要用到多线程的锁机制。

   E)锁机制
   为了解决以上使用共享全局变量的问题,threading提供了一个Lock类,这个类可以在某个县城访问某个变量的时候加锁,其他线程就不能进来,直到当前线程处理完成后,把锁释放了,其他线程才可以进来处理,示例代码如下:

import threading
value = 0
gLock = threading.Lock()
def add_value():
    global value
    gLock.acquire()
    for i in range(1000000):
        value += 1
    gLock.release()
    print('value: %d'%value)

def main():
    for i in range(2):
        t = threading.Thread(target=add_value)
        t.start()

if __name__ == '__main__':
    main()

   运行结果:

value: 1000000
value: 2000000

   F)Lock版本生产者和消费者模式
   生产者和消费者是多线程开发中经常见到的一种模式,生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中,消费者再从这个中间的变量中取出数据进行消费,但是因为要使用中间变量,中间变量经常是一些全局变量,因此需要使用锁来保证数据完整性,如下是使用threading.Lock锁实现的“生产者与消费者模式”的一个例子:

import threading
import random
import time

gMoney = 1000
gLock = threading.Lock()
# 记录生产者生产数据的次数,达到10次就不再生产了
gTotalTimes = 5
gTimes = 0

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gLock.acquire()
            # 如果已经达到10次了,就不再生产了
            if gTimes >= gTotalTimes:
                gLock.release()
                break
            gMoney += money
            print('{}当前存入{}元钱, 剩余{}元钱'.format(threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            gLock.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gTimes
        while True:
            money = random.randint(100, 500)
            gLock.acquire()
            if gMoney > money:
                gMoney -= money
                print('{}当前取出{}元钱,剩余{}元钱'.format(threading.current_thread(), money, gMoney))
                time.sleep(0.5)
            else:
                # 如果钱不够了,有可能是已经超过了次数,这时候就判断一下
                if gTimes >= gTotalTimes:
                    gLock.release()
                    break
                print('{}当前想取{}元钱,剩余{}元钱,余额不足!'.format(threading.current_thread(), money, gMoney))
            gLock.release()

def main():
    for i in range(5):
        Consumer(name='消费者线程{}'.format(i)).start()

    for i in range(5):
        Producer(name='生产者线程{}'.format(i)).start()

if __name__ == '__main__':
    main()

   运行结果:

当前取出475元钱,剩余525元钱
当前取出183元钱,剩余342元钱
当前想取366元钱,剩余342元钱,余额不足!
当前取出148元钱,剩余194元钱
当前想取226元钱,剩余194元钱,余额不足!
当前存入603元钱, 剩余797元钱
当前存入883元钱, 剩余1680元钱
当前存入705元钱, 剩余2385元钱
当前存入337元钱, 剩余2722元钱
当前存入195元钱, 剩余2917元钱
当前取出116元钱,剩余2801元钱
当前取出398元钱,剩余2403元钱
当前取出121元钱,剩余2282元钱
当前取出108元钱,剩余2174元钱
当前取出269元钱,剩余1905元钱
当前取出209元钱,剩余1696元钱
当前取出125元钱,剩余1571元钱
当前取出356元钱,剩余1215元钱
当前取出358元钱,剩余857元钱
当前取出302元钱,剩余555元钱
当前取出193元钱,剩余362元钱
当前取出133元钱,剩余229元钱
当前取出188元钱,剩余41元钱

   G)Condition版本生产者和消费者模式
   Lock版本的生产者与消费者模式可以正常的运行,但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够,上锁是一个很耗费CPU资源的行为,因此这种方式不是最好的,还有一种更好的方式便是threading.Condition来实现。threading.Condition可以在没有数据的时候处于阻塞等待状态,一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程,这样就可以不用做一些无用的上锁和解锁的操作,可以提高程序的性能。
   首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁,如下是一些常用的函数:
      G.1)acquire:上锁
      G.2)release:解锁
      G.3)wait:将当前线程处于等待状态,并且会释放锁,可以被其他线程使用notifynotify_all函数唤醒,被唤醒后会继续等待上锁,上锁后继续执行下面的代码。
      G.4)notify:通知某个正在等待的线程,默认是第1个等待的线程。
      G.5)notify_all:通知所有正在等待的线程,notifynotify_all不会释放锁,并且需要在release之间调用。
如下示例代码是Condition版的生产者与消费者模式的一个例子:

import threading
import random
import time

gMoney = 1000
gCondition = threading.Condition()
# 记录生产者生产数据的次数,达到10次就不再生产了
gTotalTimes = 5
gTimes = 0
class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gCondition.acquire()
            # 如果已经达到5次了,就不再生产了
            if gTimes >= gTotalTimes:
                gCondition.release()
                break
            gMoney += money
            print('{}当前存入{}元钱, 剩余{}元钱'.format(threading.current_thread(), money, gMoney))
            gTimes += 1
            gCondition.notify_all()
            gCondition.release()
            time.sleep(0.5)

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        while True:
            money = random.randint(100, 500)
            gCondition.acquire()
            while gMoney < money:
                if gTimes >= gTotalTimes:
                    gCondition.release()
                    return
                print('{}准备消费{}元钱,剩余{}元钱,余额不足!'.format(threading.current_thread(), money, gMoney))
                gCondition.wait()
            gMoney -= money
            print('{}当前想取{}元钱,剩余{}元钱'.format(threading.current_thread(), money, gMoney))
            gCondition.release()
            time.sleep(0.5)

def main():
    for i in range(5):
        Consumer(name='消费者线程{}'.format(i)).start()

    for i in range(5):
        Producer(name='生产者线程{}'.format(i)).start()

if __name__ == '__main__':
    main()

   运行结果:

当前想取334元钱,剩余666元钱
当前想取233元钱,剩余433元钱
准备消费495元钱,剩余433元钱,余额不足!
当前想取232元钱,剩余201元钱
准备消费208元钱,剩余201元钱,余额不足!
当前存入517元钱, 剩余718元钱
当前想取495元钱,剩余223元钱
当前想取208元钱,剩余15元钱
当前存入931元钱, 剩余946元钱
当前存入566元钱, 剩余1512元钱
当前存入556元钱, 剩余2068元钱
当前存入976元钱, 剩余3044元钱
当前想取192元钱,剩余2852元钱
当前想取333元钱,剩余2519元钱
当前想取284元钱,剩余2235元钱
当前想取186元钱,剩余2049元钱
当前想取256元钱,剩余1793元钱
当前想取432元钱,剩余1361元钱
当前想取145元钱,剩余1216元钱
当前想取489元钱,剩余727元钱
当前想取267元钱,剩余460元钱
当前想取212元钱,剩余248元钱
当前想取232元钱,剩余16元钱

   H)Queue线程安全队列
   在线程中,访问一些全局变量,加锁是一个经常的过程,如果你想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。
   Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue、LIFO(后进先出)队列LifoQueue。
   这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用,可以使用队列来实现线程间的同步,相关函数如下:
   a)初始化Queue(maxsize):创建一个先进先出的队列
   b)qsize():返回队列的大小
   c)empty():判断队列是否为空
   d)full():判断队列是否满了
   e)get():从队列中取出一个数据
   f)put():将一个数据放到队列中

from queue import Queue
import time
import threading
def set_value(q):
    index = 0
    while True:
        q.put(index)
        index += 1
        time.sleep(3)

def get_value(q):
    while True:
        print(q.get())

def main():
    q =Queue(4)
    t1 = threading.Thread(target=set_value, args=[q])
    t2 = threading.Thread(target=get_value, args=[q])

    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

   I)GIL全局解释器锁
   Python自带的解释器是CPython,CPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核),同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个GIL(全局解释器锁),这个解释器锁是有必要的,因为CPython解释器的内存管理不是线程安全的,当然除了CPython解释器,还有其他的解释器,有些解释器是没有GIL锁的,如下:
   a)Jython:用Java实现的Python解释器,不存在GIL锁,更多详情见:https://zh.wikipedia.org/wiki/Jython。
   b)IronPython:用.Net实现的Python解释器,不存在GIL锁,更多详情见:https://zh.wikipedia.org/wiki/IronPython。
   c)PyPy:用Python实现的Python解释器,存在GIL锁,更多详情见:https://zh.wikipedia.org/wiki/PyPy。
   GIL虽然是一个假的多线程,但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的,在IO操作上建议使用多线程提高效率,在一些CPU计算操作上不建议使用多线程,而建议使用多进程。

你可能感兴趣的:(Python爬虫-进阶篇之多线程爬虫)