多线程与多进程爬虫(持续更新中...)

目录

一、创建线程

1、多进程

2、多线程

3、为什么写爬虫需要多线程

4、注意事项

 二、多线程

1、使用threading模块创建线程

2、使用Thread子类创建线程

三、线程间通信

3.1 什么是互斥锁

3.1.1 互斥锁的使用

3.1.2 使用队列在线程间通信

4、创建进程的常用方式

4.1 使用multiprocessing模块创建进程

4.2 使用Process子类创建进程

4.3 使用进程池Pool创建进程

4.4 进程间通信

4.4.1 多进程队列的使用

4.4.2 使用队列在进程中通信


一、创建线程

      由于线程是操作系统直接支持的执行单元,涉及两个模块:_thread(低级模块)和threading(高级模块),threading对_thread进行了封装,一般使用threading这个高级模块。

1、多进程

     系统中运行的一个应用程序

进程的特点:

  • 一个核心只能执行一个进程 其他的进程处于阻塞状态
  • n个核心就能执行n个进程

2、多线程

特点:
  • 一个进程可以包含很多个线程
  • 线程之间有资源竞争问题

3、为什么写爬虫需要多线程

  • 尽可能快的爬下数据
  • 发送请求 等待服务器返回响应 返回响应
  • 线程可以优化我们的发起请求的速度

4、注意事项

      多线程最好与ip代理一起使用、不要一次创建非常多的线程

 二、多线程

1、使用threading模块创建线程

语法:
Thread([group[,target[,name[,args[,kwargs]]]]])
参数:
group:值为None,为以后版本而保留
target:表示一个可调用对象,线程启动时,run()方法将调用此方法,默认值为None,表示不调用任何内容
name:表示当前线程名称,默认创建一个“Thread-N"格式的唯一名称
args:表示传递给target函数的参数元组
kwargs:表示传递给target函数的参数字典

案例: 使用threading模块创建线程

import threading,time
def process():
    for i in range(3):
        time.sleep(1)
        print("thread name is %s:" % threading.current_thread().name)

if __name__ == '__main__':
    print("-----主线程开始-----")
    # 创建4个线程,存入列表
    threads = [threading.Thread(target=process) for i in range(4)]
    for t in threads:
        t.start()       # 开启线程
    for t in threads:
        t.join()        # 等待子线程结束
    print("-----主线程结束-----")
说明:

       以上创建了4个进程,然后分别用for循环执行了4次start()和join()方法,每个子线程分别执行输出3次。可以看出,执行顺序是不确定的。

2、使用Thread子类创建线程

        Thread线程类也可以通过一个继承Thread线程类的子类来创建线程。

案例:使用Thread子类创建线程

        创建一个继承threading.Thread线程类的子类SubThread,并定义一个run()方法实例化SubThread类创建2个线程,并且调用start()方法开启线程,会自动调用run()方法。

import threading,time
class SubThread(threading.Thread): # 继承threading.Thread线程
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "子线程"+self.name+'执行,i='+str(i) #name属性中保存的是当前线程的名字
            print(msg)
if __name__ == '__main__':
    print('-----主线程开始-----')
    t1 = SubThread()      # 创建子线程t1
    t2 = SubThread()   # 创建子线程t2
    t1.start()            # 启动子线程t1
    t2.start()            # 启动子线程t2
    t1.join()             # 等待子线程t1
    t2.join()             # 等待子线程t2
    print('-----主线程结束-----')
运行结果:
-----主线程开始-----
子线程Thread-1执行,i=0
子线程Thread-2执行,i=0
子线程Thread-2执行,i=1
子线程Thread-1执行,i=1
子线程Thread-1执行,i=2
子线程Thread-2执行,i=2
-----主线程结束-----

三、线程间通信

案例:验证线程之间是否可以共享信息

from threading import Thread
import time
def thread1():
    print('-----------子线程1开始 ---------------')
    global g_num
    g_num += 50
    print('g_numis %d',g_num)
    print('-----------子线程1结束-----------')

def thread2():
    time.sleep(1)
    print('-----------子线程2开始----------')
    global g_num
    g_num -= 50
    print('g_num is %d' % g_num)
    print('-----------子线程2结束---------------')

def thread3():
    time.sleep(1)
    print('-----------子线程3开始----------')
    global g_num
    g_num *= 50
    print('g_num is %d' % g_num)
    print('-----------子线程3结束---------------')
