八、学习分布式爬虫之多线程

多线程爬虫

  1. 理解多线程
  2. 掌握threading模块的使用
  3. 掌握生产者消费者模式
  4. 理解GIL
  5. 能用多线程写爬虫
    什么是多线程
    理解:默认情况下,一个程序只有一个进程和一个线程,代码是依次线性执行的,而多线程则可以并发执行,一次性多个人做多件事,自然比单线程更快。
    在这里插入图片描述
    threading模块
    threading模块是python中专门提供用来做多线程编程的模块。threading模块中最常用的类是Thread。
import time
import threading

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

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

def single_thread():  #单线程花费6秒
    start = time.time()
    conding()
    drawing()
    end = time.time()
    used_time = end-start
    return used_time

def mulei_thread(): #多线程执行花费3秒
    t1 = threading.Thread(target=conding)
    t2 = threading.Thread(target=drawing)
    t1.start()
    t2.start()

if __name__ == '__main__':
    # use = single_thread()
    mulei_thread()

继承自threading.Thread类
首先来了解两个小知识点:

  1. 使用threading.current_thread()可以看到当前线程的信息
  2. 使用threading.enumerate()函数便可以看到当前的线程
    为了让线程代码更好的封装,可以使用threading模块下的Thread类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码
import time
import threading
#
# def conding():
#     the_thread = threading.current_thread() #查看当前线程信息
#     print(the_thread.name) #线程名字
#     for x in range(3):
#         print('%s正在写代码'%the_thread.name)
#         time.sleep(1)
#
# def drawing():
#     the_thread = threading.current_thread()
#     for x in range(3):
#         print('%s正在画画'%the_thread.name)
#         time.sleep(1)
#
#
# def mulei_thread():
#     t1 = threading.Thread(target=conding,name='th1')
#     t2 = threading.Thread(target=drawing,name='th2')
#     t1.start()
#     t2.start()
#     print(threading.enumerate()) #[<_MainThread(MainThread, started 10328)>, <Thread(th1, started 16888)>, <Thread(th2, started 19120)>]
#
# if __name__ == '__main__':
#     mulei_thread()

#=================继承自threading.Thread类=================================
#1.我们自己写的类必须继承自‘threading.Thread’类
#2.线程代码需要放在run方法中执行
#3.以后创建线程的时候,直接使用我们自己创建的类来创建线程就可以了
#4.为什么要使用类的方式创建线程呢?原因是因为类可以更加方便的管理我们的代码,可以让我们使用面向对象的方式进行编程
class codingThread(threading.Thread):
    def run(self):
        the_thread = threading.current_thread()
        for x in range(3):
            print('%s正在写代码'%the_thread.name)
            time.sleep(1)

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

def multi_thread():
    th1 = codingThread()
    th2 = drawingThread()

    th1.start()
    th2.start()

if __name__ == '__main__':
    multi_thread()

多线程共享全局变量问题
多线程都是在同一个进程中运行的,因此在进程中的全局变量所有线程都是可共享的,这就造成了一个问题,因为线程执行是无序的,有可能造成数据错误。比如以下代码,正常结果本应该是1000000和2000000,但是因为多线程运行的不确定性,因此最后结果可能是随机的。

import threading
value = 0

def add_value():
    #如果在函数中修改了全局变量,那么需要使用global关键字进行声明
    global value
    for x in range(1000000):
        value += 1
    print('value的值%s'%value)

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

if __name__ == '__main__':
    main()

为了解决以上问题,就要用到锁的机制
锁机制和threading.Lock类
为了解决共享全局变量的问题,threading提供了一个Lock类,这个类可以在某个线程访问某个变量的时候加锁,其他线程此时就不能进来,直到当前线程处理完后,把锁释放了,其他线程才能进来。
使用锁的原则:

  1. 把尽量少的和不耗时的代码放到锁中执行
  2. 代码执行完成后要记得释放锁
import threading

value = 0

gLock = threading.Lock()

def add_value():
    #如果在函数中修改了全局变量,那么需要使用global关键字进行声明
    global value
    gLock.acquire() #上锁,当其他线程来到这里时,发现已被上锁,就在这里进行等待
    for x in range(1000000):
        value += 1
    gLock.release() #释放锁
    print('value的值%s'%value)

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

if __name__ == '__main__':
    main()

注意:在多线程中,如果需要修改全局变量,那么需要在修改全局变量的地方使用锁锁起来,执行完成后把锁释放掉
生产者消费者模式(包子铺)
生产者消费者模式时多线程开发中经常见到的一种模式,生产者的线程专门用来生产一些数据,然后放到一个中间的变量中,消费者再从这个中间的变量中取出数据进行消费。通过生产者消费者模式,可以让代码达到高内聚低耦合的目标,程序分工更加明确,线程更加方便管理。
高内聚:当前的代码不会对外面的代码产生影响或依赖。
低耦合:多个模块之间影响比较小
在这里插入图片描述
放到爬虫上来说就是:将爬取数据的代码放到一个线程当中(生产者),爬取下来的数据存储在一个中间容器中,然后将保存数据的代码放在另外一个线程中(消费者),将存放在中间容器中的数据取出来保存在硬盘中。
Lock版本的生产者消费者模式

