python进阶学习之多线程及队列
python学习 . 2020/04/09发布 . shanyonggang_web . 我要评论 . 379阅读

在之前的爬虫项目中,好多都是单线程的程序即程序按照从头到尾的步骤进行执行,而在实际的爬虫项目过程中,有的步骤会有延迟等待(比如访问某些网页、解析网页、下载图片视频等),这样就如果按照步骤的话就容易出现等待的情况,浪费了CUP资源,为了提高爬虫效率,因此出现了并发功能,本文主要是总结python的多线程和队列相关知识,其中可以参考官方文档:python多进程python队列

多线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,一个进程可以包含多个线程,举个简单的例子:我们在看视频的过程中,音频和视频同时运行就为多线程。

python多进程使用到的库为:threading

多线程用法:

用法1,threading,Thread(target=Function_name),如下:

import threading

def show_name():
    for i in range(10):
        print('我是天猫精灵')

def show_age():
    for i in range(10):
        print('我是小爱同学')

thread1 = threading.Thread(target=show_name)
thread2 = threading.Thread(target=show_age)

thread1.start()
thread2.start()

用法2:类继承的方式(此方法用的比较多),如下: 

import threading

class ShowAge(threading.Thread):
    def __init__(self):
        super(ShowAge, self).__init__()
    def run(self):
        for i in range(10):
            print('我是天猫精灵')

class ShowName(threading.Thread):
    def __init__(self):
        super(ShowName, self).__init__()
    def run(self):
        for i in range(10):
            print('我是小爱同学')
thread1 = ShowAge()
thread2 = ShowName()

多线程共享全局变量问题

当使用多线程时候,会出现同时操作一个变量的问题,这样容易造成数据的错乱,针对这个问题,python多线程中有锁的机制,当访问某个变量的时候加锁,其他线程就无法访问,当前线程处理完后,再将锁释放即可,如下:

import threading
import random
import time

lock = threading.Lock()

# 记录生产者生产的次数,达到10次就不再生产
gTimes = 0
 
gMoney = 1000

class Processer(threading.Thread):
    def run(self):
        global lock
        global gMoney
        global gTimes
        while True:
            money = random.randint(100,1000)
            lock.acquire()
            if gTimes >= 10:
                lock.release()
                break
            gMoney += money
            print('%s当前存入%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            lock.release()

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

if __name__ == "__main__":
    for i in range(5):
        Consumer().start()
    for i in range(4):
        Processer().start()

锁中有Lock和Rlock两种,threading.Lock() 加载线程的锁对象,是一个基本的锁对象,一次只能一个锁定,其余锁请求,需等待锁释放后才能获取,threading.RLock() 多重锁,在同一线程中可用被多次acquire。如果使用RLock,那么acquire和release必须成对出现,调用了n次acquire锁请求,则必须调用n次的release才能在线程中释放锁对象

实现一问一答的形式:

此处需要用到多线程的threading.Condition(),主要是用wait()、notify()来互相通知对方交替运行,本文使用天猫精灵和小爱同学读诗对话的形式,如下:

import threading

cod = threading.Condition()

class XiaoAi(threading.Thread):

    def __init__(self):
        super(XiaoAi,self).__init__()
        self.name = '小爱同学'

    def run(self):
        cod.acquire()
        cod.notify()
        print('{}说:天猫精灵'.format(self.name))
        cod.wait()
        cod.notify()
        print('{}说:我们来读诗吧!!!'.format(self.name))
        cod.wait()
        cod.notify()
        print('{}说:静夜思'.format(self.name))
        cod.wait()
        cod.notify()
        print('{}说:窗前明月光'.format(self.name))
        cod.wait()
        cod.notify()   
        print('{}说:举头望明月'.format(self.name))
        cod.wait()
        cod.release()

class TianMAao(threading.Thread):

    def __init__(self):
        super(TianMAao,self).__init__()
        self.name = '天猫精灵'

    def run(self):
        cod.acquire()
        cod.wait()
        print('{}说:哎...我在呢!'.format(self.name))
        cod.notify()
        cod.wait()
        print('{}说:好呀,一起PK呀!!!'.format(self.name))
        cod.notify()
        cod.wait()
        print('{}说:李白'.format(self.name))
        cod.notify()
        cod.wait()
        print('{}说:疑是地上霜'.format(self.name))
        cod.notify()
        cod.wait()
        print('{}说:低头思故乡'.format(self.name))
        cod.notify() 
        cod.release()



thread2 = XiaoAi()
thread1 = TianMAao()
thread1.start()
thread2.start()

最终结果如下:

可以看出双方一人一条来进行读诗对话,这样就实现了一问一答的形式。

queue队列

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

1、queue.Queue(maxsize=1000)   # 建一个先进先出的队列
2、qsize()   # 返回队列的大小。
3、empty()   # 判断队列是否为空。
4、full()    # 判断队列是否满了。
5、get()    # 从队列中取最后一个数据。
6、put()   # 将一个数据放到队列中。

实际用法如下,本文使用爬取表情包的案例进行说明,如下:

import queue
import threading
import requests
from lxml import etree
from urllib import request
import re
import time 
import os

class GetPages(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,img_queue,*args,**kwargs):
        super(GetPages, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            self.parse_page(url)
    def parse_page(self,url):
        response = requests.get(url,headers=self.headers)
        text = response.text
        html = etree.HTML(text)
        imgs = html.xpath("//div[@class='page-content text-center']//a//img")
        for img in imgs:
            if img.get('class') == 'gif':
                continue
            img_url = img.xpath(".//@data-original")[0]
            suffix = os.path.splitext(img_url)[1]
            alt = img.xpath(".//@alt")[0]
            alt = re.sub(r'[,。??,/\\·]','',alt)
            img_name = alt + suffix
            self.img_queue.put((img_url,img_name))

class GetImgs(threading.Thread):
    def __init__(self,page_queue,img_queue,*args,**kwargs):
        super(GetImgs, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue
 
    def run(self):
        while True:
            if self.img_queue.empty():
                if self.page_queue.empty():
                    return
            img = self.img_queue.get(block=True)
            url,filename = img
            request.urlretrieve(url,'F:/0_个人学习/0_网络爬虫/0_开发实战/多进程、多线程练习/test/'+filename)
            print(filename+'  下载完成!')
page_queue = queue.Queue(100)
img_queue = queue.Queue(100)
for x in range(1,101):
    url = "http://www.doutula.com/photo/list/?page=%d" % x
    page_queue.put(url)

for x in range(5):
    t = GetPages(page_queue,img_queue)
    t.start()

for x in range(5):
    t = GetImgs(page_queue,img_queue)
    t.start()

结果如下:

GIL

GIL是python多线程的一个鸡肋,Python自带的解释器是CPythonCPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫做GIL(Global Intepreter Lock),叫做全局解释器锁。这个解释器锁是有必要的。因为CPython解释器的内存管理不是线程安全的。GIL虽然是一个假的多线程。但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程。一个线程需要执行任务,必须获取GIL,在I/O阻塞的时候,解释器会释放GIL。

  • 好处:直接杜绝了多个线程访问内存空间的安全问题。
  • 坏处:Python的多线程不是真正多线程,不能充分利用多核CPU的资源。

  • 有疑问请在下方评论区留言,我会尽快回复。
  • Email私信我: 511248513@qq.com 或添加博主 微信
本文作者:shanyonggang_web
发布时间:2020年4月9日 16:57
许可协议: 署名-非商业性使用 4.0 国际许可协议
知识共享许可协议     转载请保留原文链接及作者
正在加载今日诗词....
您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请狠狠点击下面的


登录 后回复

当前暂无评论,点击登录来做第一个吃螃蟹的人吧!