Python教程

python爬虫(十八)多线程

本文主要是介绍python爬虫(十八)多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

多线程

多进程

系统中运行的应用程序,打开浏览器、pycham等都是一个个的应用程序,可以同时运行。一个应用程序就是一个进程,多个就是多进程。比如电脑卡住了,但是可以打开任务管理器去关掉占用资源多的应用程序。最开始电脑只有1个CPU,只能执行一个进程,其他进程就会处于堵塞的状态,之所以感觉到多个进程同时运行,是CPU进行高速的切换处理,出现了多核,多个CPU就可以同时执行多个任务。

多线程

CPU 是通过线程去执行进程的,进程中的执行单位是线程,进程中包含的执行单元就是线程,一个进程可以包含多个线程。一个微信就是一个进程,每一个聊天窗口就是一个线程,在python中一次只能执行一个线程,打开多个线程之后,会有线程锁的存在,来解决资源竞争的问题。python中的多线程是伪多线程,并不是纯粹意义上的多线程,同一时间只有一个线程处于执行的状态。
充分利用等待的时间,比如线程1对url1发送请求,在等待相应的时间内,线程2就可以对url2发送请求,在等待的时间内,线程3就可以对url3发送请求,这时url1获取到响应的内容,就可以进行下一步的操作,把时间充分利用,在等待的时间去做其他的事情,最大限度的提高爬取的效率。

多线程的创建

通过函数创建

通过threading模块中的Thread类,里面有个参数target,通过参数去传递函数对象,传递时函数不用加括号,来实现多线程的逻辑,把需要做的事情都写到函数里,通过target传递进去,要用start给一个启动的状态。
注意:要先创建一个函数,函数里存放多线程的实现功能,把通过threading模块中的Thread类,把函数传递进去。

通过类来创建

自定义一个类,若想实现多线程,就要继承父类,threading.Thread,还要重写run()方法。

import threading
import time

# 1.通过函数创建多线程
def demo1():
    #线程的函数事件
    print("子线程!")

if __name__ == '__main__':
    for i in range(7):
        # 创建多线程t,通过threading里的Thread类,把demo1传入到target中
        t = threading.Thread(target=demo1)  #仅仅是传递函数事件,并未创建线程
        t.start() # 创建并启动多线程(是一个启动的状态),告诉CPU可以调用多线程了
        # 如果需要启动多次,用for循环实现

"""def __init__(self, group=None(对线程进行分组), target=None(接收函数事件), name=None(线程分组的名字),
                 args=()(传入的元组), kwargs=None, *, daemon=None):
"""

# 2. 通过类创建多线程
# 创建MyThread类,继承父类threading.Thread的功能
class MyThread(threading.Thread):
    # 重写run方法
    def run(self):
        for i in range(5):
            print("这是一个子线程!")

if __name__ == '__main__':
    # 实例化类
    m = MyThread()  # 实例化对象
    # start 启动子线程
    m.start()  # 通过start执行多线程


# 小案例
def test():
    for i in range(4):
        print("子线程")
       # time.sleep(1)    # 如果没有强制等待,主线程111运行的时间不固定

if __name__ == '__main__':
    t = threading.Thread(target=test)
    t.start()
    print("111")
    # 函数运行的时候111是最后才执行的,执行完函数运行结束。
    # 而用了多线程之后,有点同步的意思,先打印1个“子线程”,
    # 然后打印111,之后又继续执行多线程中的剩余打印“子线程”

总结:正常函数运行的时候,在主函数中运行最后一行代码就结束了,而在多线程中,会继续运行子线程,不管主线程有没有运行完成(例题中的print(111)),都会等待子线程(print(“子线程”))运行完毕再退出。主线程会等待子线程运行结束后才结束运行。
如果非要子线程运行结束后再执行主线程,就需要在主线程前加上time.sleep,强制等待,或者 用 t.join(),不管前面的子线程运行多久,都要等待子线程运行完之后,才运行主线程。

# 小案例,先运行子线程,后运行主线程
def test():
    for i in range(4):
        print("子线程")
        # time.sleep(1)  # 如果没有强制等待,主线程111运行的时间不固定

if __name__ == '__main__':
    t = threading.Thread(target=test)
    t.start()
    # 第一种方法,用time强制等待
    time.sleep(3)
    # 第二种方法,用join
    t.join()
    print("111")

查看线程的数量

主线程是本来就有的,除了主线程之外,我们会创建多个子线程,如何查看子线程的数量?先回顾一下enumerate。

# enumerate()回顾
test_lst = ['xxx', 'yyy', 'zzz']
# 取出列表数据的方法,1是下标,2是索引
for i in test_lst:
    print(i)

for i in enumerate(test_lst):
    print(type(i), i)  # 元组类型的数据,包含索引和列表元素值

for index, i in enumerate(test_lst):
    print(index, i)  # 返回两个值,一个是索引,一个是元素值

threading.enumerate(),返回存活的线程的列表,没有消亡的,存活的线程都会以列表的形式返回。主线程一般会等到子线程运行结束,才退出。