import threading
import random

gMoney = 0
gTime = 0
gLock = threading.Lock()

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gTime
        while True:
            gLock.acquire()
            if gTime >= 10:
                gLock.release()
                break
            money = random.randint(0,100)
            gMoney += money
            gTime += 1
            print("%s生产了%d元钱"%(threading.current_thread().name,money))
            gLock.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gTime
        while True:
            gLock.acquire()
            money = random.randint(0,100)
            if gMoney >= money:
                gMoney -= money
                print("%s消费了%d元钱"%(threading.current_thread().name,money))
            else:
                if gTime >= 10:
                    gLock.release()
                    break
                print("%s想消费%d元钱,但是余额只有%d"%(threading.current_thread().name,money,gMoney))
            gLock.release()

def main():
    for x in range(5):
        th = Producer(name='生产者%d号'%x)
        th.start()

    for x in range(5):
        th = Consumer(name='消费者%d号'%x)
        th.start()

if __name__ == '__main__':
    main()

condition版的生产者消费者模式
Lock版本的生产者消费者模式可以正常的运行,但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够,上锁是一个很耗费CPU资源的行为,因此这种方式不是最好的,还有一种更好的方式便是使用threading.Condition来实现,threading.Condition可以在没有数据的时候处于堵塞等待状态,一旦有了合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程,这样就可以不用做一些无用的上锁和解锁的操作,可以提高程序的性能。首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。以下将一些常用的函数做个简单的介绍:

  1. acquire:上锁
  2. release:解锁
  3. wait:将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notify和notify_all函数唤醒。被唤醒后会继续等待上锁,上锁后会继续执行下面的代码
  4. notify:通知某个正在等待的线程,默认时第一个等待的线程
  5. notify_all:通知所有等待的线程。notify和notify_all不会释放锁,并且需要在release之前调用
import threading
import random
import time

gMoney = 0
gTime = 0
gCondition = threading.Condition()

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gTime
        while True:
            gCondition.acquire()
            if gTime >= 10:
                gCondition.release()
                break
            money = random.randint(0,100)
            gMoney += money
            gTime += 1
            print("%s生产了%d元钱"%(threading.current_thread().name,money))
            gCondition.notify_all()
            gCondition.release()
            time.sleep(1)

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gTime
        while True:
            gCondition.acquire()
            money = random.randint(0,100)
            while gMoney < money:
                if gTime >= 10:
                    gCondition.release()
                    return
                print('%s想消费%d元钱,但是余额只有%d元钱了,消费失败!'%(threading.current_thread().name,money,gMoney))
                gCondition.wait()
            gMoney -= money
            print('%s消费了%d元钱,剩余%d元钱'%(threading.current_thread().name,money,gMoney))
            gCondition.release()
            time.sleep(1)

def main():
    for x in range(5):
        th = Producer(name='生产者%d号'%x)
        th.start()

    for x in range(5):
        th = Consumer(name='消费者%d号'%x)
        th.start()

if __name__ == '__main__':
    main()

Queue线程安全队列
在线程中,访问一些全局变量,加锁是一个经常的过程,如果你是想把一些数据存储到某个队列中,那么python内置了一个线程安全的模块叫做queue模块。python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后进先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:

  1. Queue(size):创建一个先进先出的队列
  2. qsize():返回队列的大小
  3. empty():判断队列是否为空,返回布尔值
  4. full():判断队列是否满了,返回布尔值
  5. get():从队列中取最后一个数据,默认情况下时阻塞的,也就是说如果队列空了,那么再调用就会一直阻塞,知道有新的数据添加进来,也可以使用block=False来关掉阻塞,在队列为空的情况获取就会抛出异常
  6. put():将一个数据放到队列中,跟get一样,在队列满了的时候也会一直堵塞,并且可以通过block=False来关掉阻塞,同样也会抛出异常
from queue import Queue
import random
import threading
import time

# q = Queue(4)  #最多能存储四个数据
#
# for x in range(4):
#     q.put(x,block=False) #block=False当数据超出队列最大尺寸,不会阻塞,而是抛出异常
#
# if q.full():
#     print('满了')
#
# print(q)  #<queue.Queue object at 0x02E00FB8>
# print(q.qsize()) #4
#
# for x in range(4):
#     value = q.get()
#     print(value)
#
# if q.empty():
#     print('空了')

def add_value(q):
    while True:
        ran = random.randint(0,10)
        q.put(ran)
        print('已经放入了:%d'%ran)
        time.sleep(1)

def get_value(q):
    while True:
        print("获取到的值:%d"%q.get())

def main():
    q = Queue(10)
    th1 = threading.Thread(target=add_value,args=[q])
    th2 = threading.Thread(target=get_value,args=[q])

    th1.start()
    th2.start()

if __name__ == '__main__':
    main()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章