Python3 多线程 threading学习笔记

先了解下进程和线程是个什么东西

这两个概念与操作系统的知识关系密切的,暂时只能先总结下基础概念。待以后看有没有必要系统学习。对这一块知识,我有种盲人摸象的感觉。所以总结起来难免不全面,或者出现错误。若发现了,请帮忙指正。
进程:
了解进程之前,了解下进程是怎么来的或许有帮助。
我们放在磁盘上的各种软件,可以称作【程序】;这个时候我们的软件和操作系统的几乎没什么关系。当一个程序被选中执行后,就可以称作【作业】;当作业被载入内存时,就变成了进程。进程是一个驻留在内存中的作业,也可以理解为是一个驻留在内存中的程序。这就是我们磁盘上的软件变成操作系统中一个进程的大致过程。这个过程请参考如下图:

当作业被载入内存时,操作系统就会为其分配系统资源。所以有个大家都在总结的一句话是:进程是操作系统分配资源的最小单位。
进程一般分为三个状态:运行、阻塞、就绪。不过也还有另外两个状态:创建、退出。
一个操作系统有很多任务要处理,但计算机资源是有限的,所以操作系统中有各种队列来处理这种场景。即没有得到系统资源的任务就先乖乖的排队,这个场景和我们平时在饭堂各个窗口排队打饭差不多。
进程所在的队列由进程管理器负责处理。当进程在不同类型的队列当中,就可以看作进程处于不同的状态。
进程和线程从本质上来说,是个虚拟的东西。我们只好想象一下,进程所在的世界发生了啥。假如是这样的:一个程序被选中执行的时候,挺高兴,终于升职成为作业了。到了新衙门一看,前面排着一群作业,一颗准备大展宏图的心瞬间凉了,凉了之后就得面对现实,加入到了队列中。这个队列就叫做【作业队列】。一群在排队的作业(这么说对于学生身份的人好像不大友好哈),百无聊赖打着哈欠之际,内存中有人跑过来说,这边的坑空了一个,下一个谁啊赶紧上。接着排在作业队列最前面的那个作业,就被载入内存,成为一名光荣的进程。这种队列方式,可以看作是先入先出队列。这个作业进入了新的衙门任职进程,这个新的衙门也就是内存。
作业进入内存之后变成了进程后,才会知道现实无论在哪里都是现实。因为他一来又得排队。
作业进入内存之后,这个时候是进程了,首先会在【就绪队列】中排队,这个时候进程的状态可以看作就绪状态。当在就绪队列中排完对之后,就得到了CPU分配的时间片,去CPU那遨游了一圈,这个时候的进程是运行状态。当进程运行的时候,突然说有IO需求,我们也该知道接下来会发生什么了。继续排队,没毛病。这次是去【I/O队列】排队,因为需要IO需求的也不只一个进程。当进程在IO队列中时,可以看作是阻塞状态。当这个进程拿到I/O权限的时候,也不是立即返回到CPU那继续运行的,而是需要再去就绪队列那重新排队。
以上整个过程,请参考如下图:图片简洁明了,请允许我给自己加了这么多戏。
Python3 多线程 threading学习笔记_第1张图片

线程:
线程的概念,百度百科是这样说的:线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是怎么来的,借鉴百度百科:线程的引入: 60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。 因此在80年代,出现了能独立运行的基本单位——线程(Threads)。 大概意思就是,线程是计算机发展的产物。当到了某个时代,就诞生了线程这样一个东西的需求。
线程来源于进程,是进程的一个执行单位。所以其特性之一:同一个进程下的所有线程共享进程所拥有的全部资源。
按照线程的诞生历史,当时OS中拥有资源和独立运行的基本单位是进程。处理任务时,要么多个进程一起去处理,要么一个进程轮流处理各个任务。这种方式对对操作系统和对进程本身都不友好,前一种可以比作操作系统这个老板要多发工资,后一种可以比作一个进程要干多个人的活,但又不能分身,累的不行。线程可以比作进程的分身,当一个进程需要处理多个任务时,就分身一个线程去处理。而且多个线程还可以并发运行。那么总结出来就是,线程可以理解进程的一个分身,进程的一些特性,线程也可以有。具体哪些特性,待以后接触多了再慢慢了解。

