python爬虫之多线程爬虫

多线程

1.多线程介绍

如果我们把CPU比作一个工厂,这个工厂里面有多个车间,每一个车间就是一个进程;每一个车间有多个工人,每一个工人就是一个线程。我们之所以要学习多线程就是为了在同一时间里完成多项任务。

python中的多线程使用的是threading模块。

我们来看下面的一个例子,

import time


def drinking():
    for x in range(3):
        print("正在喝啤酒:%s" % x)
        time.sleep(1)


def eating():
    for x in range(3):
        print("正在吃小龙虾:%s" % x)
        time.sleep(1)


def main():
    drinking()
    eating()


if __name__ == '__main__':
    main()

执行结果如下,

正在喝啤酒:0
正在喝啤酒:1
正在喝啤酒:2
正在吃小龙虾:0
正在吃小龙虾:1
正在吃小龙虾:2

我们可以发现系统会在drinking()执行完毕后再执行eating(),如果我们要同时执行drinking()和eating()就需要使用多线程的方式进行了。例如,

import threading
import time


def drinking():
    for x in range(3):
        print("正在喝啤酒:%s" % x)
        time.sleep(1)


def eating():
    for x in range(3):
        print("正在吃小龙虾:%s" % x)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=drinking)  # 创建线程并且指定目标
    t2 = threading.Thread(target=eating)

    t1.start()  # 开启线程
    t2.start()


if __name__ == '__main__':
    main()

执行结果如下,

正在喝啤酒:0
正在吃小龙虾:0
正在吃小龙虾:1
正在喝啤酒:1
正在喝啤酒:2
正在吃小龙虾:2

在上例中,我们创建了两个Thread对象,Thread对象执行target参数中的方法为执行方法,分别用于执行drinking()和eating(),使用start()方法让线程开始任务。

我们可以看到drinking()和eating()同时执行。

2.自定义线程类

我们可以自定义一个类继承于threading.Thread类。threading.Thread类中的run()方法是当创建Thread对象的时候,如果没有传入target参数,则会执行run()方法中的代码。因此,我们需要对run()方法进行重写。

我们使用自定义线程类的方式重写上例,

import threading
import time


class DrinkingThread(threading.Thread):
    def run(self):
        for x in range(3):
            print("%s正在喝啤酒" % threading.current_thread())
            # threading.current_thread()方法会显示当前线程的名称
            time.sleep(1)


class EatingThread(threading.Thread):
    def run(self):
        for x in range(3):
            print("%s正在撸串" % threading.current_thread())
            time.sleep(1)


def main():
    t1 = DrinkingThread()
    t2 = EatingThread()

    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

执行结果同上例。

3.共享变量问题

有时我们可能需要多个线程对同一个变量进行操作,这时会出现线程冲突的问题,例如,我们使用多个线程进行自增操作,

import threading

VALUE = 0


def add_value():
    global VALUE
    for x in range(1000000):
        VALUE += 1
    print("VALUE:{}".format(VALUE))


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


if __name__ == '__main__':
    main()

执行结果如下,

VALUE:923582
VALUE:1135998

我们需要的执行结果为1000000和2000000。但是输出结果为923582和1135998,这是由于线程同时操作VALUE变量导致结果出错。

要解决线程冲突问题,我们需要对线程上锁。

首先,创建一个Lock对象,在对共享变量操作的代码前使用acquire()方法上锁,在代码后使用release()方法解锁。例如,

import threading

VALUE = 0

gLock = threading.Lock()


def add_value():
    global VALUE
    gLock.acquire()  # 上锁
    for x in range(1000000):
        VALUE += 1
    gLock.release()  # 释放锁
    print("VALUE:{}".format(VALUE))


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


if __name__ == '__main__':
    main()

执行结果如下,

VALUE:1000000
VALUE:2000000

注意,上锁操作是非常占用CPU的。

4.生产者和消费者模式

生产者和消费者模式是我们使用爬虫的一种模式,我们使用生产者来生成url链接,使用消费者来下载。

我们这里根据操作共享变量的模式分为Lock版和Condition版。

4.1.Lock版