g_num = 100
if __name__ == '__main__':
    print('------------主线程开始--------------')
    print('g_num is %d' % g_num)
    t1 = Thread(target=thread1())
    t2 = Thread(target=thread2())
    t3 = Thread(target=thread3())
    t1.start()
    t2.start()
    t3.start()
    t1.join()
    t2.join()
    t3.join()

运行结果:

------------主线程开始--------------
g_num is 100
-----------子线程1开始 ---------------
g_numis %d 150
-----------子线程1结束-----------
-----------子线程2开始----------
g_num is 100
-----------子线程2结束---------------
-----------子线程3开始----------
g_num is 5000
-----------子线程3结束---------------

3.1 什么是互斥锁

       由于线程可以对全局变量随意修改,这就可能造成多线程之间对全局变量使用的混乱。

        互斥锁(Mutual exclusion 缩写:Mutex),可以防止多个线程同时读取某一块内存区域,互斥锁为资源引入一个状态:锁定和非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为"锁定",其他线程不能更改,直到该线程释放资源,将资源的状态变成"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程才能写入操作,从而保证了多线程情况下的数据正确性。

3.1.1 互斥锁的使用

        在threading模块中使用Lock类可以方便处理锁定。Lock类有两个方法:acquire()锁定和release()释放锁。

语法:
mutex = threading.Lock()   # 创建锁
mutex.acquire([blocking])  # 锁定
mutex.release()            # 释放锁
参数说明:
acquire([blocking]): 获取锁定,必要时需要阻塞到锁定释放为止。如果提供blocking参数并将其设置为False,当无法获取锁定时将立即返回False,如果成功获取锁定则返回True.
release():释放一个锁定。当锁定处于未锁定状态时,或者从与原本调用了acquire()方法的不同线程调用此方法,将出现错误。

案例: 使用多线程的互斥锁模拟多人购票功能

       使用多线程和互斥锁模拟实现多人同时订购电影票的功能,假设电影院某个场次只有10张电影票,10个人用户同时抢购该电影票,每售出一张,显示一次剩余的电影票张数。

from threading import Thread,Lock
import time
n = 10
def task():
    global n
    mutex.acquire() # 上锁
    temp = n
    time.sleep(0.1)
    n = temp - 1# 数量减1
    if n > 0:
        print('购买成功,剩余%d张电影票' % n)
    else:
        print('已经没有票了')
    mutex.release() # 释放锁

if __name__ == '__main__':
    mutex = Lock()
    t_1 = [] # 线程列表
    for i in range(10):
        t = Thread(target=task)
        t_1.append(t)
        t.start()
    for t in t_1:
        t.join()

运行结果:

购买成功,剩余9张电影票
购买成功,剩余8张电影票
购买成功,剩余7张电影票
购买成功,剩余6张电影票
购买成功,剩余5张电影票
购买成功,剩余4张电影票
购买成功,剩余3张电影票
购买成功,剩余2张电影票
购买成功,剩余1张电影票
已经没有票了

注意:

       使用互斥锁时,要避免思索,在多任务系统下,当一个或多个线程等待系统资源,而资源又被线程本身或其他线程占用时,就形成了死锁。

3.1.2 使用队列在线程间通信

       multiprocessing模块的Queue队列可以实现线程间通信。使用Queue队列在线程间通信通常应用与生产者消费者模式。产生数据的模块称为生产者,而处理数据的模块的模块称为消费者。在生产者与消费者之间的缓存区成为仓库。

案例: 使用Queue队列实现在线程间通信

       定义一个生产类Producer,定义一个消费者类Consumer,生产者生产5件商品,依次写入队列,而消费者依次从队列中取出产品

from queue import Queue
import random,threading,time
# 生产者类
class Producer(threading.Thread):
    def __init__(self,name,queue):
        threading.Thread.__init__(self,name=name)
        self.data = queue
    def run(self):
        for i in range(5):
            print('生产者%s将产品%d加入队列' %(self.getName(),i))
            self.data.put(i)
            time.sleep(random.random())
        print('生产者%s完成' % self.getName())
# 消费者类
class Consumer(threading.Thread):
    def __init__(self,name,queue):
        threading.Thread.__init__(self,name=name)
        self.data = queue

    def run(self):
        for i in range(5):
            val = self.data.get()
            print("消费者%s将产品%d从队列中取出" % (self.getName(),val))
            time.sleep(random.random())
        print('消费者%s完成' % self.getName())