我们的主题:threading

首先感谢下前辈们把多线程做了优秀的封装,让后进之我使用多线程的门槛降低了一大截。
Python3提供了threading模块来支持多线程的使用。
附上学习文档:
https://docs.python.org/3/library/threading.html?highlight=threading#
threading模块提供了一些方法,和一些对象。这些方法和对象可以使得我们科学的使用多线程。其中threading.Thread这个线程对象是用来创建线程的。

简单了解下threading提供的部分方法:

threading.active_count()  #返回值是个整型数,返回当前活动的线程数量。需要注意的是,如果当前有两个你自己创建子线程在运行,那这时候这个方法会返回3.包含一个主线程。
threading.current_thread() #返回值是个线程对象。返回当前Thread对象。可以理解为:虽然是多线程并发,但其实只是看起来同步执行。一个时刻其实只有一个线程在运行。这个方法就是返回该时刻运行的线程对象。
threading.get_ident() #返回值是个非零整数,返回当前线程对象的“线程标识符”。需要注意的是,这个线程标识符在每次线程被创建的时候被赋予一个新值。在线程退出并创建另一个线程时,线程标识符被回收。
threading.enumerate() #返回值是个对象列表。返回当前活动的所有线程对象的列表。
threading.main_thread() #返回值是个线程对象。返回主线程对象。
threading.settrace(func) 
#这个方法:文档中解释为:
#Set a trace function for all threads started from the threading module. 
#The func will be passed to sys.settrace() for each thread, before its run() method is called.
#大意是从threading启动的每一个线程,这个方法将设置一个追踪函数。追踪函数作为参数传入,并且这个传入的动作在线程对象的run方法被调用之前。run方法调用的时候就是执行threading.Thread.start()的时候。之所以说这么多,是因为我没搞懂这个方法具体怎么使用。
threading.setprofile(func) #和上个方法差不多,不过这次是为线程设置profile函数。同样没搞懂具体怎么用。
threading.stack_size() #返回值是个整型数。返回创建新线程时使用的线程堆栈大小。这个方法还有别的功能,但感觉自己没有理解透,无法归纳。所以建议看原文档。
threading.TIMEOUT_MAX #这是个属性,用来限制阻断函数(比如Lock.acquire(), RLock.acquire(), Condition.wait()等)的超时参数的最大值,Specifying a timeout greater than this value will raise an OverflowError.试了下,不才还是不知道具体怎么用。

threading.Thread线程对象:

了解下Thread对象参数

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={},  daemon=None)
#参数的含义:
#group:默认为None,目前也应该为None,是为以后拓展而保留的
#target:默认为None,这个参数应该被赋予一个函数名,该函数将用于Thread.run()方法调用。可以理解为线程运行时是从这个函数开始的。为None代表run()方法不调用任何函数。
#name:线程的名字,自定义。默认为None。
#args:应该被赋予一个元组。元组中是被调用函数的参数,以位置参数的形式表达。
#kwargs:应该被赋予一个字典。字典中是被调用函数的参数,以关键字参数的方式表达。
#daemon:If not None, daemon explicitly sets whether the thread is daemonic. If None (the default), the daemonic property is inherited from the current thread.大意是:默认是None,创建的线程是否是守护线程会继承自当前线程(莫非是主线程?),反正一般是None的话,该线程就不是守护线程。不是None的话,就显示地设置该线程为守护线程。守护线程地含义后面会作特别说明。

接下来了解下threading.Thread会提供哪些属性和方法:

