多进程和多线程爬虫

如果只使用单线程的爬虫,效率会非常低。通常有实用价值的爬虫会使用多线程和多进程,这样可以很多工作同时完成,尤其在多CPU的机器上,执行效率更是惊人。
标题三之后的代码可看;另外补充标题3.3之后的都看不懂。

一.进程与线程的区别

线程和进程都可以让程序并行运行。

1.1进程

计算机程序有静态和动态的区别。静态的计算机程序就是存储在磁盘上的可执行二进制(或其他类型)文件,而动态的计算机程序就是将这些可执行文件加载到内存中并被操作系统调用,这些动态的计算机程序被称为一个进程,也就是说,进程是活跃的,只有可执行程序被调入内存中才叫进程。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。操作系统会管理系统中的所有进程的执行,并为这些进程合理的分配时间。进程可以通过派生新的进程来执行其他任务,不过由于每个新进程也都拥有自己的内存和数据栈,所以只能采用进程间通信(IPC)的方式共享信息。

1.2线程

线程(有时候也被称为轻量级进程)与进程类似,不过线程是在同一进程下执行的,并共享同一个上下文。也就是说,线程属于进程,而且线程必须依赖进程才能执行。一个进程可以包含一个或多个线程。
线程包括开始、执行和结束三部分。他有一个指令指针,用于记录当前运行的上下文,当其他线程运行时,当前线程有可能被抢占(终端)或临时挂起(睡眠).
一个进程的各个线程与主线程共享同一块数据空间,因此相对于独立的进程而言,线程间的信息共享和通信更容易。线程是以并发方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作称为可能。当然,在单核CPU的系统中,并不存在真正的并发运行,所以线程的执行实际上还是同步执行的,只是系统会根据调度算法在不同的时间安排某个线程在CPU上执行一小会儿,然会就会让其他的线程在CPU上再执行一会儿,通过这种多个线程之间不断切换的方式让多个线程交替执行。因此,从宏观上看,即使在单核CPU的系统上仍然像多个线程并发运行一样。
当然,多线程之间共享数据并不是没有风险的。如果两个或多个线程访问了同一块数据,由于数据访问顺序不同,可能导致结果的不一致。这种情况通常称为静态条件,幸运的是,大多数线程库都有一些机制让共享内存区域的数据同步,也就是说,当一个线程访问这片内存区域时,这片内存区域就暂时被锁定了,其他的线程就只能等待这片内存区域解锁后再访问。
要注意的是,线程的执行时间是不平均的,例如,有6个线程,6秒的CPU执行时间,并不是为6这6个线程平均分配CPU执行时间(每个线程1秒),而是根据线程中具体的执行代码分配CPU计算时间。例如,在调动一些函数时,这些函数会在完成之前保存阻塞状态(阻止其他线程获得CPU执行时间),这样函数就会长时间占用CPU资源,通常来讲,系统在分配CPU计算时间时会更倾向于这些贪婪的函数。

二.Python与线程

Python多线程在底层使用了兼容POSIX的线程,也就是众所周知的pthread.

2.1使用单线程执行程序

本例使用Python单线程调用两个函数:fun1和fun2,在这两个函数中都使用了sleep函数休眠一定时间,如果用单线程调用这两个函数,那么会顺序执行这两个函数,也就是说,直到第一个函数执行完,才会执行第二个函数。

from time import sleep, ctime
def fun1():
    print('开始运行fun1:', ctime())
    # 休眠4秒
    sleep(4)
    print('fun1运行结束:', ctime())

def fun2():
    print('开始运行fun2:', ctime())
    # 休眠2秒
    sleep(2)
    print('fun2运行结束:', ctime())

def main():
    print('开始运行时间:', ctime())
    # 在单线程中调用fun1函数和fun2函数
    fun1()
    fun2()
    print('结束运行时间:', ctime())

if __name__ == '__main__':
    main()

2.2使用多线程执行程序

使用_thread模块中的start_new_thread函数会直接开启一个线程,该函数的第一个参数需要指定一个函数,可以把这个函数称为线程函数,当线程启动时会自动调用这个函数。start_new_thread函数的第2个参数是给线程函数传递的参数,必须是元组类型。
thread.start_new_thread函数可以判断哪个函数内阻塞时间长,他反正会先运行阻塞时间短的函数,然后阻塞时间长的函数在这个阻塞短的时间内可以同样的等待,这样就可以达到多线程的目的
它的执行顺序是谁的阻塞时间短就先运行睡