if __name__ == '__main__':
    print('-----主线程开始-----')
    queue = Queue()        # 实例化队列
    producer = Producer('Producer',queue)   # 实例化线程Producer,并传入队列作为参数
    consumer = Consumer('Consumer',queue)   # 实例化线程Consumer,并传入队列作为参数
    producer.start()    # 启动线程Producer
    consumer.start()    # 启动线程Consumer
    producer.join()     # 等待线程Producer结束
    consumer.join()     # 等待线程Consumer结束
    print('-----主线程结束-----')
运行结果:
-----主线程开始-----
生产者Producer将产品0加入队列
消费者Consumer将产品0从队列中取出
生产者Producer将产品1加入队列
消费者Consumer将产品1从队列中取出
生产者Producer将产品2加入队列
消费者Consumer将产品2从队列中取出
生产者Producer将产品3加入队列
消费者Consumer将产品3从队列中取出
生产者Producer将产品4加入队列
消费者Consumer将产品4从队列中取出
生产者Producer完成
消费者Consumer完成
-----主线程结束-----

4、创建进程的常用方式

        在Python中有多个模块可以创建进程,比较常用的有os.fork()函数,multiprocessing模块和Pool进程池。由于os.fork()函数只适用于Unix/Linux/Mac系统上运行,在Windows操作系统上不可用。

4.1 使用multiprocessing模块创建进程

multiprocessing模块提供了一个Process类来代表一个进程对象
语法:
Process([group[,target[,name[,args[,jwargs]]]]])
参数说明:
group:参数未使用,值始终为None
target:表示当前进程启动时执行的可调用对象。
name:当前进程实例的别名
args:表示传递给target函数的参数元组
kwargs:表示传递给target函数的参数字典
from multiprocessing import Process
import time
#执行子进程代码
def process_sub(interval):
    time.sleep(2)
    print('我是子进程1')

#执行子进程代码
def process_sub2(interval):
    print('我是子进程2')

# 执行主进程
def main():
    print('主进程开始')
    # 实例化Process进程类,并调用test()函数,并传递元组参数(1,)
    p = Process(target=process_sub,args=(1,))
    p2 = Process(target=process_sub2, args=(2,))
    p.start()
    p2.start()
    p.join()
    p2.join()
    print('主进程结束')
if __name__ == '__main__':
    main()

除了p.start()外。

  • is_alive():判断进程实例是否还在执行
  • join([timeout]):是否等待进程实例执行结束,或等待多少秒。
  • start(): 启动进程实例(创建子进程)
  • run(): 如果没有给定target参数,对这个对象调用start()方法时,就将执行对象中的run()方法。
  • terminate():不管任务是否完成,立即终止。

Process类属性:

name: 当前进程实例别名,默认为Process-N,N为从1开始递增的整数。

pid:当前进程实例的PID值。

案例: Process的方法和属性的使用

from multiprocessing import Process
import time
import os
#两个子进程将会调用的两个方法
def  child_1(interval):
    print("子进程(%s)开始执行,父进程为(%s)" % (os.getpid(), os.getppid()))
    t_start = time.time()   # 计时开始
    time.sleep(interval)    # 程序将会被挂起interval秒
    t_end = time.time()     # 计时结束
    print("子进程(%s)执行时间为'%0.2f'秒"%(os.getpid(),t_end - t_start))

def  child_2(interval):
    print("子进程(%s)开始执行,父进程为(%s)" % (os.getpid(), os.getppid()))
    t_start = time.time()   # 计时开始
    time.sleep(interval)    # 程序将会被挂起interval秒
    t_end = time.time()     # 计时结束
    print("子进程(%s)执行时间为'%0.2f'秒"%(os.getpid(),t_end - t_start))

if __name__ == '__main__':
    print("------父进程开始执行-------")
    print("父进程PID:%s" % os.getpid())   # 输出当前程序的PID
    p1=Process(target=child_1,args=(1,))    # 实例化进程p1
    # 实例化进程p2
    p2=Process(target=child_2,name="mrsoft",args=(2,))  
    p1.start()  # 启动进程p1
    p2.start()  # 启动进程p2
    #同时父进程仍然往下执行,如果p2进程还在执行,将会返回True
    print("p1.is_alive=%s"%p1.is_alive())
    print("p2.is_alive=%s"%p2.is_alive())
    #输出p1和p2进程的别名和PID
    print("p1.name=%s"%p1.name)
    print("p1.pid=%s"%p1.pid)
    print("p2.name=%s"%p2.name)
    print("p2.pid=%s"%p2.pid)
    print("------等待子进程-------")
    p1.join() # 等待p1进程结束
    p2.join() # 等待p2进程结束
    print("------父进程执行结束-------")

