如果只使用单线程的爬虫,效率会非常低。通常有实用价值的爬虫会使用多线程和多进程,这样可以很多工作同时完成,尤其在多CPU的机器上,执行效率更是惊人。
一.进程与线程的区别
线程和进程都可以让程序并行运行。
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个参数是给线程函数传递的参数,必须是元组类型。
2.2.1本例使用多线程调用fun1函数和fun2函数,这两个函数会交替执行

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, ())
    # 启动一个线程运行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个线程,并未每一个线程函数传递不同的参数值,然后在线程函数中输出传入的参数值。

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))
#  启动8个线程
for i in range(8):
    # 为每一个线程函数传入2个参数值
    thread.start_new_thread(fun, (i + 1,'a' * (i + 1)))
# 通过从终端输入一个字符串的方式让程序暂停
input()

2.3线程和锁
在main函数的最后需要使用sleep函数让程序处理休眠状态,或使用input函数从终端采集一个字符串,目的是让程序暂停,其实这些做法的目的只有一个,在所有的线程执行完之前,阻止程序退出。
本例启动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类可以看作线程的面向对象版本。
3.1Thread类与线程函数
使用Thread类也很简单,首先需要创建Thread类的实例,通过Thread类构造方法的target关键字参数执行线程函数,通过args关键字参数指定传给线程函数的参数。然后调用Thread对象的start方法启动线程。
本例使用Thread对象启动2个线程,并在各自的线程函数中使用sleep函数休眠一段时间。最后使用Thread对象的join方法等待2个线程函数都执行完再退出程序。

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 = MyThread(fun,(10, 4)))
    # 启动第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方法等待这两个线程结束后再退出程序。

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()
努力拼搏吧,不要害怕,不要去规划,不要迷茫。但你一定要在路上一直的走下去,尽管可能停滞不前,但也要走。