2.2.1本例使用多线程调用fun1函数和fun2函数,这两个函数会交替执行

介绍:thread原本是python2中的模块,在python3中被threading替换,但为了兼容python2的程序,在
python3中将thread模块重命名为"_thread"

在锁定对象_thread中包含如下方法

lock.acquire(waitflag=1,timeout=-1):里面的是参数值设置,如果设置了第一个参数,那么将依赖该参数的值:如果该参数为0,则
锁不等待且立即返回;不为0,则锁无条件的等待。
第二个参数的值若大于0,则指定等待的最大秒数,若参数为负数,则表示永久等待,如果第一个参数为0,则无法设置第二个参数。(一般情况下都不设置参数,一次仅一个线程能获取锁)
lock.release():释放锁。锁必须早已获取,但不一定由同一线程获取
lock.locked():返回锁的状态,True表示以被某个线程获取,False表示没有被某个线程获取

import _thread as thread

from time import sleep, ctime
def fun1():
    print('开始运行fun1:', ctime())
    # 休眠4秒
    sleep(4)
    print('fun1运行结束:', ctime())

def fun2():
    print('开始运行fun2:', ctime())
    # 休眠2秒
    sleep(2)
    print('fun2运行结束:', ctime())

def main():
    print('开始运行时间:', ctime())
    # 启动一个线程运行fun1函数
    thread.start_new_thread(fun1, ())
    '''
    start_new_thread()函数的第二个参数必须是元组,第一个参数必须是线程函数
    '''
    # 启动一个线程运行fun2函数
    thread.start_new_thread(fun2, ())
    # 休眠6秒
    sleep(6)
    print('结束运行时间:', ctime())

if __name__ == '__main__':
    main()

2.2.2为线程函数传递参数

通过start_new_thread函数的第2个参数可以为线程函数传递参数,该参数类型必须是元组。
本例利用for循环和start_new_thread函数启动8个线程,并未每一个线程函数传递不同的参数值,然后在线程函数中输出传入的参数值。
thread.start_new_thread(fun, (i + 1,'a' * (i + 1)))尽然还可以通过参数为函数中传递参数,也对,thread.start_new_thread的第二个参数必须是元组,就该表元素不指一个,它的第二个参数的作用就是给函数传参。第一个参数是函数名。

import random
from time import sleep
import _thread as thread
# 线程函数,其中a和b是通过start_new_thread函数传入的参数
def fun(a,b):
    print(a,b)
    # 随机休眠一个的时间(1到4秒)
    sleep(random.randint(1,5))
    '''
    print()函数的执行结果;可以得知该多线程是无序的
    1 a
4 aaaa    
2 aa      
5 aaaaa   
8 aaaaaaaa
6 aaaaaa  
3 aaa     
7 aaaaaaa 
    '''
#  启动8个线程
for i in range(8):
    # 为每一个线程函数传入2个参数值
    thread.start_new_thread(fun, (i + 1,'a' * (i + 1)))
# 通过从终端输入一个字符串的方式让程序暂停
input() # 去掉该input函数,就不会将print()函数的内容打印在终端

在main函数的最后需要使用sleep函数让程序处理休眠状态,或使用input函数从终端采集一个字符串,目的是让程序暂停,其实这些做法的目的只有一个,在所有的线程执行完之前,阻止程序退出。

2.3线程和锁

本例启动2个线程,并创建2个锁,在运行线程函数之前,获取这2个锁,这就意味着锁处于锁定状态,然后在启动线程时将这2个锁对象分别传入2个线程各自的锁对象,当线程函数执行完,会调用锁对象是否已经释放,只要有一个锁对象没释放,while循环就不会退出,如果2个锁对象都释放了,那么main函数就立刻结束,程序退出。

在不同的函数为参数下,多线程的执行是同时开始的,然后谁的阻塞时间短,谁就先执行完

在相同的函数传递不同的参数,好像也是这样,看谁的阻塞时间短,谁就先执行完

