前言

在爬虫学习的过程中,一旦爬取的数量过大,很容易带来效率问题,为了能够快速爬取我们想要的内容。为此我们可以使用多线程或者多进程来处理。

多线程和多进程是不一样的!一个是 threading 库,一个是 multiprocessing 库。而多线程 threading 在 Python
里面被称作鸡肋的存在!关于 Python 多线程有这样一句名言——“Python下多线程是鸡肋,推荐使用多进程!”

为什么称 Python 多线程是鸡肋?原因如下:

在大多数环境中,单核CPU情况下,本质上某一时刻只能有一个线程被执行,多核 CPU 则可以支持多个线程同时执行。但是在 Python 中,无论 CPU
有多少核,CPU在同一时间只能执行一个线程。这是由于 GIL 的存在导致的。

GIL 的全称是 Global Interpreter Lock(全局解释器锁),是 Python 设计之初为了数据安全所做的决定。Python
中的某个线程想要执行,必须先拿到 GIL。可以把 GIL 看作是执行任务的“通行证”,并且在一个 Python 进程中,GIL
只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。GIL 只在 CPython 解释器中才有,因为 CPython 调用的是 c
语言的原生线程,不能直接操作 CPU,只能利用 GIL 保证同一时间只能有一个线程拿到数据。在 PyPy 和 JPython 中没有 GIL。

在 Python 多线程下,每个线程的执行方式:

* 拿到公共数据
* 申请GIL
* Python解释器调用操作系统原生线程
* cpu执行运算
* 当该线程执行一段时间消耗完,无论任务是否已经执行完毕,都会释放GIL
* 下一个被CPU调度的线程重复上面的过程

任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

那么是不是python的多线程就完全没用了呢?

Python 针对不同类型的任务,多线程执行效率是不同的:

* 对于 CPU 密集型任务(各种循环处理、计算等等),由于计算工作多,ticks 计数很快就会达到阈值,然后触发 GIL
的释放与再竞争(多个线程来回切换是需要消耗资源的),所以 Python 下的多线程对 CPU 密集型任务并不友好。
* IO 密集型任务(文件处理、网络通信等涉及数据读写的操作),多线程能够有效提升效率(单线程下有 IO 操作会进行 IO
等待,造成不必要的时间浪费,而开启多线程能在线程 A 等待时,自动切换到线程 B,可以不浪费 CPU 的资源,从而能提升程序执行效率)。所以 Python
的多线程对 IO 密集型任务比较友好。
实际中的建议

Python 中想要充分利用多核 CPU,就用多进程。因为每个进程有各自独立的 GIL,互不干扰,这样就可以真正意义上的并行执行。在 Python
中,多进程的执行效率优于多线程(仅仅针对多核 CPU 而言)。同时建议在 IO 密集型任务中使用多线程,在计算密集型任务中使用多进程。

Python 创建多线程

1.直接利用函数创建多线程

在 Python3 中,Python 提供了一个内置模块 threading.Thread,可以很方便地让我们创建多线程。
threading.Thread()一般接收两个参数:

* target(线程函数名):要放置线程让其后台执行的函数,由我们自已定义,注意不要加();
* args(线程函数的参数):线程函数名所需的参数,以元组的形式传入。若不需要参数,可以不指定。 import time from threading
import Thread # 自定义线程函数。 def main(name="Python"): for i in range(2):
print("hello", name) time.sleep(2) # 创建线程01,不指定参数 thread_01 =
Thread(target=main) # 启动线程01 thread_01.start() # 创建线程02,指定参数,注意逗号 thread_02 =
Thread(target=main, args=("MING",)) # 启动线程02 thread_02.start()
输出结果:
hello Python hello MING hello Python hello MING
2. 使用 Threading 模块构建类对象

使用 Threading 模块创建线程,直接从 threading.Thread 继承,然后重写 init 方法和 run 方法。
import time from threading import Thread class MyThread(Thread): def
__init__(self, name="Python"): # 注意,super().__init__() 一定要写 # 而且要写在最前面,否则会报错。
super().__init__() self.name=name def run(self): for i in range(2):
print("hello", self.name) time.sleep(2) if __name__ == '__main__': #
创建线程01,不指定参数 thread_01 = MyThread() # 创建线程02,指定参数 thread_02 = MyThread("MING")
thread_01.start() thread_02.start()
输出结果:
hello Python hello MING hello MING hello Python
上述是最基本的多线程创建方法,看起来很简单,但在实际的应用中,会复杂许多。

接下来,关于我实际学习中的多线程练习,我会一一记录下来。

多线程练习

在讲述下面的线程同步前,我先用个实例描述一下线程未同步。
import threading import time class myThread(threading.Thread): def
__init__(self, threadID, name, counter): threading.Thread.__init__(self)
self.threadID = threadID self.name = name self.counter = counter def run(self):
print('Starting '+self.name) print_time(self.name, self.counter, 3)
print("Exiting " + self.name) def print_time(threadName, delay, count): while
count: time.sleep(1) print('{0} processing {1}'.format(threadName,
time.ctime(time.time()))) count -= 1 if __name__ == '__main__': threadList =
["Thread-1", "Thread-2"] threads = [] threadID = 1 for tName in threadList:
thread = myThread(threadID, tName, threadID) thread.start()
threads.append(thread) threadID += 1 # for t in threads: # t.join()
print("Exiting Main Thread")
输出结果:
Starting Thread-1Starting Thread-2 Exiting Main Thread Thread-1 processing Sat
May 11 11:06:11 2019 Thread-2 processing Sat May 11 11:06:12 2019 Thread-1
processing Sat May 11 11:06:12 2019 Thread-1 processing Sat May 11 11:06:13
2019 Exiting Thread-1 Thread-2 processing Sat May 11 11:06:14 2019 Thread-2
processing Sat May 11 11:06:16 2019 Exiting Thread-2
由输出结果中可以看出,第二个线程在打印过程中没有回车打印,说明线程间没有同步。
线程同步——Lock

