Python 爬虫进阶篇——多线程

Python 爬虫进阶篇——多线程

本文介绍一下多线程。不过值得注意的是,不能滥用多线程,多线程爬虫请求内容速度过快,可能会导致服务器过载,或者是IP被封禁。为了避免这一问题,我们在使用多线程爬虫的时候需要设置一个delay时间,用于请求同一域名时的最小时间间隔。

线程和进程如何工作

当程序在运行时,就会创建包含代码和状态的进程。这些进程通过一个或者多个CPU来执行。不过同一时刻每个CPU只会执行一个进程,然后在不同进程之间快速切换,这样就感觉多个程序同时运行。同理,在一个进程中,程序的执行也是在不同线程间进行切换的,每个线程执行程序的不同部分。这就意味着一个线程在等待执行时,进程会切换到其他的线程执行,这样可以避免浪费CPU时间。

Threading线程模块

在Python标准库中,使用threading模块来支持多线程。Threading模块对thread进行了封装,绝大数情况,只需要使用threading这个模块。使用起来也非常简单:

t1=threading.Thread(target=run,args=("t1",)) 创建一个线程实例
# target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式存在
t1.start() 启动这个线程实例。

普通创建方式

线程的创建很简单,如下:

import threading
import time

def printStr(name):
    print(name+"-python知识学堂")
    s=0.5
    time.sleep(s)
print(name+"-python知识学堂")

t1=threading.Thread(target=printStr,args=("你好!",))
t2=threading.Thread(target=printStr,args=("欢迎你!",))
t1.start()
t2.start()

自定义线程

本质是继承threading.Thread,重构Thread类中的run方法

import threading
import time

class testThread(threading.Thread):
    def __init__(self,s):
        super(testThread,self).__init__()
        self.s=s

    def run(self):
        print(self.s+"——python")
        time.sleep(0.5)
        print(self.s+"——知识学堂")

if __name__=='__main__':
    t1=testThread("测试1")
    t2=testThread("测试2")
    t1.start()
    t2.start()

守护线程

使用setDaemon(True)把子线程都变成主线程的守护线程,因此当主线程结束后,子线程也会随之结束。也就是说,主线程不等待其守护线程执行完成再去关闭。

import threading
import time

def run(s):
    print(s,"python")
    time.sleep(0.5)
    print(s,"知识学堂")

if __name__ == "__main__":
    t=threading.Thread(target=run,args=("你好!",))
    t.setDaemon(True)
    t.start()
    print("end")

结果:

你好! python

end

当主线程结束后,守护线程不管有没有结束,都自动结束。

主线程等待子线程结束

使用join方法,让主线程等待子线程执行。如下:

import threading
import time

def run(s):
    print(s,"python")
    time.sleep(0.5)
    print(s,"知识学堂")

if __name__ == "__main__":
    t=threading.Thread(target=run,args=("你好!",))
    t.setDaemon(True)
    t.start()
    t.join()
    print("end")

结果:

你好! python

你好! 知识学堂

end

以上是多线程的几种简单的用法,那么threading模块还有做什么呢?请往下看。

Lock 锁

其实在介绍diskcache缓存的时候也介绍过锁的相关内容,其实不难理解为啥多线程中也会出现锁的概念,当没有保护共享资源时,多个线程在处理同一资源时,可能会出现脏数据,造成不可以预期的结果,即线程不安全。

如下示例出现不可预期的结果:

import threading

price=0
def changePrice(n):
    global price
    price=price+n
    price=price-n

def runChange(n):
    for i in range(2000000):
        changePrice(n)

if __name__ == "__main__":
    t1=threading.Thread(target=runChange,args=(5,))
    t2=threading.Thread(target=runChange,args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(price) 

理论上的结果为0,但是每次运行的结果可能都是不一样的。

所以这个时候就需要锁去处理了,如下:

import threading
import time
from threading import Lock

price=0
def changePrice(n):   
    global price
    lock.acquire() #获取锁
    price=price+n
    print("price:"+str(price))
    price=price-n
    lock.release() #释放锁

def runChange(n):
    for i in range(2000000):
        changePrice(n)

if __name__ == "__main__":
    lock=Lock()
    t1=threading.Thread(target=runChange,args=(5,))
    t2=threading.Thread(target=runChange,args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(price) 

结果值与理论值是一致的。锁的意义在于每次只允许一个线程去修改同一数据,以保证线程安全。

信号量

BoundedSemaphore类,同时允许一定数量的线程更改数据,如下:

import threading
import time

def work(n):
    semaphore.acquire()
    print("序号:"+str(n))
    time.sleep(1)
    semaphore.release()

if __name__ == "__main__":
    semaphore=threading.BoundedSemaphore(5)
    for i in range(100):
        t=threading.Thread(target=work,args=(i+1,))
        t.start()

#active_count获取当前正在运行的线程数
while threading.active_count()!=1:
        pass
    else:
        print("end")

结果为:每5次打印停顿一下,直到结束。

GIL全局解释器锁

说到多线程,不得不提一下GIL。GIL的全称是Global Interpreter Lock(全局解释器锁),这是python设计之初,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,并且在一个进程中,GIL只有一个。只有拿到GIL的线程,才能进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。

总结

本篇文章介绍了多线程的用法,要根据实际的情况去使用。多线程编程,容易发生冲突,必须用锁加以隔离,又得小心发生死锁。由于Python的设计时有GIL全局锁,导致多线程无法利用多核,使得在多线程并发的情况下并不理想。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值