import _thread as thread
from time import sleep, ctime
# 线程函数,index是一个整数类型的索引,sec是休眠时间(单位:秒),lock是锁对象
def fun(index, sec,lock):
    print('开始执行', index,'执行时间:',ctime())
    # 休眠sec秒
    sleep(sec)
    print('执行结束',index,'执行时间:',ctime())
    # 释放锁对象
    lock.release()

def main():
    # 创建第1个锁对象
    lock1 = thread.allocate_lock()
    # 获取锁(相当于把锁锁上)
    lock1.acquire()
    # 启动第1个线程,并传入第1个锁对象,10是索引,4是休眠时间,lock1是锁对象
    thread.start_new_thread(fun,(10, 4, lock1))
    # 创建第2个锁对象
    lock2 = thread.allocate_lock()
    # 获取锁(相当于把锁锁上)
    lock2.acquire()
    # 启动第2个线程,并传入第2个锁对象,20是索引,2是休眠时间,lock2是锁对象
    thread.start_new_thread(fun,
            (20, 2, lock2))
    # 使用while循环和locked方法判断lock1和lock2是否被释放
    # 只要有一个没有释放,while循环就不会退出
    while lock1.locked() or lock2.locked():
        pass
if __name__ == '__main__':
    main()

三.高级线程模块(threading)

threading模块中有一个非常重要的Thread类,该类的实例表示一个执行线程的对象。在前面讲的_thread模块可以看作线程的面向过程版本,而Thread类可以看作线程的面向对象版本。

threading模块创建线程有两种方式

一种是通过继承Thread类,重写它的run方法;
另一种是创建一个threading.Thread对象,在他的初始化函数中(init)将可调用对象作为参数传入
本例(3.2Thread类与线程对象)就是用的是第二种。使用target关键字将类包裹函数传入,这个类可复用,就是参数需要研究;不用该类也可以,该类就是继承Thread类相似(指的是函数中的参数)。

3.1Thread类与线程函数

使用args关键字参数赋值传进函数的参数
这里的多线程对象thread1.join()执行完毕的作用是线程同步;来源于Python多线程与多线程中join()的用法
使用Thread类也很简单,首先需要创建Thread类的实例,通过Thread类构造方法的target关键字参数执行线程函数,通过args关键字参数指定传给线程函数的参数。然后调用Thread对象的start方法启动线程。
本例使用Thread对象启动2个线程,并在各自的线程函数中使用sleep函数休眠一段时间。最后使用Thread对象的join方法等待2个线程函数都执行完再退出程序。

在threading模块中,除包含"_thread"模块中的所有方法之外,还提供了如下的核心方法

.currentThread():返回当前的Thread对象,这是一个线程变量
.enumerate:返回一个包含正在运行的线程的列表
.activeCount():返回正在运行的线程数量
.main_thread():返回主thread对象。正常情况下,主线程是从python解释器中启动的线程
settrace(func):为所有从threading模块启动的线程设置一个跟踪方法。在每个线程的run()方法调用之前,func将传递给sys.settrace()
.setprofile(func):为所有从threading模块启动的线程设置一个profile()方法
还提供.TIMEOUT_MAX,该方法表示阻塞方法允许等待的最长时限,设置超过此值的超时将会引发OverflowError

import threading

from time import sleep, ctime
# 线程函数,index表示整数类型的索引,sec表示休眠时间,单位:秒
def fun(index, sec):
    print('开始执行', index, ' 时间:', ctime())
    # 休眠sec秒
    sleep(sec)
    print('结束执行', index, '时间:', ctime())
def main():
    # 创建第1个Thread对象,通过target关键字参数指定线程函数fun,传入索引10和休眠时间(4秒)
    thread1 = threading.Thread(target=fun,
            args=(10, 4))
    # 启动第1个线程
    thread1.start()
    # 创建第2个Thread对象,通过target关键字参数指定线程函数fun,传入索引20和休眠时间(2秒)
    thread2 = threading.Thread(target=fun,
            args=(20, 2))
    # 启动第2个线程
    thread2.start()
    # 等待第1个线程函数执行完毕
    thread1.join()
    # 等待第2个线程函数执行完毕
    thread2.join()

if __name__ == '__main__':
    main()