import threading
import time

# threading.enumerate()
# 返回存活的线程的列表,没有消亡的,存活的线程都会以列表的形式返回。
def test1():
    for i in range(5):
        print("demo1--%d" % i)

def test2():
    for i in range(5):
        print("demo2--%d" % i)

if __name__ == '__main__':
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)
    t1.start()
    t2.start()
    print(threading.enumerate())
    # 会根据子线程执行的速度快慢,主线程执行的先后顺序,返回1个或2个存活线程

import threading
import time


def test1():
    for i in range(5):
        time.sleep(1)
        print("demo1--%d" % i)

def test2():
    for i in range(5):
        time.sleep(1)
        print("demo2--%d" % i)

if __name__ == '__main__':
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)
    t1.start()
    t2.start()
    print(threading.enumerate())
    # 返回3个当前存活的线程,在t1,t2强制等待的时候,先运行了主线程

对程序稍微做一下修改,当只剩一个线程的时候,退出。

import threading
import time


def test1():
    for i in range(8):
        time.sleep(1)
        print("demo1--%d" % i)

def test2():
    for i in range(5):
        time.sleep(1)
        print("demo2--%d" % i)

if __name__ == '__main__':
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)
    t1.start()
    t2.start()
    while True:
        print(threading.enumerate())
        time.sleep(1)
        if len(threading.enumerate()) <= 1:
            break
        # 让程序一直运行,如果只剩下一个线程的时候,退出,demo1运行8次,demo2运行5次
        # 之前运行的是3个线程,demo2 运行到4的时候,该线程就已经消亡了,剩下的线程就只有2个
        # 在demo1 运行到7的时候,该线程也消亡了,就只剩下主线程

threading.Thread()只是把函数事件传递进去,start才是真正意义上的创建并启动线程。

import threading
import time


def test1():
    for i in range(3):
        time.sleep(1)
        print("demo1--%d" % i)

def test2():
    for i in range(3):
        time.sleep(1)
        print("demo2--%d" % i)

if __name__ == '__main__':
    print('前', threading.enumerate())  # 1个线程
    t1 = threading.Thread(target=test1) # 该方法只是把函数时间传递进去
    print('中', threading.enumerate())  # 1个线程
    t2 = threading.Thread(target=test2)
    t2.start()  # 创建并启动线程
    print('后', threading.enumerate())  # 2个线程
    # 只有经过start之后,才算是真正的创建并启动线程

多线程的工作原理

在没有创建,启动多线程之前,只有主线程在运行。增加了一个子线程,好比公司招了一个人,给他分配任务,子线程往下做事情,同时主线程也会继续做自己该做的事情,两个线程同时在运行,两者并不冲突。再增加的话,三者同时运行,各司其职。

线程间的资源竞争

用线程锁解决之间资源竞争的问题。
复习一下局部变量和全局变量

a = 10  # 全局变量

def fn():
    # global a  # 声明为全局后,外部1为10,内部、外部打印的都是99
    a = 99  # 局部变量
    print("函数内部的a为%d" % a)  #99

print("函数外部1的a为%d" % a)  # 10
fn()
print("函数外部的a为%d" % a)  # 10

没有资源竞争时的多线程。

num = 100
def demo1():
    global num
    num += 1
    print("demo1--%d" % num)
def demo2():
    print("demo1--%d" % num)

def main():
    t1 = threading.Thread(target=demo1)  # 把函数传递进去
    t2 = threading.Thread(target=demo2)
    t1.start()  # 创建并启动线程
    t2.start()
    print("main--%d" % num)

if __name__ == '__main__':
    main()
""""运行结果三者都为101,程序启动的时候,t1运行,num为全部变量,执行+1操作,返回101
t2运行时,使用+1过的num。也不排除t2抢到资源优先启动的情况,几率很小。主线程只进行了资源访问"""

通过传参的形式,往函数中传入参数,当传入的参数足够大的时候,会出现资源竞争的问题。

import threading
import time

num = 0
def demo1(nums):
    global num
    for i in range(nums):
        num += 1
    print("demo1--%d" % num)
def demo2(nums):
    global num
    for i in range(nums):
        num += 1
    print("demo1--%d" % num)

def main():
    # 注意,传参的时候是以元组的形式传入的,只传一个内容,要加个逗号
    t1 = threading.Thread(target=demo1, args=(1000000,))  # 把函数传递进去
    t2 = threading.Thread(target=demo2, args=(1000000,))
    t1.start()  # 创建并启动线程
    t2.start()
    time.sleep(3)
    print("main--%d" % num)

if __name__ == '__main__':
    main()

"""当传入的10000时,demo1--10000,demo1--20000,main--20000;
当传入1000000时候,三者的值就发生了变化,没有直接相加的那么多,出现了资源竞争的问题
CPU运行的时候,要看哪个先抢到资源,当传入的数值比较小的时候,循环次数少,
循环的次数多了,就有可能被抢走资源。"""