Thread.start() #该方法负责启动线程,并且在启动的线程中调用Thread.run()方法。每个线程只能调用一次start()方法,若多次调用,将raise一个RuntimeError错误。
Thread.run() #该方法代表线程所作的活动。标准的run方法的作用是调用target参数传递进来的函数。如果一个类继承了threading.Thread类,可以在子类里重载这个run方法。
Thread.join(timeout=None) #该方法会阻塞调用线程(请注意,是调用线程,即调用使用join方法的子线程的线程,大多数情况下,该调用线程为主线程)。会阻塞到什么时候呢,阻塞到使用join方法的子线程终结,或者时间达到timuout计数后超时。当join函数自定义设置了timeout参数后,当阻塞结束后如何判断是因为子线程终结还是超时导致的呢。可以通过在join()方法之后调用threading.Thread.is_alive()方法来判断,如果该方法返回True,则证明子线程还是活动的,那么阻塞是因为超时结束的。如果该方法返回False,则证明子线程是非活动的,那么阻塞是因为子线程终结而结束的。timoout默认是None,应该是一个浮点数,以秒为单位(或其分数)
Thread.name #这是一个属性,用来标识线程的名字。
Thread.getName()
Thread.setName() #Thread.name属性可以取代这两个方法的作用。这两个方法也很简单,直接操作Thread.name属性。设置name,或者获取name.
Thread.ident #这是一个属性,该线程的线程标识符。当线程未被启动时,该值为None。当线程启动时,该值被赋予一个非零整数,当线程被终结时,该值被回收,即该值仍然等于线程启动后被赋予的非零整数值。
Thread.is_alive() #该方法在之前有提及,返回子线程的活动状态。当子线程是活动的,返回True,当子线程是非活动的,返回False。
Thread.daemon #这是一个属性,是一个布尔值。用来设置该线程是否是守护线程。默认是False,如果需要自定义设置的话,必须在threading.Thread.start()方法调用之前设置。

了解下如何创建子线程
threading.Thread用来创建子线程,可以使用两种方法来创建子线程:
第一种方法:实例化一个Thread线程对象,将可调用函数(func_name)和函数参数(arg)传递给构造函数。这种方法很直观,很容易理解子线程是怎么来的,又是怎么结束的。

from threading import Thread
import time

#定义子线程的可调用函数
def test_thread(name):	
	print(name)
	time.sleep(3)
	print('z')
	
#定义子线程的可调用函数
def test_thread_2(name):
	print(name)
	time.sleep(2)
	print('v')
	
#实例化一个子线程对象,target传递可调用函数,args传递可调用函数需要的参数。
t1 = Thread(target = test_thread, name = 't1',args = ('t1',))
#再实例化一个子线程对象
t2 = Thread(target = test_thread_2, name = 't2',args = ('t2',))

#启动子线程t1和t2
t1.start()
t2.start()

#t1子线程调用阻塞方法join.这里阻塞的是主线程。因为是主线程调用的t1子线程。
t1.join()
print('1')
#t2线程调用join方法。阻塞的是主线程。但当程序运行到这里时,t2子线程早已经终结。
t2.join()

print('Over')

输出结果:
t1
t2
v
z
1
Over
[Finished in 4.0s]
#从这个输出结果中可以看出,print('1')是在t1子线程被终结后才开始执行的,之前因为被阻塞,所以无法执行。



第二种方法:通过继承Thread对象创建子类。
这种方法有两点需要注意的地方:
1.最多重载Thread对象中的__init__()和run()方法。不应该在子类中重载其他方法。
2.如果子类重载了__init__构造函数,那么第一步必须是调用基类的构造函数Thread.init()
这两点的具体原理怎样先不管,反正就是要这样做。

from threading import Thread

#创建一个子类,继承自Thread对象
class subThread(Thread):
	#重载构造函数
	def __init__(self,args):
		#调用基类的构造函数,必要的一步
		Thread.__init__(self)
		self.args = args

	#重载Thread.run()方法
	def run(self):
		print(self.args)

t1 = subThread('thread_01')
t1.start()
print('over')

输出结果:
thread_01
over
[Finished in 0.9s]