多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。
import threading import queue import time exitFlag = 0 class
myThread(threading.Thread): def __init__(self, que):
threading.Thread.__init__(self) self.que = que def run(self): print("Starting "
+ threading.currentThread().name) process_data(self.que) print("Ending " +
threading.currentThread().name) def process_data(que): while not exitFlag:
queueLock.acquire() if not workqueue.empty(): data = que.get()
queueLock.release() print('Current Thread Name %s, data: %s ' %
(threading.currentThread().name, data)) else: queueLock.release()
time.sleep(0.1) if __name__ == '__main__': start_time = time.time() queueLock =
threading.Lock() workqueue = queue.Queue() threads = [] # 填充队列
queueLock.acquire() for i in range(1,10): workqueue.put(i) queueLock.release()
for i in range(4): thread = myThread(workqueue) thread.start()
threads.append(thread) #等待队列清空 while not workqueue.empty(): pass #通知线程退出
exitFlag = 1 for t in threads: t.join() print("Exiting Main Thread") end_time =
time.time() print('耗时{}s'.format((end_time - start_time)))
输出结果:
Starting Thread-1 Current Thread Name Thread-1, data: 1, time: Sat May 11
10:47:25 2019 Starting Thread-2 Current Thread Name Thread-2, data: 2, time:
Sat May 11 10:47:25 2019 Starting Thread-3 Current Thread Name Thread-3, data:
3, time: Sat May 11 10:47:25 2019 Starting Thread-4 Current Thread Name
Thread-4, data: 4, time: Sat May 11 10:47:25 2019 Current Thread Name Thread-1,
data: 5, time: Sat May 11 10:47:25 2019 Current Thread Name Thread-2, data: 6,
time: Sat May 11 10:47:25 2019 Current Thread Name Thread-3, data: 7, time: Sat
May 11 10:47:25 2019 Current Thread Name Thread-4, data: 8, time: Sat May 11
10:47:25 2019 Current Thread Name Thread-2, data: 9, time: Sat May 11 10:47:25
2019 Ending Thread-4 Ending Thread-1 Ending Thread-2 Ending Thread-3 Exiting
Main Thread 耗时0.31415677070617676s
线程同步——Queue

Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括 FIFO(先入先出)队列
Queue,LIFO(后入先出)队列LifoQueue,和优先级队列
PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
import threading import time import queue def get_num(workQueue):
print("Starting " + threading.currentThread().name) while True: if
workQueue.empty(): break num = workQueue.get_nowait() print('Current Thread
Name %s, Url: %s ' % (threading.currentThread().name, num)) time.sleep(0.3) #
try: # num = workQueue.get_nowait() # if not num: # break # print('Current
Thread Name %s, Url: %s ' % (threading.currentThread().name, num)) # except: #
break # time.sleep(0.3) print("Ending " + threading.currentThread().name) if
__name__ == '__main__': start_time = time.time() workQueue = queue.Queue() for
i in range(1,10): workQueue.put(i) threads = [] thread_num = 4 #线程数 for i in
range(thread_num): t = threading.Thread(target=get_num, args=(workQueue,))
t.start() threads.append(t) for t in threads: t.join() end_time = time.time()
print('耗时{}s'.format((end_time - start_time)))
输出结果:
Starting Thread-1 Current Thread Name Thread-1, data: 1, time: Sat May 11
10:48:27 2019 Starting Thread-2 Current Thread Name Thread-2, data: 2, time:
Sat May 11 10:48:27 2019 Starting Thread-3 Current Thread Name Thread-3, data:
3, time: Sat May 11 10:48:27 2019 Starting Thread-4 Current Thread Name
Thread-4, data: 4, time: Sat May 11 10:48:27 2019 Current Thread Name Thread-2,
data: 5, time: Sat May 11 10:48:27 2019 Current Thread Name Thread-1, data: 6,
time: Sat May 11 10:48:27 2019 Current Thread Name Thread-4, data: 7, time: Sat
May 11 10:48:27 2019 Current Thread Name Thread-3, data: 8, time: Sat May 11
10:48:27 2019 Current Thread Name Thread-2, data: 9, time: Sat May 11 10:48:28
2019 Ending Thread-1 Ending Thread-4 Ending Thread-3 Ending Thread-2
耗时0.9028356075286865s
因为考虑到爬虫方面的实际应用,所以在程序中使用了 Python 的 Queue 模块,使用队列来实现线程间的同步(关于线程同步在后面会讲到)。在
get_num() 方法中注释的内容是另一种判断队列为空,结束子线程任务的代码实现。

在实际运行中,通过调节线程数可以看到,执行时间会随着线程数的增加而缩短。

技术
下载桌面版
GitHub
百度网盘(提取码:draw)
Gitee
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:766591547
关注微信