3.2Thread类与线程对象

线程对象对应的类需要有一个可以传入线程函数和参数的构造方法,而且在类中还必须有一个名为"call"的方法。当线程启动时,会自动调用线程对象的"call"方法,然后在该方法中调用线程函数。
本例使用Thread类的实例启动线程时,通过Thread类构造方法传入了一个线程对象,并通过线程对象指定了线程函数和对应的参数。

import threading
from time import sleep, ctime
# 线程对象对应的类
class MyThread(object):
    # func表示线程函数,args表示线程函数的参数
    def __init__(self, func, args):
    # 将线程函数与线程函数的参数赋给当前类的成员变量
        self.func = func
        self.args = args
    # 线程启动时会调用该方法
    def __call__(self):
    # 调用线程函数,并将元组类型的参数值分解为单个的参数值传入线程函数
        self.func(*self.args)
# 线程函数
def fun(index, sec):
    print('开始执行', index, ' 时间:', ctime())
    # 延迟sec秒
    sleep(sec)
    print('结束执行', index, '时间:', ctime())
def main():
    print('执行开始时间:', ctime())
    # 创建第1个线程,通过target关键字参数指定了线程对象(MyThread),延迟4秒
    thread1 = threading.Thread(target =fun,args=(10, 4)) # 可以用args传入函数的参数
    # 启动第1个线程
    thread1.start()
    # 创建第2个线程,通过target关键字参数指定了线程对象(MyThread),延迟2秒
    thread2 = threading.Thread(target = MyThread(fun,(20, 2)))
    # 启动第2个线程
    thread2.start()
    # 创建第3个线程,通过target关键字参数指定了线程对象(MyThread),延迟1秒
    thread3 = threading.Thread(target = MyThread(fun,(30, 1)))
    # 启动第3个线程
    thread3.start()
    # 等待第1个线程函数执行完毕
    thread1.join()
    # 等待第2个线程函数执行完毕
    thread2.join()
    # 等待第3个线程函数执行完毕
    thread3.join()
    print('所有的线程函数已经执行完毕:', ctime())
if __name__ == '__main__':
    main()

3.3从Thread类继承

为了更好的对线程有关的代码进行封装,可以从Thread类派生一个子类。然后将与线程有关的代码都放到这个类中。Thread类的子类的使用方法与Thread相同。从Thread类继承最简单的方式是在子类的构造方法中通过super()函数调用父类的构造方法,并传入相应的参数值。
本例编写了一个从Thread类继承的子类MyThread,重写父类的构造方法和run方法。最后通过MyThread类创建并启动两个线程,并使用join方法等待这两个线程结束后再退出程序。

该程序下的self._args与self.args的区别,好像就是不能用.args,默认使用的就是._args

import threading
from time import sleep, ctime


# 从Thread类派生的子类
class MyThread(threading.Thread):
    # 重写父类的构造方法,其中func是线程函数,args是传入线程函数的参数,name是线程名
    def __init__(self, func, args, name=''):
        # 调用父类的构造方法,并传入相应的参数值
        super().__init__(target=func, name=name,
                         args=args)

    # 重写父类的run方法
    def run(self):
        self._target(*self._args)


# 线程函数
def fun(index, sec):
    print('开始执行', index, '时间:', ctime())
    # 休眠sec秒
    sleep(sec)
    print('执行完毕', index, '时间:', ctime())


def main():
    print('开始:', ctime())
    # 创建第1个线程,并指定线程名为“线程1”
    thread1 = MyThread(fun, (10, 4), '线程1')
    # 创建第2个线程,并指定线程名为“线程2”
    thread2 = MyThread(fun, (20, 2), '线程2')
    # 开启第1个线程
    thread1.start()
    # 开启第2个线程
    thread2.start()
    # 输出第1个线程的名字
    print(thread1.name)
    # 输出第2个线程的名字
    print(thread2.name)
    # 等待第1个线程结束
    thread1.join()
    # 等待第2个线程结束
    thread2.join()

    print('结束:', ctime())


if __name__ == '__main__':
    main()

四.线程同步(不哭护额)