在上述例题中,num初始值为0,有可能demo1先抢到资源,对num进行+1的操作,重新赋值给num,再重新获取num的变量,此时num为1,再进行+1的运算,有可能还没来得及重新赋值给num,就被demo2抢走了资源,此时demo2拿到的num就为1,做完+1操作后重新赋值给num,此时做了3次+1的操作,但有效的只有2次,丢了一次;在运行过程中就会出现相互抢的情况,造成了最后的结果没有那么多。
此时,就要用线程锁解决资源竞争的问题,可以在t1.start之后,用time强制等待3秒,让demo1先做完运算,再执行demo2。这时如果运算用不了3秒,就会造成程序资源的浪费,如果强制等待1秒,demo1运行不完,又解决不了实际问题。可以用一把锁来解决问题,threading.Lock(),Lock对应的是一个类,有很多方法,acquire是加锁,release是解锁。

import threading
import time

num = 0
# Lock只能上一把锁
lock = threading.Lock()
def demo1(nums):
    global num
    # 上锁
    lock.acquire()
    for i in range(nums):
        num += 1
    # 解锁
    lock.release()
    print("demo1--%d" % num)
def demo2(nums):
    global num
    # 上锁
    lock.acquire()
    for i in range(nums):
        num += 1
    # 解锁
    lock.release()
    print("demo1--%d" % num)

def main():
    # 注意,传参的时候是以元组的形式传入的,只传一个内容,要加个逗号
    t1 = threading.Thread(target=demo1, args=(1000000,))  # 把函数传递进去
    t2 = threading.Thread(target=demo2, args=(1000000,))
    t1.start()  # 创建并启动线程
    t2.start()
    time.sleep(3)
    print("main--%d" % num)

if __name__ == '__main__':
    main()

我们要把锁放在有可能产生资源竞争的地方,上锁和解锁是一一对应的,不能在同一个地方上2把锁,好比门已经锁上了,再去锁的话就没有意义,程序会停到上锁的地方,因为找不到地方可以上锁。
可以用rlock完成同一个地方多次上锁和多次解锁,上锁和解锁的次数也是一一对应的。

import threading
import time

num = 0
# RLock能上多把锁,上多少锁就要解多少锁
rlock = threading.RLock()
def demo1(nums):
    global num
    # 上锁
    rlock.acquire()
    rlock.acquire()
    for i in range(nums):
        num += 1
    # 解锁
    rlock.release()
    rlock.release()
    print("demo1--%d" % num)
def demo2(nums):
    global num
    # 上锁
    rlock.acquire()
    rlock.acquire()
    rlock.acquire()
    for i in range(nums):
        num += 1
    # 解锁
    rlock.release()
    rlock.release()
    rlock.release()
    print("demo1--%d" % num)

def main():
    # 注意,传参的时候是以元组的形式传入的,只传一个内容,要加个逗号
    t1 = threading.Thread(target=demo1, args=(1000000,))  # 把函数传递进去
    t2 = threading.Thread(target=demo2, args=(1000000,))
    t1.start()  # 创建并启动线程
    t2.start()
    time.sleep(3)
    print("main--%d" % num)

if __name__ == '__main__':
    main()

不管是Lock还是Rlock,上锁和解锁次数不一致都会出现问题,锁要上在可能会出现资源竞争的位置。

线程队列

特点:先进先出

from queue import Queue

"""
empty():判断队列是否为空
full():判断队列是否满了
get():从队列中取数据
put():把一个数据放到队列中
"""

# 实例化对象,然后可以使用里面的方法
q = Queue()
# 返回布尔型,队列为空,返回为True;队列不为空,返回False
print(q.empty())  # True
# 如果不为空,就可以用get从队列中取值

print(q.full())  # False
# 判断队列是不是满了,True表示满的,False表示不是满的
# 如果队列不是满的,可以用put往里面加数据

现在往里面加数据进行测试

from queue import Queue

# 在初始化的时候,没有设定容量,可以存放大概2G的数据

# q = Queue()
# 可以在初始化的时候指定容量大小
q = Queue(3)  # 初始化容量为3

print(q.empty())
print(q.full())
q.put(1)
q.put(2)
q.put(3)
print('*'*50)
print(q.empty())
print(q.full())
"""没有指定容量,在添加1之前,队列是空的,不是满的;添加了1之后,队列不是空的,但也不是满的,
得到的结果是True,False,False,False
如果指定了队列长度为3,添加了1,2,3,之后,队列容量达到了上限,得到的结果是True,False,False,True
如果队列添加满了后,再往队列里加内容,程序就会卡到那,q.put(4,timeout=2),在2秒后就会提示queue.full的错误
跟q.put_nowait(4)一样的效果"""
print(q.get())
print(q.get())
print(q.get())
print(q.get(timeout=2))

"""先进先出,先传进去的最先取出来,如果多取一个程序也会卡到那,q.get(timeout=2),2秒后提示queue.Empty的错误"""
这篇关于python爬虫(十八)多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!