4.2 使用Process子类创建进程

        对于一些简单的小任务,通常使用Process(target=test)方式实现多进程。但是如果要处理复杂任务的进程,通常定义一个类,使其继承Process类,每次实例化这个类的时候,就等同于实例化一个进程对象。

案例: 使用Process子类创建多个进程

from multiprocessing import Process
import time
import os

class SubProcess(Process):
    def __init__(self,interval,name=''):
        Process.__init__(self)
        self.interval = interval
        if name:
            self.name = name

    # 重写Process类的run()方法
    def run(self):
        print('子进程(%s)开始执行,父进程为(%s)' % (os.getpid(),os.getppid()))
        t_start = time.time()
        time.sleep(self.interval)
        t_stop = time.time()
        print('子进程(%s)执行结束,耗时%0.2f秒' % (os.getpid(),t_stop - t_start))

if __name__ == '__main__':
    print('----------父进程开始执行----------')
    print('父进程PID %s' % os.getpid())
    p1 = SubProcess(interval=1,name='mrsoft')
    p2 = SubProcess(interval=2)
    p1.start()
    p2.start()
    print('p1.is_alive=%s' % p1.is_alive())
    print('p2.is_alive=%s' % p2.is_alive())
    print('p1.name=%s' % p1.name)
    print('p1.pid=%s' % p1.pid)
    print('p2.name=%s' % p2.name)
    print('p2.pid=%s' % p2.pid)
    print('----------等待子进程----------')
    p1.join()
    p2.join()
    print('父进程执行结束')

4.3 使用进程池Pool创建进程

        如果要创建几十个或者上百个进程,则需要实例化更多个Process类。解决该问题:使用multiprocessing模块提供的Pool类。即Pool进程池。

语法:

apply_async(func[,args[,kwds]]):使用非阻塞方式调用func函数(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传输给func的参数列表,kwds为传递给func的关键字参数列表。

apply(func[,args[,kwds]]):使用阻塞方式调用func函数。

close():关闭进程池,使其不再接受新的任务。

terminate():不管任务是否完成,立即终止。

join():主进程阻塞,等待子进程的退出,必须在close()或terminate()方法之后使用。

apply_async()方法使用非阻塞方式调用函数,而apply()方法使用阻塞方式嗲用函数。如果使用阻塞方式,必须等待上一个进程退出才能执行下一个进程,而使用非阻塞方式,则可以并行执行3个进程。

案例:使用进程池创建多进程

        定义一个进程池,设置最大进程数为3,然后使用非阻塞方式执行10个任务,查看每个进程执行的任务

from multiprocessing import Pool
import os,time
def task(name):
    print('子进程(%s)执行task %s...' %  (os.getpid(),name))
    time.sleep(1)
    print('任务%d 执行完毕' % name)

if __name__ == '__main__':
    print('父进程(%s)' % os.getpid())
    p = Pool(3)
    for i in range(10):
        # 使用非阻塞方式调用task()函数
        p.apply_async(task,args=(i,))
    print('------------start---------------')
    p.close()  # 关闭进程池,关闭后p不再接收新的请求
    p.join()   # 等待子进程结束
    print('----------------end-----------------')

运行结果:

父进程(16480)
------------start---------------
子进程(14556)执行task 0...
子进程(17788)执行task 1...
子进程(6852)执行task 2...
任务2 执行完毕
任务0 执行完毕
子进程(6852)执行task 3...
任务1 执行完毕
子进程(17788)执行task 5...
子进程(14556)执行task 4...
任务3 执行完毕
任务4 执行完毕
子进程(6852)执行task 6...
任务5 执行完毕
子进程(14556)执行task 7...
子进程(17788)执行task 8...
任务6 执行完毕
任务8 执行完毕任务7 执行完毕

子进程(6852)执行task 9...
任务9 执行完毕
----------------end-----------------

4.4 进程间通信

案例:验证进程之间是否直接共享信息

from multiprocessing import Process
import time
def plus():
    print('-------子进程1开始----------')
    global g_num
    g_num += 50
    time.sleep(2)
    print('进程1:_num is %d' % g_num)
def minus():
    print('-------子进程2开始----------')
    global g_num
    g_num -=50
    print('进程2:g_num is %d' % g_num)

g_num = 100
if __name__ == '__main__':
    print('主进程开始')
    print('原g_num is %d' % g_num)
    p1 = Process(target=plus)
    p2 = Process(target=minus)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print('主进程结束')

说明:

    由于g_num变量在父进程和2个子进程中的初始值是100,也就是全局变量g_num在一个进程的结果,没有传递到下一个进程中,即进程之间没有共享信息。如何实现才能实现进程间的通信呢,使用multiprocessing模块包装了底层的机制,提供了Queue(队列),Pipes(管道)等多种方式来交换数据。

4.4.1 多进程队列的使用

        初始化Queue()对象时,(例如: q=Queue(num)),若括号中没有指定最大可接收的消息数量,或数量为负值,那么久代表可接受的消息数量没有上限(知道内存的尽头)。queue的常用方法

Queue.qsize():返回当前队列包含的消息数量

Queue.empty():如果队列为空,放回True,否则返回False

Queue.full(): 如果队列满了,返回True,否则返回False

Queue.get([block[,timeouot]]):获取队列中的一条消息,然后将其从队列中移出,block默认值为True

如果block使用默认值,且没有设置timeout,消息队列为空,此时程序将被阻塞(停在读取状态),直到从消息队列读取到消息为止,如果设置了timeout,则会等待timeout秒,若还没有读取到任何消息,则抛出Queue.Empty异常。

如果block值为False,当消息队列为空,则会like抛出Queue.Empty异常。

Queue.get_nowait():相当于Queue.get(False)

Queue.put(item,[block,[,timeout]]):将item消息写入到队列,block默认值为True

如果block使用默认值,且没有设置timeout,当消息队列已经没有空间可写入时,程序将被阻塞(停在写入状态),知道从消息队列腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没有空间,则抛出Queue.Full异常。

如果block值为False,当消息队列没有空间可写入时,则会立即抛出Queue.Full异常。

Queue.put_nowait(item):相当于Queue.put(item,False)

案例:使用processing.Queue实现多进程队列

from multiprocessing import Queue
if __name__ == '__main__':
    q = Queue(3) # 最多可接收3条消息
    q.put('消息1')
    q.put('消息2')
    print('q.full():',q.full())
    q.put('消息3')
    print('q.full():', q.full())
    try: # 等待2秒后抛出异常
        q.put('消息4',True,2)
    except:
        print('消息队列已满,现有消息数量:%s' % q.qsize())

    try: # 立即抛出异常
        q.put_nowait('消息4')
    except:
        print('消息队列已满,现有消息数量:%s' % q.qsize())

    if not q.empty():
        print('---从队列中获取消息----')
        for i in range(q.qsize()):
            print(q.get_nowait())
        # 先判断消息队列是否已满,不满时再写入
        if not q.full():
            q.put_nowait('消息4')

运行结果:

q.full(): False
q.full(): True
消息队列已满,现有消息数量:3
消息队列已满,现有消息数量:3
---从队列中获取消息----
消息1
消息2
消息3

4.4.2 使用队列在进程中通信

案例:使用队列实现在进程间通信

        创建2个子进程,一个子进程负责向队列中写入数据,另一个子进程负责从队列中读取数据,为了保证能够正确从队列中读取数据,设置读取数据的进程等待时间为2秒,如果2秒后仍然无法读取数据,则抛出异常
from multiprocessing import Process,Queue
import time
# 向队列中写入数据
def writer_task(q):
    if not q.full():
        for i in range(5):
            message = '消息' + str(i)
            q.put(message)
            print('写入:%s' % message)
# 从队列读取数据
def read_task(q):
    time.sleep(1)
    while not q.empty():
        print('读取:%s'% q.get(True,2)) # 等待2秒还没有读取到任何消息抛出异常

if __name__ == '__main__':
    print('----------主进程开始----------')
    q = Queue()
    pw = Process(target=writer_task,args=(q,))
    pr = Process(target=read_task,args=(q,))
    pw.start()
    pr.start()
    pw.join()
    pr.join()
    print('----------主进程结束----------')

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小马哥-码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值