多线程的概念
参考博文:https://www.jianshu.com/p/e50b9e4ce5aa
什么是进程?什么是线程?
“操作系统进行资源分配的最小单元是进程,即进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序,数据集,进程控制块三部分组成.我们编写的程序 用来描述进程完成那些功能以及如何完成;数据集 则是程序在执行过程中多需要使用的资源;进程控制块 用来记录进程外部特征,描述进程的执行变化过程,系统可以用它来控制和管理进程,它是系统感知进程存在的唯一标志。”——简单来说,进程就是一次程序的执行,在执行中会用到数据以及其他的一些资源。是资源分配的最小单元。
“线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元”。“操作系统进行运算调度的最小单元是线程”。——简单来说,线程是CPU一次运算调度。是运算的最小单元。
综上,进程的概念要大于线程。对于一个应用程序或软件,它开始执行,则系统为其开启一个进程(也可能是多个,暂不讨论)。系统默认开启一个主线程,如果没有创建多线程,则该主线程沿时间线(对应程序代码从上到下)开始调度运算,直至主线程结束,程序才能退出。故此,主线程相当于一个程序的“后台主线”,主线程贯穿程序的整个周期。
多个线程
- 为什么要创建多个线程? 假设一段程序代码,拟实现“A+B+C”的功能,A为听音乐,B为写博客,C为打印,他们所消耗的时间分别为7、8、9秒,则程序总体消耗时间为7+8+9秒,为一种串行的结构(对应代码自上而下的运行)。像一条单车道,先跑A段,再跑B段C段。
如何提速呢?可以将A、B、C设置为3个线程,同时运行,这样最终程序的时间为9秒。像三车道,A、B、C同时跑。为一种并行的结构(实为“并发”,并行与并发暂不讨论)。 - 如何在Python中创建多个线程? 采用threading模块中的Thread类直接创建或继承式创建。子线程是在默认的主线程(“后台主线”)上创建出来的。
(1)Thread类直接创建
# 多线程的并发,只能是交给一个cpu执行,不能多个cpu执行.即多个线程不能实现并行.
# 多线程并发方式一:
import threading
import time
def tingge():
print("听歌")
time.sleep(3)
print("听歌结束时间:")
print(time.time() - s)# 计算整个此线程运行时间
def xieboke():
print("写博客")
time.sleep(5)
print("写博客结束时间:")
print(time.time()-s) # 计算整个此线程运行时间
s = time.time()
t1 = threading.Thread(target=tingge) # 创建听歌线程,多线程的子线程.
t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
t1.start() # 运行听歌线程,多线程的子线程
t2.start() #运行写博客线程,多线程的子线程
time.sleep(0)
print("主线程‘前端’结束时间:") # 该print在多线程的主线程上
print(time.time()-s)# 计算‘前端’运行时间
=========================
听歌
写博客
主线程‘前端’结束时间:
0.01013493537902832
听歌结束时间:
3.018101930618286
写博客结束时间:
5.015822887420654
该三线程,最长的线程为写博客,消耗5s,故总的程序耗时(即后台主线程耗时)为5s。可以反复调整各time.sleep()中线程的耗时时间,观测程序结果。
在该实例中,听歌、写博客、主线程‘前端’三个线程为并列关系,谁也不等谁,谁最后结束,程序才结束。
如果采用传统的单线程,则程序总耗时为8秒。代码为:
# 不采用多线程并发。即传统的串行结果仿真。
import threading
import time
def tingge():
print("听歌")
time.sleep(3)
print("听歌结束时间:")
print(time.time()-s)# 计算整个此线程运行时间
def xieboke():
print("写博客")
time.sleep(5)
print("写博客结束时间:")
print(time.time()-s) # 计算整个此线程运行时间
s = time.time()
# t1 = threading.Thread(target=tingge) # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
# t1.start() # 运行听歌线程,多线程的子线程
# t2.start() #运行写博客线程,多线程的子线程
tingge()
xieboke()
time.sleep(0)
print("主线程‘前端’结束时间:") # ‘前端’在多线程的主线程上
print(time.time()-s)# 计算‘前端’运行时间
# 总的程序耗时为听歌耗时+写博客耗时+主线程耗时。
(2) Thread类继承式创建:
# 调用多线程方式二
import threading
import time
class MyThread(threading.Thread):
def __init__(self,num):
threading.Thread.__init__(self)
self.num = num
def run(self):
print("running on number:%s" %self.num)
time.sleep(3)
t1 = MyThread(56)
t2 = MyThread(78)
t1.start() # 该进程运行run函数原因,请查看源码,一系列的调用最终是调用run函数
t2.start() # 该进程运行run函数原因,请查看源码,一系列的调用最终是调用run函数
print("ending")
- 创建多个线程可能会遇到哪些问题?(不影响对其他问题的理解时,不再区分主线程‘前端’或后台)
(1)当各线程之间相互独立时:各线程及主线程是一个简单的“并”。如以上实例所示。
(2)当各线程之间不独立时:
(2a)如果主线必须要等待某一个子线程结束后方能结束,则可以采用join()的方法。join()使得子线程加入到主线程中,即在子线程完成之前,主线程(或称父线程、父进程)将一直被阻塞。
如:
import threading
from time import ctime,sleep
import time
def Music(name):
print("Begin listenning to {name}.{time}".format(name=name,time=ctime()))
sleep(3)
print("end listening {time}".format(time=ctime()))
def Blog(title):
print("Begin recording the {title}.{time}".format(title=title,time=ctime()))
sleep(5)
print("end recording {time}".format(time=ctime()))
threads = []
t1 = threading.Thread(target=Music,args=("FILL ME",))
t2 = threading.Thread(target=Blog,args=("My Blog",))
threads.append(t1)
threads.append(t2)
if __name__ == "__main__":
for t in threads:
t.start()
t2.join()
print("Main thread ‘head’ over %s "%ctime())
=========================
Begin listenning to FILL ME.Sat Nov 2 17:49:41 2019
Begin recording the My Blog.Sat Nov 2 17:49:41 2019
end listening Sat Nov 2 17:49:44 2019
end recording Sat Nov 2 17:49:46 2019
Main thread ‘head’ over Sat Nov 2 17:49:46 2019
此时,写博客堵塞了主线程。相当于两车道,听歌一个车道,写博客和主线程在一个车道。
如果将t2.join()改为t1.join(),则结果为:
Begin listenning to FILL ME.Sat Nov 2 17:52:31 2019
Begin recording the My Blog.Sat Nov 2 17:52:31 2019
end listening Sat Nov 2 17:52:34 2019
Main thread ‘head’ over Sat Nov 2 17:52:34 2019
end recording Sat Nov 2 17:52:36 2019
此时,听歌堵塞了主线程,即听歌和主线程在一个车道,写博客在一个车道。
如果当t.join()在for循环内就不能实现多线程了,没有意义。
if __name__ == "__main__":
for t in threads:
t.start()
t.join()
=======================
Begin listenning to FILL ME.Sat Nov 2 17:54:58 2019
end listening Sat Nov 2 17:55:01 2019
Begin recording the My Blog.Sat Nov 2 17:55:01 2019
end recording Sat Nov 2 17:55:06 2019
Main thread ‘head’ over Sat Nov 2 17:55:06 2019
此时,听歌先堵塞主线程,然后写博客再堵塞主线程,即三者在一个车道,多线程无意义了。
(2b)在以上实例中,主线程‘前端’与其并行的子线程互不干扰,即便主线程‘前端’结束了,后台主线程还是要等到所有的子线程都结束后,程序才退出。有的时候,我们希望主线程‘前端’结束的时候,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon()方法了。
setDaemon()设置守护线程:将某一线程声明为守护线程(必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起(应该是所有线程结束后就退出了)),这样当主线程可以结束时,守护线程也就一起退出了。如:
# 主线程可以结束但子线程未结束,整个程序同样结束。
import threading
from time import ctime, sleep
def Music(name):
print("Begin listening to {name}. {time}".format(name=name, time=ctime()))
sleep(3)
print("end listening {time}".format(time=ctime()))
def Blog(title):
print("Begin recording the {title}. {time}".format(title=title, time=ctime()))
sleep(5)
print('end recording {time}'.format(time=ctime()))
threads = []
t1 = threading.Thread(target=Music, args=('FILL ME',))
t2 = threading.Thread(target=Blog, args=('python',))
threads.append(t1)
threads.append(t2)
if __name__ == '__main__':
for t in threads:
t.setDaemon(True) # 注意:一定在start之前设置
t.start()
sleep(2)
print("all over %s" % ctime())
==============================================
Begin listening to FILL ME. Sat Nov 2 19:08:44 2019
Begin recording the python. Sat Nov 2 19:08:44 2019
all over Sat Nov 2 19:08:46 2019
此时,t1,t2均为守护线程(即都是主线程的守护神),主线程在2s后要结束时,其所有守护神也一并退出了。
如果仅将t1设置为守护线程,结果为:
t1.setDaemon(True) # 注意:一定在start之前设置
t1.start()
t2.start()
sleep(2)
print("all over %s ?" % ctime())
========================================
Begin listening to FILL ME. Sat Nov 2 19:16:38 2019
Begin recording the python. Sat Nov 2 19:16:38 2019
all over Sat Nov 2 19:16:40 2019 ?
end listening Sat Nov 2 19:16:41 2019
end recording Sat Nov 2 19:16:43 2019
此时,仅t1为守护线程,当2s后主线程准备结束时,由于t2独立运行(耗时最长),后台主线程不能退出,等到t2完成时,所有其他线程也运行完毕。
如果仅将t2设置为守护线程,结果为:
Begin listening to FILL ME. Sat Nov 2 20:16:30 2019
Begin recording the python. Sat Nov 2 20:16:30 2019
all over Sat Nov 2 20:16:32 2019 ?
end listening Sat Nov 2 20:16:33 2019
此时,仅t2为守护线程,当2s后主线程准备结束时,由于t1独立运行(耗时3s),后台主线程不能退出,等到t1完成时,后台主线程退出。整个程序随之结束。
4. 其它方法
Thread实例对象的方法
t.start() : 激活线程,
t.getName() : 获取线程的名称
t.setName() : 设置线程的名称
t.name : 获取或设置线程的名称
t.is_alive() : 判断线程是否为激活状态
t.isAlive() :判断线程是否为激活状态
t.setDaemon() 设置为后台线程或前台线程(默认:False);通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之后才可以使用。如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
t.isDaemon() : 判断是否为守护线程
t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
t.join() :逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
t.run():线程被cpu调度后自动执行线程对象的run方法
threading模块提供的一些方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
5. 关于线程的退出
没有特别的设置时,线程运算完毕即自退出。下面的实例可以直观的说明:
# -*- coding: utf-8 -*-
"""
Created on Fri Oct 25 09:46:38 2019
@author: Maguoxin
"""
import threading
import time
exitFlag = 0# 没有用到
class myThread (threading.Thread): #继承父类threading.Thread
def __init__(self, threadID, name, counter, delay):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
self.delay = delay
def run(self): #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数。当run运行完毕时即自退出
print ("Starting " + self.name)
print_time(self.name, self.counter, self.delay)# 每个线程都要调用这个函数。可以同时调用。
print(threading.enumerate())
print ("Exiting " + self.name)
def print_time(threadName, counter, delay):
while counter:# 当线程调用此函数时,此函数内含有一个循环。当循环counter次后,循环即结束跳出。此函数亦即执行完毕。
# if exitFlag:
# (threading.Thread).exit() # 没有用到
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 创建新线程
thread1 = myThread(1, "Thread-1", 8,3)# 线程1调用print_time函数时,函数内循环体共循环8次,每次delay时间为3s
thread2 = myThread(2, "Thread-2", 10,2)# 线程2调用print_time函数时,函数内循环体共循环10次,每次delay时间为2s
# 开启线程
thread1.start()
thread2.start()
time.sleep(25)
print(threading.enumerate())
print ("Exiting Main Thread")
===================================
Starting Thread-1
Starting Thread-2
Thread-2: Tue Nov 5 21:00:31 2019
Thread-1: Tue Nov 5 21:00:32 2019
Thread-2: Tue Nov 5 21:00:33 2019
Thread-1: Tue Nov 5 21:00:35 2019
Thread-2: Tue Nov 5 21:00:35 2019
Thread-2: Tue Nov 5 21:00:37 2019
Thread-1: Tue Nov 5 21:00:38 2019
Thread-2: Tue Nov 5 21:00:39 2019
Thread-1: Tue Nov 5 21:00:41 2019
Thread-2: Tue Nov 5 21:00:41 2019
Thread-2: Tue Nov 5 21:00:43 2019
Thread-1: Tue Nov 5 21:00:44 2019
Thread-2: Tue Nov 5 21:00:45 2019
Thread-1: Tue Nov 5 21:00:47 2019
Thread-2: Tue Nov 5 21:00:47 2019
Thread-2: Tue Nov 5 21:00:49 2019
[<_MainThread(MainThread, started 17140)>, <myThread(Thread-1, started 5400)>, <myThread(Thread-2, started 16740)>]
Exiting Thread-2#此时线程2是真的退出了,由下可见
Thread-1: Tue Nov 5 21:00:50 2019
Thread-1: Tue Nov 5 21:00:53 2019
[<_MainThread(MainThread, started 17140)>, <myThread(Thread-1, started 5400)>]#由此可见
Exiting Thread-1#此时线程1也自退出了
[<_MainThread(MainThread, started 17140)>]
Exiting Main Thread
- 发现的那些坑儿
(1)当采用threading.Thread(target=tingge)直接创建线程时,发现了一个问题,当tingge后有小()时,就不能创建新线程,而是在主线程中开始调用tingge函数。当上述实例中tingge和xieboke后仅添加两个小()后,结果没有实现多线程,而是传统的主线程自己运行的结果:
t1 = threading.Thread(target=tingge()) # 不能创建多线程的子线程,而是主线程开始执行tingge的功能.
t2 = threading.Thread(target=xieboke()) # 亦不能创建多线程的子线程,而是主线程开始执行写博客的功能.
===================================
听歌
听歌结束时间:
3.0002450942993164
写博客
写博客结束时间:
8.000455856323242
主线程‘前端’结束时间:
8.002456903457642
即便是在threading.Thread(添加name=‘ 起名 ’),也不能改变这个情况。这里个人认为是threading.Thread创建新线程的一个大的bug。请反复实现下列代码进行试验,即可观测结果。
但是使用类继承的方式创建,则可以在tingge后添加小(),并没有bug出现。而我们最终添加这个小()的目的,是为了调用函数传递实参。
# 当threading.Thread直接创建多线程时,发现:如果target=后的function带()则不能实现多线程
# 多线程实验
import threading
import time
def tingge():
print("听歌")
time.sleep(3)
print("听歌结束时间:")
print(time.time()-s)# 计算整个此线程运行时间
print(threading.enumerate())
def xieboke():
print("写博客")
time.sleep(5)
print("写博客结束时间:")
print(time.time()-s) # 计算整个此线程运行时间
print(threading.enumerate())
s = time.time()
#''' 实验1
# t1 = threading.Thread(target=tingge) # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
# t1.start() # 运行听歌线程,多线程的子线程
# t2.start() #运行写博客线程,多线程的子线程
# time.sleep(0)
#''' 实验1
#''' 实验2
# t1 = threading.Thread(target=tingge) # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
# t1.start() # 运行听歌线程,多线程的子线程
# t2.start() #运行写博客线程,多线程的子线程
# time.sleep(8.1)
#''' 实验2
#''' 实验3
# t1 = threading.Thread(target=tingge()) # 当此处tingge有()时,发现并没有创建新线程。而是主线程在运行tingge函数功能
# t2 = threading.Thread(target=xieboke()) # 同上,主线程在运行xieboke函数功能
# t1.start() # 并没有新的线程启动
# t2.start() # 并没有新的线程启动
# time.sleep(8.1)
#''' 实验3
#''' 实验4
# t1 = threading.Thread(name='a',target=tingge()) # 添加线程名称,不能改观结果
# t2 = threading.Thread(name='b',target=xieboke()) #
# t1.start() # 并没有新的线程启动
# t2.start() # 并没有新的线程启动
# time.sleep(8.1)
# #''' 实验4
#''' 实验5
# t1 = threading.Thread(name='a',target=tingge) # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(name='b',target=xieboke()) # 主线程在运行xieboke函数功能
# t1.start() # tingg作为一个新线程
# t2.start() # 并没有新的线程启动
# time.sleep(8.1)
#''' 实验5
#''' 实验6
# t1 = threading.Thread(name='a',target=tingge()) # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(name='b',target=tingge()) # 主线程在运行xieboke函数功能
# t1.start() # 并没有新的线程启动
# t2.start() # 并没有新的线程启动
# time.sleep(3.1)
#''' 实验6
#''' 实验7
# class MyThread(threading.Thread):
# def __init__(self):
# threading.Thread.__init__(self)
#
# def run(self):
# tingge()
#
#
# t1 = MyThread()
# t2 = MyThread()
# t1.start() # 有新的线程启动
# t2.start() # 有新的线程启动
# time.sleep(3.1)
#''' 实验7
#''' 实验8
def tingge_new(delay):
print("听歌")
time.sleep(delay)
print("听歌结束时间:")
print(time.time()-s)# 计算整个此线程运行时间
print(threading.enumerate())
class MyThread(threading.Thread):
def __init__(self, name, delay):
threading.Thread.__init__(self)
self.name = name
self.delay = delay
def run(self):
tingge_new(self.delay)
t1 = MyThread("Thread-1", 3)
t2 = MyThread("Thread-2", 5)
t1.start() # 有新的线程启动
t2.start() # 有新的线程启动
time.sleep(3.1)
#''' 实验8
print("主线程‘前端’结束时间:") # ‘前端’在多线程的主线程上
print(time.time()-s)# 计算‘前端’运行时间
print(threading.enumerate())
#
注:早期的Python2.0 thread模块,后变更为_thread,中的_thread.start_new_thread(函数名,(实参1,))的方式可以启用新线程并向函数传递实参,但该模块其他问题较多,不建议使用。