多线程的目的就是让多段程序并发运行,但在一些情况下,让多段程序同时运行会造成很多麻烦,如果这些并发运行的程序还共享数据,有可能造成脏数据以及其他数据不一致的后果,这里的脏数据是指由于多端程序同时读写一个或一组变量,由于读写顺序的问题导致最终的结果与期望的不一样。
例如,有一个整数变量n,初始值为1,现在要为该变量加1,然后输出该变量的值,目前有两个线程(Thread1和Thread2)做同样的工作。当Thread1为变量n加1后,这时CPU的计算时间恰巧被Thread2夺走,在执行Thread2的线程函数时又对变量n加1,所以目前n被加了两次1,变成了3.这时不管是继续执行Thread2,还是接着执行Thread1,输出的n都会等于3.这就意味着n等于2的值没有输出,如果正好在n=2时需要更多的处理,这就意味着n等于2的值没有输出,如果正好在n等于2时需要做更多的处理,这就意味着这些工作都不会按预期完成了,因为这时已经等于3了。这个变量当前的值称为脏数据,就是说n原本应该等于2的,而现在却等于3了。这一过程可以看下面的线程函数

n = 1
# 如果用多个线程执行fun函数,就有可能造成n持续加1,而未处理的情况
def fun():
    n +=1
    print(n) # 此处可能有更多的代码

解决这个问题的最好方法就是将改变变量n和输出变量n的语句变成原子操作,在Python线程中可以用线程锁来达到这个目的。

4.1线程锁

线程锁的目的是将一段代码锁住,一旦获得了锁权限,除非释放线程锁,否则其他任何代码都无法再次获得锁权限。
为了使用线程锁,首先需要创建Lock类的实例,然后通过Lock对象的acquire方法获取锁权限,当需要完成原子操作的代码段执行完后,再使用Lock对象的release方法释放锁,这样其他代码就可以再次获得这个锁权限了。要注意的是,锁对象要放到线程函数的外面作为一个全局变量,这样所有的线程函数实例都可以共享这个变量,如果将锁对象放到线程函数内部,那么这个锁对象就变成局部变量了,多个线程函数实例使用的是不同的锁对象,所以仍然不能有效保护原子操作的代码。
本例在线程函数中使用for循环输出线程名和循环变量的值,并通过线程锁将这段代码变成原子操作,这样就只有当前线程函数的for循环执行完,其他线程函数的for循环才会重新获得线程锁权限并执行。

from atexit import register
import random
from threading import Thread, Lock, currentThread
from time import sleep, ctime
# 创建线程锁对象
lock = Lock()
def fun():
    # 获取线程锁权限
   # lock.acquire()
    # for循环已经变成了原子操作
    for i in range(5):
        print('Thread Name','=',currentThread().name,'i','=',i)
        # 休眠一段时间(1到4秒)
        sleep(random.randint(1,5))
    # 释放线程锁,其他线程函数可以获得这个线程锁的权限了
  #  lock.release()
def main():
    # 通过循环创建并启动了3个线程
    for i in range(3):
        Thread(target=fun).start()
# 当程序结束时会调用这个函数
@register
def exit():
    print('线程执行完毕:', ctime())
if __name__ == '__main__':
    main()

结论:如果为fun函数加上线程锁,那么只有当某个线程的线程函数执行完,才会运行另一个线程的线程函数。

4.2信号量

信号量是最古老的同步原语之一,他是一个计数器,用于记录资源消耗情况。当资源消耗时递减,当资源释放时递增。可以认为信号量代表资源是否可用。消耗资源使计数器递减的操作习惯上称为P,当一个线程对一个资源完成操作时,该资源需要返回资源池,这个操作一般称为V。Python语言统一了所有的命名,使用与线程锁同样的方法名消耗和释放资源。acquire方法用于消耗资源,调用该方法计数器会减1,release方法用于释放资源,调用该方法计数器加1.
使用信号量首先创建BoundedSemaphore类的实例,并且通过该类的构造方法传入计数器的最大值,然后就可以使用BoundedSemaphore对象的acquire方法和release方法获取资源(计数器减1)和释放资源(计数器加1)了。
本例演示了信号量对象的创建,以及获取与释放资源

