Logo

郎哥编程

多线程的使用场景及方法

2019-04-27 516

前面一节了解了多线程的概念。本节主要介绍如何使用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个并发客户检索通讯录,在模拟检索任务开始之前记录系统当前时间,模拟检索任务执行结束后,再获取任务执行完成后的时间,然后计算两个时间的差值,该差值就是检索任务运行的时间。程序输出结果如下图所示:

image.png      

                                 

图 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()

下图是执行代码后的输出结果:

image.png


图 2 用多线程完成同学通讯录检索任务输出结果

从输出结果看,整个检索耗时552ms,用多线程技术实现通讯录的并发检索,并没有提高检索效率,反而不如不使用多线程的运行速度快。主要原因是系统每启动一个线程,都要耗费一定的系统资源,导致运行效率降低,多线程在这个例子程序中,并没有体现出多线程的性能优势。

我们换个场景,假如把通讯录的检索放到服务器端,1000个用户在同一时间并发检索通讯录。如果服务端不使用多线程服务,虽然1000个用户是并发访问,但要在服务器端随机排队等候服务器响应,如果1个用户的响应时间为1秒,那么依次类推,最后1个用户的响应时间为1000秒。如果是多线程服务,平均每个用户的响应时间为2到3秒左右,显然能够满足大多数用户的响应需求。在这个场景下,多线程就体现出了性能优势。

在什么场景下使用多线程呢?一是对用户响应要求比较高,又允许用户并发访问的场景;二是程序存在耗费时间的计算,整个系统都会等待这个操作,为了提高程序的响应,将耗费时间的计算通过线程来完成。


代码在线纠错(通义千问 qwen-max)

支持粘贴多个代码文件,提交后由阿里云通义千问自动分析代码漏洞、语法错误、逻辑问题并给出修改建议。
您已解锁 AI 代码纠错功能,可正常使用!

评论区

登录 后发表评论
暂无评论