在《网络工程师的Python之路 -- netdev(异步并行)》笔者先容了如何在Python中利用单线程异步来提高Python脚本的事情效率,通过给5台交流机做配置的实验,证明了异步(Asynchronous)是如何将同步(Synchronous)下一个须要45秒才能实行完成的脚本的运行韶光缩短至仅仅5秒的。本日我们来讲下其余一种提高脚本实行效率的技能:多线程(Multithreading)。
1. 单线程 VS 多线程在《网络工程师的Python之路 -- netdev(异步并行)》中我们引用了数学家华罗庚的《统筹方法》中所举的例子来解释了同步和异步在单线程中的差异。实在我们也可以引用同样的例子来解释单线程和多线程的差异。在华罗庚《统筹方法》讲到的沏茶的这个例子中,如果只有一个人来完成烧水、洗茶杯、倒茶叶三项任务的话,由于此时只有一个劳动力,我们就可以把它算作是单线程。假设我们能找来三个人分别卖力烧水、洗茶杯、倒茶业,并且担保三个人同时开工干活的话,那我们就可以把它算作是多线程,每一个劳动力代表一个线程。
在打算机的天下中也是一样的,一个程序可以启用多个线程同时完成多个任务,如果一个线程壅塞,其他线程并不受影响。现在的CPU都是多核的,单线程只能用到个中的一核,这实在是对硬件资源的一种摧残浪费蹂躏(当然不可否认的是随着时期的进步,现在的CPU已经足够强大,纵然只用单核也能同时搪塞多个任务,这也是后来Python开始支持异步的缘故原由之一)。如果我们利用多线程来运行Python脚本的话不仅能极大的提高脚本的运行速率,增加事情效率,并且还能充分利用我们主机的硬件资源。接下来我们就看下如何在Python中利用多线程。