例如,我们使用生产者给gMoney共享变量增加值,使用消费者给gMoney共享变量减少值。

import threading
import random
import time

gMoney = 666
gLock = threading.Lock()
gTotalTimes = 10
gTimes = 0


class Producer(threading.Thread):
    def run(self):
        global gMoney, gTotalTimes, gTimes
        while True:
            money = random.randint(666, 6666)
            gLock.acquire()  # 上锁
            if gTimes >= gTotalTimes:
                gLock.release()
                break
            gMoney += money
            gTimes += 1
            print("{}挣了{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
            gLock.release()
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global gMoney, gTimes, gTotalTimes, gLock
        while True:
            money = random.randint(666, 6666)
            gLock.acquire()
            if gMoney >= money:
                gMoney -= money
                print("{}消费了{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
            else:
                if gTimes >= gTotalTimes:
                    gLock.release()  # 如果不释放,容易卡死
                    break
                print("{}准备消费{}元钱,余额{}元,余额不足".format(threading.current_thread(), money, gMoney))
            gLock.release()
            time.sleep(1)


def main():
    # 五个生产者
    for x in range(5):
        t = Producer(name="生产者线程{}".format(x + 1))
        t.start()

    # 三个消费者
    for x in range(3):
        t = Consumer(name="消费者线程{}".format(x))
        t.start()


if __name__ == '__main__':
    main()

因为频繁上锁消耗CPU,因此,这种方式不是最优方式。

4.2.Condition版

我们可以使用threading.Condition让线程在没有数据时,进入等待状态。

我们同样需要创建一个Condition对象,在操作共享变量的代码前使用acquire()方法上锁,在使用release()方法解锁前,需要使用notify()或者notify_all()方法通知第一个或这所有线程。

注意,notify()、notify_all()方法不会释放锁,必须在release()方法之前调用。

import threading
import random
import time

gMoney = 666
gCondition = threading.Condition()
gTotalTimes = 10
gTimes = 0


class Producer(threading.Thread):
    def run(self):
        global gMoney, gTotalTimes, gTimes, gCondition
        while True:
            money = random.randint(666, 6666)
            gCondition.acquire()  # 上锁
            if gTimes >= gTotalTimes:
                gCondition.release()
                print("当前生产者总共生产了{}次".format(gTimes))
                break
            gMoney += money
            gTimes += 1
            print("{}挣了{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
            gCondition.notify_all()  # 通知所有等待的线程
            gCondition.release()  # 释放之前,通知正在等待的消费者线程
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global gMoney, gTimes, gTotalTimes, gCondition
        while True:
            money = random.randint(666, 6666)
            gCondition.acquire()
            while gMoney < money:
                print("{}准备消费{}元钱,余额{}元,余额不足".format(threading.current_thread(), money, gMoney))
                if gTimes >= gTotalTimes:
                    gCondition.release()
                    return
                gCondition.wait()
            gMoney -= money
            print("{}准备消费{}元钱,余额{}元".format(threading.current_thread(), money, gMoney))
            gCondition.release()
            time.sleep(1)


def main():
    # 五个生产者
    for x in range(5):
        t = Producer(name="生产者线程{}".format(x + 1))
        t.start()

    # 三个消费者
    for x in range(3):
        t = Consumer(name="消费者线程{}".format(x))
        t.start()


if __name__ == '__main__':
    main()

5.Queue安全队列

为了防止线程冲突,我们也可以使用队列的方式存取数据。在python中我们使用queue模块。

我们可以创建两种队列,

  1. Queue,先进先出队列
  2. LifoQueue,后进先出队列

上面的两种队列都实现了锁,即要么不操作,要么操作完,我们可以直接拿来使用。

下面是队列的一些方法,

  1. Queue(),初始化队列,创建队列对象。
  2. qsize(),类方法,返回队列的大小。
  3. empty(),类方法,判断队列是否为空。
  4. full(),类方法,判断是否满了。
  5. get(),类方法,从队列中取最后一个数据。
  6. put(),类方法,将数据放到队列中。

你可能感兴趣的:(python,队列,多线程)