from threading import BoundedSemaphore
MAX = 3
# 创建信号量对象,并设置了计数器的最大值(也是资源的最大值),计数器不能超过这个值
semaphore = BoundedSemaphore(MAX)
# 输出当前计数器的值,输出结果:3
print(semaphore._value)
# 获取资源,计数器减1
semaphore.acquire()
# 输出结果:2
print(semaphore._value)
# 获取资源,计数器减1
semaphore.acquire()
# 输出结果:1
print(semaphore._value)
# 获取资源,计数器减1
semaphore.acquire()
# 输出结果:0
print(semaphore._value)
# 当计数器为0时,不能再获取资源,所以acquire方法会返回False
# 输出结果:False
print(semaphore.acquire(False)) #人为返回False方法么
# 输出结果:0
print(semaphore._value)
# 释放资源,计数器加1
semaphore.release()
# 输出结果:1
print(semaphore._value)
# 释放资源,计数器加1
semaphore.release()
# 输出结果:2
print(semaphore._value)
# 释放资源,计数器加1
semaphore.release()
# 输出结果:3
print(semaphore._value)
# 抛出异常,当计数器达到最大值时,不能再次释放资源,否则会抛出异常
semaphore.release()

要注意的是信号量对象的acquire方法与release方法。当资源枯竭时(计数器为0)时调用acquire方法会有两种结果。第1种是acquire方法的参数值为True或不指定参数时,acquire方法会处于阻塞状态,直到使用release释放资源时,acquire方法才会往下执行。第2种是acquire方法的参数值为False,当计数器为0时调用acquire方法并不会阻塞,而是直接返回False,表示未获得资源,如果成功获得资源,会返回True.
release方法在释放资源时,如果计数器已经达到了最大值(本例是3),会直接抛出异常,表示已经没有资源释放了


本例通过信号量和线程锁模拟一个糖果机补充糖果和用户取得糖果的过程,糖果机有5个槽,如果发现每个槽都没有糖果了,需要补充新的糖果。当5个槽都满了,就无法补充新的糖果了,如果5个槽都是空的,顾客也无法购买糖果了。为了方便,本例假设顾客一次会购买整个槽的糖果,每次补充整个槽的糖果。

# 那么糖果机的案例会用在哪里呢,原子性,买卖供应保持不断
from atexit import register
from random import randrange
from threading import BoundedSemaphore, Lock, Thread
from time import sleep, ctime
# 创建线程锁
lock = Lock()
# 定义糖果机的槽数,也是信号量计数器的最大值
MAX = 5
# 创建信号量对象,并指定计数器的最大值
candytray = BoundedSemaphore(MAX)
# 给糖果机的槽补充新的糖果(每次只补充一个槽)
def refill():
    # 获取线程锁,将补充糖果的操作变成原子操作
    lock.acquire()
    print('重新添加糖果...', end=' ')
    try:
    # 为糖果机的槽补充糖果(计数器加1)
        candytray.release() # release()作用在计数器上计数器才加1,同理acquire()作用在计数器上计数器才减1
    except ValueError:
        print('糖果机都满了,无法添加')
    else:
        print('成功添加糖果')
    # 释放线程锁
    lock.release()
# 顾客购买糖果
def buy():
    # 获取线程锁,将购买糖果的操作变成原子操作
    lock.acquire()
    print('购买糖果...', end=' ')
    # 顾客购买糖果(计数器减1),如果购买失败(5个槽都没有糖果了),返回False
    if candytray.acquire(False):
        print('成功购买糖果')
    else:
        print('糖果机为空,无法购买糖果')
    # 释放线程锁
    lock.release()
# 产生多个补充糖果的动作
def producer(loops):
    for i in range(loops):
        refill() # 补充糖果的操作变成原子操作
        sleep(randrange(3))
# 产生多个购买糖果的动作
def consumer(loops):
    for i in range(loops):
        buy()
        sleep(randrange(3))

def main():
    print('开始:', ctime())
    # 参数一个2到5的随机数
    nloops = randrange(2, 6)
    print('糖果机共有%d个槽!' % MAX)
    # 开始一个线程,用于执行consumer函数
    Thread(target=consumer, args=(randrange(
        nloops, nloops+MAX+2),)).start()
    # 开始一个线程,用于执行producer函数
    Thread(target=producer, args=(nloops,)).start()

