一、多线程的介绍:
1.进程
通常一个进程包含一个或者多个线程,每个进程有自己独立的一块内存空间,所有的线程共享这一块空间,例如:在Windows操作系统中,一个运行的xx.exe就是一个进程。
2.线程
一个进程总得有多个执行任务吧,例如我有一个学生信息查询进程,当我想要查询某个学生信息时,它大致包含以下几个任务:
1.进程接收学生信息并发送给服务器
2.进程接收学生信息
3.进程分析学生信息
4.进程将学生信息展示出来
不难理解,如果一个进程只有一个线程,那么这个线程将会按照顺序执行四个任务,那么总耗费时间就是t1+t2+t3+t4的时间。
但是如果我们使用多线程,我们拥有三个线程,那么我们就可以边接收学生信息(任务二),边分析学生信息(任务三),边展示学生信息(任务四)
那么总时间将无限接近于t1+t2。
可以看到,这大大提升了工作效率,除此之外,线程还拥有更小的系统开销代价,相较于多进程来说,多线程的开销远远小于多进程,并且由于线程间数据可以共享,为此在处理“密集型I/O型任务”时,我们可以使用多线程来提升工作效率。
密集型I/O型任务举例:爬虫中大量获取接口、GUI页面的子页面。
二、多线程的使用
0.多线程的使用方式:
在Python中实现多线程的库有很多,在这里我们使用最常见的“threading”库。
1.显示线程信息的属性的方法:
import threading
if __name__ == "__main__":
print("当前进程数量为:",threading.active_count())
print("当前所有的线程为:",threading.enumerate()) #以列表的形式显示
print("当前线程为:",threading.current_thread())
效果图:
2.添加线程
在threading中添加线程,使用内置的“Thread”类,先创建Thread对象,再使用start()方法启动这个线程。
threading.Thread(target=None, name=None, args=(), kwargs={}, *, daemon=None)
其中:
1.target为函数名,或者可调用对象。
2.name是线程名称,如果不指定系统会随机生成。
3.args是用于发起调用目标函数的参数列表或元组,默认是()
4.kwargs是用于发起调用目标函数的关键字参数字典,默认是{}
5.daemon(3.3版本新增),daemon如果不是默认值None,则该线程为守护线程。
import threading
import time
def job1():
#让这个线程暂停5秒,便于区分
time.sleep(1)
print("job1成功执行啦,执行该函数的线程是:",threading.current_thread())
if __name__ == "__main__":
#创建一个线程,并且不给该线程使用name参数指定名字
my_thread_1 = threading.Thread(target=job1)
#创建一个线程,并且给该线程使用name参数指定名字
my_thread_2 = threading.Thread(target=job1,name="t1")
#启动两个线程
my_thread_2.start()
my_thread_1.start()
print("当前进程数量为:",threading.active_count())
print("当前所有的线程为:",threading.enumerate()) #以列表的形式显示
print("当前线程为:",threading.current_thread())
效果图:
可以看到,线程1和线程2是同时完成的,因为两条打印语句都“挤到一起”了。
3.线程间的堵塞
3.1为什么要使用堵塞
我们继续看上面提到的学生查询例子,在任务四中,进程需要展示学生信息,但如果任务四的执行速度大于任务三大于任务二,会出现什么状况呢?
此时,学生的全部信息尚未接收完全,但是任务四的线程已经执行完毕了,这就导致最后展示出来的学生信息是不全的,这显然不是我们想要的结果,那我们该怎么办呢?
这就不得不搬出线程的“堵塞”了。
3.2堵塞的作用
线程堵塞顾名思义,会将某个线程堵住,从而使这个线程卡住,直到解除线程堵塞。
为此我们可以适当的将任务四的线程堵塞,直到任务二任务三的线程执行的差不多了,我们再解除任务四的堵塞即可。
3.3使用join()函数来完成线程堵塞的操作
join()函数的使用方法:
在A函数中使用join()函数,那么A函数会卡住,直到被使用join()函数的线程执行完毕,函数A才会继续进行。
import threading
import time
def B():
#让这个线程暂停1秒,便于区分
time.sleep(1)
print(f"嗨,我是B函数~我被线程{threading.current_thread()}使用.")
def A():
#创建B线程,注意的是A本身就是一个线程A
new_threading = threading.Thread(target=B)
#启动B线程
new_threading.start()
#堵塞A线程,使用线程B堵塞A线程。
new_threading.join()
print(f"嗨,我是A函数~我被线程{threading.current_thread()}使用.")
print("当前进程数量为:",threading.active_count())
print("所有的线程为:",threading.enumerate()) #以列表的形式显示
if __name__ == "__main__":
A()
效果图:
可以看到,线程B在执行完B函数之后,自动销毁了,此时线程数量仅为1了。
3.4线程执行的结果使用Queue来存储
虽然线程之间共享数据,但是在Threading库中,除主线程外,子线程执行的结果并不能通过return来返回数据,为此我们需要使用“Queue”来存储。
可以理解为,子线程虽然无法返回数据,但是子线程之间可以用Queue队列管道来存储结果。
为此,我们需要安装queue库。
import threading
from queue import Queue
def job(l, q):
for i in range(len(l)):
l[i] = l[i] * 10
#使用put方法向队列中添加数据
q.put(l)
def tasks():
# 创建队列
q = Queue()
# 线程列表
threads = []
# 二维列表
data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for i in range(3):
t = threading.Thread(target=job, args=(data[i], q))
t.start()
threads.append(t)
# 对所有线程进行阻塞
for thread in threads:
thread.join()
results = []
# 将新队列中的每个元素挨个放到结果列表中
for _ in range(3):
#使用get方法在队列中取数据.
#当对列为空时,线程会卡在这里,一直在请求获取数据,直到获取到数据为止.
#可以使用q.empty()方法判断队列是否为空.
results.append(q.get())
print(results)
if __name__ == "__main__":
tasks()
效果图: