前面一节了解了多线程的概念。本节主要介绍如何使用threading类来创建和启动线程。
在讲解之前,先考虑一个编程任务。假设有一个同学通讯录,通讯录长度为1000,用于记录同学的姓名、电话、地址信息,用户可以并发检索该通讯录,输入通讯录中的姓名,程序从通讯录中查找该姓名,如果存在则输出与该姓名相关的电话、地址信息。任务要求简单模拟1000个用户的并发访问,检索功能分别采用不使用线程和多线程实现,比较在1000个用户的并发访问下,不使用多线程和使用多线程的检索效率。
1、线程的创建和启动
Python提供了两种创建线程的方式。
一种方式是使用theading模块的Thread类创建一个thread对象,然后运行它的start方法。
import threading
def method1(message):
print(message)
def method2(message):
print(message)
method1(message)
def main():
t1 = threading.Thread(target=method2, args=("hello",))
t1.start()
if __name__ == '__main__':
main()另外一种方式是声明一个新的类,该类继承于theading模块的Thread类,然后调用类的start()方法。
import threading
class MyThread(threading.Thread):
def __init__(self, id, interval):
threading.Thread.__init__(self)
self.id = id
self.interval = interval
def run(self):
for x in filter(lambda x: x % self.interval == 0, range(10)):
print("Thread id : %d time is %d \n" % (self.id, x))
def main():
t1 = MyThread(1, 2)
t2 = MyThread(2, 4)
t1.start()
t2.start()
if __name__ == "__main__":
main()上面的代码声明了MyThread类,MyThread的父类是theading模块的Thread类,从Thread类继承的子类必须重写run函数。在main方法中,实例化MyThread对象,并执行Thread类start()方法。
注意:在两种创建线程的方式中:第一种方式比较灵活,但代码的维护性和扩展性不强;第二种方式将线程代码封装到一个类中,虽然增加了编码工作量,但提高了代码的维护性和扩展性。
2、不使用多线程完成同学通讯录检索任务
(1)首先建立一个同学通讯录类,该类代码存储到phonebook.py文件。
# 声明同学通讯录类 class PhoneBook: # 姓名 name = "" # 电话 phone = "" # 地址 address = "" # 通讯录构造方法 def __init__(self, inname, inphone, inaddress): self.name = inname self.phone = inphone self.address = inaddress def getName(self): return self.name def getPhone(self): return self.phone def getAdress(self): return self.address def setName(self, inname): self.name = inname def setPhone(self, inphone): self.phone = inphone def setAddress(self, inaddress): self.address = inaddress
(2)建立search.py模块文件,初始化通讯录数据,用list存储1000个PhoneBook对象,用for循环模拟1000个并发客户检索通讯录,并输出通讯录信息,记录检索全部完成时间,代码如下:
import phonebook
import random
import time
import datetime
#声明存储通讯录的列表
phonebook_list = []
#初始化通讯录
def initPhoneBook():
r1 = range(0, 1000);
for i in r1:
phone = phonebook.PhoneBook("同学"+ str(i),"电话" + str(i),"地址" + str(i))
phonebook_list.append(phone)
def searchPhoneBook(name):
for i in range(len(phonebook_list)):
phone = phonebook_list[i]
if phone.getName() == name:
print("姓名:%s 电话:%s 地址:%s" % (phone.getName(), phone.getPhone(), phone.getAdress()))
def main():
initPhoneBook();
#输出起始时间毫秒
t1 = datetime.datetime.now().microsecond
t3 = time.mktime(datetime.datetime.now().timetuple())
#模拟并发检索通讯录
r1 = range(0, 1000)
for i in r1:
index = random.randint(0,1000)
searchPhoneBook("同学"+ str(index))
#输出结束时间毫秒
t2 = datetime.datetime.now().microsecond
t4 = time.mktime(datetime.datetime.now().timetuple())
strTime = '检索花费时间:%dms' % ((t4 - t3) * 1000 + (t2 - t1) / 1000)
print(strTime)
if __name__ == "__main__":
main()程序用for循环模拟1000个并发客户检索通讯录,在模拟检索任务开始之前记录系统当前时间,模拟检索任务执行结束后,再获取任务执行完成后的时间,然后计算两个时间的差值,该差值就是检索任务运行的时间。程序输出结果如下图所示:
图 1 不使用完成同学通讯录检索任输出结果
从上图可以看出,1000个并发检索耗时193ms。
3、用多线程完成同学通讯录检索任务
(1)建立PhoneBookSearch线程类,这样就可以实现当多用户检索通讯录时,程序针对每个用户的检索请求,都会启动一个线程去执行检索任务,由顺序执行改为并发执行。线程类的模块文件为phonebooksearch.py,线程类代码如下:
import threading
class PhoneBookSearch(threading.Thread):
def __init__(self, phonebooklist,name):
threading.Thread.__init__(self)
self.phonebook_list = phonebooklist
self.name = name
def run(self):
for i in range(len(self.phonebook_list)):
phone = self.phonebook_list[i]
if phone.getName() == self.name:
print("姓名:%s 电话:%s 地址:%s" % (phone.getName(), phone.getPhone(), phone.getAdress()))代码声明了PhoneBookSearch类,该类继承于threading模块的Thread类,并重写Thread类的run()方法,在run方法中,完成通讯录的检索及输出功能。
(2)改造search.py模块文件的代码
在search.py模块文件的main方法中,不再使用searchPhoneBook方法,而是实例化PhoneBookSearch对象,并将通讯录和检索名称作为参数传入进去,然后调用PhoneBookSearch对象的start方法启动线程,代码如下:
import time
import datetime
#声明存储通讯录的列表
phonebook_list = []
#初始化通讯录
def initPhoneBook():
r1 = range(0, 1000);
for i in r1:
phone = phonebook.PhoneBook("同学"+ str(i),"电话" + str(i),"地址" + str(i))
phonebook_list.append(phone)
def main():
initPhoneBook();
#输出起始时间毫秒
t1 = datetime.datetime.now().microsecond
t3 = time.mktime(datetime.datetime.now().timetuple())
#模拟并发检索通讯录
r1 = range(0, 1000)
for i in r1:
index = random.randint(0, 1000)
phonebook = phonebooksearch.PhoneBookSearch(phonebook_list,"同学" + str(index))
phonebook.start()
#输出结束时间毫秒
t2 = datetime.datetime.now().microsecond
t4 = time.mktime(datetime.datetime.now().timetuple())
strTime = '检索花费时间:%dms' % ((t4 - t3) * 1000 + (t2 - t1) / 1000)
print(strTime)
if __name__ == "__main__":
main()下图是执行代码后的输出结果:

图 2 用多线程完成同学通讯录检索任务输出结果
从输出结果看,整个检索耗时552ms,用多线程技术实现通讯录的并发检索,并没有提高检索效率,反而不如不使用多线程的运行速度快。主要原因是系统每启动一个线程,都要耗费一定的系统资源,导致运行效率降低,多线程在这个例子程序中,并没有体现出多线程的性能优势。
我们换个场景,假如把通讯录的检索放到服务器端,1000个用户在同一时间并发检索通讯录。如果服务端不使用多线程服务,虽然1000个用户是并发访问,但要在服务器端随机排队等候服务器响应,如果1个用户的响应时间为1秒,那么依次类推,最后1个用户的响应时间为1000秒。如果是多线程服务,平均每个用户的响应时间为2到3秒左右,显然能够满足大多数用户的响应需求。在这个场景下,多线程就体现出了性能优势。
在什么场景下使用多线程呢?一是对用户响应要求比较高,又允许用户并发访问的场景;二是程序存在耗费时间的计算,整个系统都会等待这个操作,为了提高程序的响应,将耗费时间的计算通过线程来完成。