2. 多线程在Python中的运用
在Python 3中已经内置了_thread和threading两个模块来实现多线程。相较于_thread,threading供应的方法更多而且更常用,因此接下来我们将举例讲解threading模块的用法,首先来看下面这段代码:
import threading import time def say_after(what, delay): print (what) time.sleep(delay) print (what) t = threading.Thread(target = say_after, args = ('hello',3)) t.start()
这里我们导入了threading这个Python内置模块来实现多线程。之后定义了一个say_after(what, delay)函数,该函数包含what和delay两个参数,分别用来表示打印的内容,以及time.sleep()休眠的韶光。随后我们利用threading的Thread()函数为say_after(what, delay)函数创建了一个线程并将它赋值给变量t,把稳Thread()里的target参数对应的是函数名称(即这里的say_after),args对应的是该say_after函数里的参数,这里等同于“what = ‘hello’”,“delay = 3”。末了我们调用threading中的start()来启动我们刚刚创建的线程。
运行代码看效果:
在打印出第一个hello后,程序由于time.sleep(3)休眠了三秒,三秒之后随即打印出了第二个hello。由于这时我们只运行了say_after(what, delay)这一个函数,并且只运行了一次,因此纵然我们现在启用了多线程,我们也感想熏染不了它和单线程有什么差异。接下来我们将该代码修正如下:
#coding=utf-8 import threading import time def say_after(what, delay): print (what) time.sleep(delay) print (what) t = threading.Thread(target = say_after, args = ('hello',3)) print (f"程序于 {time.strftime('%X')} 开始实行") t.start() print (f"程序于 {time.strftime('%X')} 实行结束")
这一次我们调用time.strftime()来考试测验记录程序实行前和实行后的韶光,看看有什么“意想不到”的结果。
运行代码看效果:
这里你肯定会问为什么程序在02:23:44开始实行,又在同一韶光结束?难道不是该休眠三秒吗?为什么明明“print (f"程序于 {time.strftime('%X')} 开始实行")”和“print (f"程序于 {time.strftime('%X')} 实行结束")”分别写在“t.start()”的上面和下面,但是不等第二个hello被打印出来,“print (f"程序于 {time.strftime('%X')} 实行结束")”就被实行了?
这是由于除了threading.Thread()为say_after()函数创建的用户线程外,“print (f"程序于 {time.strftime('%X')} 开始实行")”和“print (f"程序于 {time.strftime('%X')} 实行结束")”两个print()函数也共同占用了公用的内核线程。也便是说该脚本现在实际上是调用了两个线程:一个用户线程,一个内核线程,也就构成了一个多线程的环境。由于分属不同的线程,say_after()函数和函数之外的两个print语句是同时运行的,互不干涉,因此“print (f"程序于 {time.strftime('%X')} 实行结束")”是不会像在单线程中那样等到t.start()实行完了才被实行,而是在“print (f"程序于 {time.strftime('%X')} 开始实行")”被实行后就立时随着被实行。这也就阐明了为什么你会看到原来须要休眠三秒韶光的脚本会在“程序在02:23:44开始实行”和“程序在02:23:44实行结束”同时开始和结束。
如果想要精确捕捉say_after(what, delay)函数开始和结束时的韶光,我们须要额外利用threading模块的join()方法,来看下面的代码:
#coding=utf-8 import threading import time def say_after(what, delay): print (what) time.sleep(delay) print (what) t = threading.Thread(target = say_after, args = ('hello',3)) print (f"程序于 {time.strftime('%X')} 开始实行") t.start() t.join() print (f"程序于 {time.strftime('%X')} 实行结束")
这里我们只修正了代码的一处地方,即在t.start()下面添加了一个t.join(),join()方法的浸染是逼迫壅塞调用它的线程,直到该线程运行完毕或者终止为止(类似于单线程同步)。由于这里调用join()方法的变量t正是我们用threading.Thread()为say_after(what, delay)创建的用户线程,因此利用内核线程的“print (f"程序于 {time.strftime('%X')} 实行结束")”必须等待该用户线程实行完毕过后才能连续实行,因此脚本在实行时的效果会让你以为整体还是以“单线程同步”的办法运行的。
运行代码看效果:
这里可以看到由于调用了.join()方法,在内核线程上运行的“print (f"程序于 {time.strftime('%X')} 实行结束")”必须等待在用户线程上运行的say_after(what, delay)实行完毕后,才能连续被实行,因此程序前后实行统共花费了3秒,类似于“单线程同步”的效果。
末了我们再举一例,来看看如何创建多个用户线程并运行,代码如下:
#coding=utf-8 import threading import time def say_after(what, delay): print (what) time.sleep(delay) print (what) print (f"程序于 {time.strftime('%X')} 开始实行\n") threads = [] for i in range(1,6): t = threading.Thread(target=say_after, name="线程" + str(i), args=('hello',3)) print(t.name + '开始实行。') t.start() threads.append(t) for i in threads: i.join() print (f"\n程序于 {time.strftime('%X')} 实行结束")
这里我们利用for循环合营range(1,6)创建了5个线程,并且将它们以多线程的形式实行,也便是把say_after(what, delay)函数以多线程的形式实行了5次。每个线程作为元素加入进了threads这个空列表,然后我们再次利用for语句来遍历现在已经有了5个线程的threads列表,对个中的每个线程都调用的join()方法,确保直到它们都实行结束后,我们才实行内核线程下的“print (f"程序于 {time.strftime('%X')} 实行结束")”。
运行代码看效果:
可以看到这里我们成功的利用了多线程将程序实行,如果以单线程来实行5次say_after(what,delay)函数的话,那么须要花费3x5=15秒才能跑完全个脚本,而在多线程的形式下,全体程序只花费了3秒就运行完毕。
3. 多线程在Netmiko中的运用在节制了threading模块的基本用法后,接下来我们看看如何将它和netmiko结合,实现通过netmiko对网络做多线程登录和操作。
在《网络工程师的Python之路 -- netdev(异步并行)》中,我们利用传统的“单线程同步”办法,通过netmiko对5台交流机(192.168.2.11--192.168.2.15)下的“line vty 5 15”配置了“login local”,全体脚本从开始实行到结束,前后统共耗时了45.02秒。这里我们用netmiko以多线程的办法,对这5台交流机做同样的配置并计时,来看看利用多线程能为我们节省多少韶光。
脚本代码如下:
#coding=utf-8 import threading from queue import Queue import time from netmiko import ConnectHandler f = open('ip_list.txt') threads = [] def ssh_session(ip, output_q): commands = ["line vty 5 15", "login local","exit"] SW = {'device_type': 'cisco_ios', 'ip': ip, 'username': 'python', 'password': '123'} ssh_session = ConnectHandler(SW) output = ssh_session.send_config_set(commands) print (output) print (f"程序于 {time.strftime('%X')} 开始实行\n") for ips in f.readlines(): t = threading.Thread(target=ssh_session, args=(ips.strip(), Queue())) t.start() threads.append(t) for i in threads: i.join() print (f"\n程序于 {time.strftime('%X')} 实行结束")
在利用netmiko实现多线程时,我们须要导入queue这个内置行列步队模块。在Python中,行列步队(queue)是线程间最常用的交流数据的形式,这里我们引用了queue模块中的Queue类,也便是前辈先出(FIFO)行列步队。并将它作为出行列步队参数配置给了ssh_session(ip, output_q)这个函数,有关queue模块的详细先容不在本书谈论范围内,这里只须要知道这是利用netmiko实现多线程时必备的步骤即可。别的代码讲解从略。
运行脚本看效果:
可以看到利用netmiko多线程统共仅耗时10秒钟(08:11:41 – 08:11:51)便完成了对5台交流机的配置。比默认利用单线程同步的传统办法快了35秒,不过这个速率依然慢于《网络工程师的Python之路 -- netdev(异步并行)》中提到的单线程异步办法,在该节实验中,我们利用netdev单线程统共仅耗时5秒钟便对同样的5台交流机完成了同样的配置。