上面这个简单的例子描述了如何通过继承Thread对象,重载run方法来创建子线程。这种写法主要用于理解,但在实际应用中存在局限性。即子线程的活动内容被写死在子类的run()方法中,那么一个子类就被限制了只能做一件事。如果需要一个子线程做别的事,难道我们又要创建一个子类么?所以个人觉得下面的这种写法更好。

from threading import Thread

#创建一个类,继承自Thread对象
class subThread(Thread):
	#重载构造函数
	def __init__(self,func,args):
		#调用基类的构造函数,必要的一步
		Thread.__init__(self)
		self.func = func
		self.args = args

	#重载Thread.run()方法
	def run(self):
		#带一个*号的参数,代表可以收集位置参数
		#若是带两个*号的参数,代表可以收集关键字参数
		self.func(*self.args)

#定义子线程的可调用函数,该函数说明子线程的活动内容
def print_params(name,age,sex,city):
	print(name)
	print(age)
	print(sex)
	print(city)


#实例化一个subThread类,可调用函数及其参数通过形参传递给构造函数
#可调用函数和子线程对象实现了分离,降低了耦合。可调用函数可进行自定义。
t1 = subThread(print_params,('阿刁',26,'male','广州'))

t1.start()
t1.join()
print('over')

输出结果:
阿刁
26
male
广州
over
[Finished in 0.9s]

以上,就是创建子线程的两种方法。

了解下守护线程

官方文档的释义较简单,大概是这两句话:

  • 当仅仅守护线程存在时,则Python程序会退出。
  • 当没有活动的非守护线程,则Python程序会退出。
    还有一个需要注意的提示:
    如果一个非守护线程被突然中断会导致它们的资源无法释放,那么建议将这个线程设置为非守护的并且使用合适的信号机制。

给人的感觉守护线程的地位有点低啊。是吧。
守护线程是否结束了,好像没人理。而非守护线程如果没结束,那么Python程序就不会结束,如果所有的非守护线程都结束了,即使守护线程没有结束,那Pyhton程序也会结束;Python程序都结束了,那么守护线程自然也会被中断并结束。
守护线程的命运和古代给帝王陪葬的妃子有点类似呀。

看这个例子,说明所有非守护线程结束后,主程序退出,守护线程也会中断并退出

from threading import Thread
import time

def test_thread(name):	
	time.sleep(3)
	print(name)

def test_thread_2(name):
	print(name)

t1 = Thread(target = test_thread, name = 't1',args = ('t1',))
t2 = Thread(target = test_thread_2, name = 't2',args = ('t2',))

t1.daemon = True
t1.start()
t2.start()
print('Over')

输出结果:
t2
Over
[Finished in 1.0s]

接下来看这个例子,说明当守护线程结束后,非守护线程还在运行,Python程序也不会结束

from threading import Thread
import time

def test_thread(name):	
	print(name)

def test_thread_2(name):
	time.sleep(3)
	print(name)

t1 = Thread(target = test_thread, name = 't1',args = ('t1',))
t2 = Thread(target = test_thread_2, name = 't2',args = ('t2',))

t1.daemon = True
t1.start()
t2.start()
print('Over')

输出结果:
t1
Over
t2
[Finished in 3.9s]

在实际应用中,其中一种情况是:守护线程可以用来处理接受客户请求的任务,如果没有客户请求,他就一直等着,啥也不用干。当有客户请求时,就处理下。如果当主程序都结束了,那么他也可以下班了,因为这个时候处理客户请求也没意义。而主程序结束,也不会注意这个守护线程怎么样了,因为无论他怎么样,都不会影响任何东西。所以,通过这样一个场景,可以理解守护线程的意义。

最后

threading模块中还提供其他的对象,比如锁、事件、信号量、条件、定时器等等对象。
这些对象的意义在于管理多线程运行。
就像当一个只有3个人的创业公司,做事很自由和随意。而一个十万人的公司,工作规范就需要各种制度和流程来规范怎样做事。
当多个线程并发运行时,合理的使用这些对象就可以是的我们的程序有序高效的运行。

你可能感兴趣的:(Python)