@register
def exit():
    print('程序执行完毕:', ctime())

if __name__ == '__main__':
    main()


五.生产者————消费者问题与queue模块

生产者————消费者模型.在这个场景下,商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中,生产商品的时间是不确定的,同样消费者生产者生产的商品的时间也是不确定的。
这里使用queue模块来提供线程间通信的机制,也就是说,生产者和消费者共享一个队列。生产者生产商品后,会将商品添加到队列中。消费者消费商品,会从队列中取一个商品。由于向队列中添加商品和从队列中获取商品都不是原子操作,所以需要使用线程锁将这两个操作锁住。
本例使用线程锁和队列实现一个生产者————消费者模型的程序。通过for循环产生若干个生产者和消费者,并向队列中添加商品,以及从队列中获取商品

from random import randrange
from time import sleep,time, ctime
from threading import Lock, Thread
from queue import Queue
# 创建线程锁对象
lock = Lock()
# 从Thread派生的子类
class MyThread(Thread):
    def __init__(self, func, args): # 仅仅是修改了下__init__方法
        super().__init__(target = func, args = args)
# 向队列中添加商品
def writeQ(queue):
    # 获取线程锁
    lock.acquire()
    print('生产了一个对象,并将其添加到队列中', end='  ')
    # 向队列中添加商品
    queue.put('商品') # 入队列
    print("队列尺寸", queue.qsize()) # qsize队列的属性
    # 释放线程锁
    lock.release()

# 从队列中获取商品
def readQ(queue):
    # 获取线程锁
    lock.acquire()
    # 从队列中获取商品
    val = queue.get(1)
    print('消费了一个对象,队列尺寸:', queue.qsize())
    # 释放线程锁
    lock.release()
# 生成若干个生产者
def writer(queue, loops):
    for i in range(loops):
        writeQ(queue) # 先添加商品在睡个几秒
        sleep(randrange(1, 4))
# 生成若干个消费者
def reader(queue, loops):
    for i in range(loops):
        readQ(queue) # 获取商品在睡个几秒
        sleep(randrange(2, 6))

funcs = [writer, reader] # 函数对象变为队列
nfuncs = range(len(funcs))

def main():
    nloops = randrange(2, 6) # 这个值因该只会传第一次
    q = Queue(32) # 

    threads = []
    # 创建2个线程运行writer函数和reader函数
    for i in nfuncs:
        t = MyThread(funcs[i], (q, nloops)) # q, nloops分别为传入的队列中和一个随机数
        threads.append(t)
    # 开始线程
    for i in nfuncs:
        threads[i].start()

    # 等待2个线程结束
    for i in nfuncs:
        threads[i].join() # jion()函数保证两个线程都能完成
    print('所有的工作完成')
if __name__ == '__main__':
    main()

六.多进程

如果使用的进程比较多,可以使用multiprocessing模块的进程池(Pool类),通过Pool类构造方法的Processes参数,可以指定创建的进程数。Pool类有一个map方法,用于将回调函数与要给回调函数传递的数据管理起来,代码如下

pool = Pool(processes=4)
pool.map(callback_fun,values)

上面的代码利用Pool对象创建了4个进程,并通过map方法指定了进程回调函数,当进程执行时,就会调用这个函数,values是一个可迭代对象,每次进程运行时,就会从values中取一个值传递给callback_fun,也就是说,callback_fun函数至少要有一个参数接收values中的值


本例使用Pool对象创建4个进程,这4个进程从values列表中取值,然后传入回调函数get_value中。

from multiprocessing import Pool
import time

# 进程回调函数
def get_value(value):
    
    i = 0
    while i < 3:
        # 休眠1秒
        time.sleep(1)
        print(value,i)
        i += 1


if __name__ == '__main__':
    # 产生5个值,供多进程获取
    values = ['value{}'.format(str(i)) for i in range(0,5)]
    # 创建4个进程
    pool = Pool(processes=4)
    # 将进程回调函数与values关联
    pool.map(get_value,values)

Tips:笔记来自Python爬虫技术————深入理解原理、技术与开发:第18章
笔记源码文件夹名parallel

posted @ 2021-09-08 16:33  索匣  阅读(408)  评论(4编辑  收藏  举报