发布时间:2023-05-24 16:00
目录:
一、函数方式
二、类的方式
Python中使用线程有两种方式:函数或类。
Python3 通过两个标准库_thread 和 threading提供对线程的支持。前者是将python2中的thread模块重命名后的结果,后者为高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块,比如,在这里!w(゚Д゚)w
一、函数方式
我们借助\"吃饭睡觉打豆豆\"的小故事来一个经典案例,什么?没听过?自!己!度!娘!
首先来一个正常的逻辑,不使用多线程操作:
import time
import threading
def eating():
print(\'吃饭时间到!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) #休眠两秒钟用来吃饭
print(\'吃饱啦!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
def sleeping():
print(\'睡觉时间到!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) #休眠两秒钟用来睡觉
print(\'醒啦!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
def hitting():
print(\'打豆豆时间到!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) #休眠两秒钟用来打豆豆
print(\'打出翔了!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
if __name__==\'__main__\':
eating()
sleeping()
hitting()
#输出:
吃饭时间到!当前时间:2019-01-18 23:58:18
吃饱啦!当前时间:2019-01-18 23:58:20
睡觉时间到!当前时间:2019-01-18 23:58:20
醒啦!当前时间:2019-01-18 23:58:22
打豆豆时间到!当前时间:2019-01-18 23:58:22
打出翔了!当前时间:2019-01-18 23:58:24
我们可以看到耗时6s,每个行为耗时2s,,wo*,,怎么十二点了,emmmmm,稳住,稳住。。。。
继续,继续,然后我们使用多线程操作来改写,只有函数执行变了,代码只放执行部分:
if __name__==\'__main__\':
t1=threading.Thread(target=eating) #借助threading.Thread()函数创建的对象t1来构造子线程执行eating()函数
t2=threading.Thread(target=sleeping)
t3=threading.Thread(target=hitting)
for i in [t1,t2,t3]: #这里写的可能有点偷懒了,没关系,埋个伏笔做对比~
i.start() #for循环依次启动线程活动。
for i in [t1,t2,t3]:
i.join() #等待至线程中止,这个我们下面展开讲。
#输出:
吃饭时间到!当前时间:2019-01-19 00:03:59
睡觉时间到!当前时间:2019-01-19 00:03:59
打豆豆时间到!当前时间:2019-01-19 00:03:59
打出翔了!当前时间:2019-01-19 00:04:01
吃饱啦!当前时间:2019-01-19 00:04:01
醒啦!当前时间:2019-01-19 00:04:01
借助多线程操作,所有线程并发启动,整个程序运行过程耗时2s,当然,如果线程耗时不同的话,单个线程耗时最多的时间即为程序运行总时间。
join()方法
join()方法用以阻塞主线程,直到调用此方法的子线程完成之后主线程才继续往下运行。白话一点就是当前子线程如果不结束,主线程不允许结束。
这里的确有点绕,以至于有部分开发在这里也是死用,而并不清楚它的实际意义。
我们先来梳理个概念:任何进程默认会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,这也就是我们使用的多线程,也就是多个子线程。
刚好Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字可以在创建时指定,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
我们借此来探究下如果不使用join()方法阻塞主线程会怎么样,略改下代码,在每一个函数的开始与结束引入我们的current_thread()函数,即线程开始与结束的位置(代码变工整了有木有!才记起老师的毒打!):
import time
import threading
timeis = time.asctime( time.localtime(time.time()) )
def eating():
print(\'thread %s is running...\' % threading.current_thread().name)
print(\'吃饭时间到!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) #休眠两秒钟用来吃饭
print(\'吃饱啦!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
print(\'thread %s ended.\' % threading.current_thread().name)
def sleeping():
print(\'thread %s is running...\' % threading.current_thread().name)
print(\'睡觉时间到!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) #休眠两秒钟用来睡觉
print(\'醒啦!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
print(\'thread %s ended.\' % threading.current_thread().name)
def hitting():
print(\'thread %s is running...\' % threading.current_thread().name)
print(\'打豆豆时间到!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) #休眠两秒钟用来打豆豆
print(\'打出翔了!当前时间:{}\'.format(time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
print(\'thread %s ended.\' % threading.current_thread().name)
def main():
print(\'thread %s start.\' % threading.current_thread().name)
t1 = threading.Thread(target=eating)
t2 = threading.Thread(target=sleeping)
t3 = threading.Thread(target=hitting)
for i in [t1, t2, t3]:
i.start()
print(\'thread %s ended.\' % threading.current_thread().name)
if __name__==\'__main__\':
main()
#输出:
thread MainThread start.
thread Thread-1 is running...
吃饭时间到!当前时间:2019-01-19 00:43:10
thread Thread-2 is running...
thread Thread-3 is running...
thread MainThread ended.
打豆豆时间到!当前时间:2019-01-19 00:43:10
睡觉时间到!当前时间:2019-01-19 00:43:10
打出翔了!当前时间:2019-01-19 00:43:12
醒啦!当前时间:2019-01-19 00:43:12
吃饱啦!当前时间:2019-01-19 00:43:12
thread Thread-3 ended.
thread Thread-2 ended.
thread Thread-1 ended.
我们可以看到thread MainThread ended.的位置,在子线程sleep的时候,主线程居然就结束了!下面,我们加上join()方法,之改动了main():
def main():
print(\'thread %s start.\' % threading.current_thread().name)
t1 = threading.Thread(target=eating)
t2 = threading.Thread(target=sleeping)
t3 = threading.Thread(target=hitting)
for i in [t1, t2, t3]:
i.start()
for i in [t1,t2,t3]:
i.join()
print(\'thread %s ended.\' % threading.current_thread().name)
#输出:
thread MainThread start.
thread Thread-1 is running...
吃饭时间到!当前时间:2019-01-19 00:51:48
thread Thread-2 is running...
睡觉时间到!当前时间:2019-01-19 00:51:48
thread Thread-3 is running...
打豆豆时间到!当前时间:2019-01-19 00:51:48
打出翔了!当前时间:2019-01-19 00:51:50
吃饱啦!当前时间:2019-01-19 00:51:50
醒啦!当前时间:2019-01-19 00:51:50
thread Thread-3 ended.
thread Thread-1 ended.
thread Thread-2 ended.
thread MainThread ended.
此时thread MainThread ended.的位置出现在了最后,也就是说,在三个子线程全部执行完毕后,主线程才退出,这就是join()方法的意义。为什么要等子线程完成主线程才退出呢?我们买下个伏笔在接下来守护线程部分拓展。
线程守护
守护线程,即为了守护主线程而存在的子线程。
如果你设置一个线程为守护线程,就表示你在说这个线程并不重要,在进程退出的时候,不用等待这个线程退出。那就设置这些线程的daemon属性。即在线程开始(thread.start())之前,调用setDeamon()函数,设定线程的daemon标志。(thread.setDaemon(True))就表示这个线程“不重要”。
主线程的结束意味着进程结束,进程整体的资源都会被回收,而理论上进程必须保证非守护线程都运行完毕后才能结束,这也就是我们为什么要使用join()方法的原因。
我们对上面讨论join()方法时用到的例子进行改写(只变更main()函数):
def main():
print(\'thread %s start.\' % threading.current_thread().name)
t1 = threading.Thread(target=eating)
t2 = threading.Thread(target=sleeping)
t3 = threading.Thread(target=hitting)
t1.setDaemon(True) #设置这daemon属性
t2.setDaemon(True) #此操作必须再start()之前
t3.setDaemon(True)
for i in [t1, t2, t3]:
i.start()
# for i in [t1,t2,t3]:
# i.join()
print(\'thread %s ended.\' % threading.current_thread().name)
#输出:
thread MainThread start.
thread Thread-1 is running...
吃饭时间到!当前时间:2019-01-19 02:28:37
thread Thread-2 is running...
睡觉时间到!当前时间:2019-01-19 02:28:37
thread Thread-3 is running...
thread MainThread ended.
我们可以看到,守护线程完全是与join()相对的,守护线程会随着主线程的退出而退出,而join()方法却是阻塞主线程直到子线程执行完毕、主线程最终才退出。
这两种逻辑的选择根据我们的需求自行调整。
二、类的方式
类中多线程的使用是从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法。
老规矩,改写上方打豆豆的例子(工整再升级有没有,有没有意义不重要,代码好看是一种自我修养):
import time
import threading
class DemoThread(threading.Thread):
def __init__(self,action):
threading.Thread.__init__(self)
self.action=action
def run(self):
print(\'{}时间到!当前时间:{}\'.format(self.action,time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) # 休眠两秒钟用来吃饭
print(\'{}完毕!当前时间:{}\'.format(self.action,time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
def main():
# 创建新线程
t1=DemoThread(\'吃饭\')
t2 = DemoThread(\'睡觉\')
t3 = DemoThread(\'打豆豆\')
#创建线程列表
threads=[]
#添加线程组
threads.append(t1)
threads.append(t2)
threads.append(t3)
#启动多线程
for i in threads:
i.start()
for i in threads:
i.join()
if __name__==\'__main__\':
main()
#输出:
吃饭时间到!当前时间:2019-01-19 01:22:14
睡觉时间到!当前时间:2019-01-19 01:22:14
打豆豆时间到!当前时间:2019-01-19 01:22:14
打豆豆完毕!当前时间:2019-01-19 01:22:16
吃饭完毕!当前时间:2019-01-19 01:22:16
睡觉完毕!当前时间:2019-01-19 01:22:16
线程同步
同样,类这边我们同样也丢一个知识点,线程同步问题,即线程锁的问题。
问:为什么要使用线程锁?
答:多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表里所有元素都是0,线程\"set\"从后向前把所有元素改成1,而线程\"print\"负责从前往后读取列表并打印。
那么,可能线程\"set\"开始改的时候,线程\"print\"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如\"set\"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如\"print\"获得锁定了,那么就让线程\"set\"暂停,也就是同步阻塞;等到线程\"print\"访问完毕,释放锁以后,再让线程\"set\"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
threading涉及到两种锁:Lock和RLock,这两种琐的主要区别是:RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐,RLock从一定程度上可以避免死锁情况的出现。
此处,简单逻辑我们以Lock锁为例改写我们上面的例子:
import time
import threading
mylock=threading.Lock() #创建一个Lock锁
class DemoThread(threading.Thread):
def __init__(self,action):
threading.Thread.__init__(self)
self.action=action
def run(self):
mylock.acquire() #申请锁
print(\'{}时间到!当前时间:{}\'.format(self.action,time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
time.sleep(2) # 休眠两秒钟用来吃饭
print(\'{}完毕!当前时间:{}\'.format(self.action,time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())))
mylock.release() #释放锁
def main():
# 创建新线程
t1 = DemoThread(\'吃饭\')
t2 = DemoThread(\'睡觉\')
t3 = DemoThread(\'打豆豆\')
#创建线程列表
threads=[]
#添加线程组
threads.append(t1)
threads.append(t2)
threads.append(t3)
#启动多线程
for i in threads:
i.start()
for i in threads:
i.join()
if __name__==\'__main__\':
main()
#输出
吃饭时间到!当前时间:2019-01-19 01:52:17
吃饭完毕!当前时间:2019-01-19 01:52:19
睡觉时间到!当前时间:2019-01-19 01:52:19
睡觉完毕!当前时间:2019-01-19 01:52:21
打豆豆时间到!当前时间:2019-01-19 01:52:21
打豆豆完毕!当前时间:2019-01-19 01:52:23
大家可以看到,所有子线程是一个接一个执行的,前一个子线程结束,后一个子线程开始,而非并发执行。线程同步问题正是为了解决多个线程共同对某个数据修改的情况,避免出现不可预料的结果,保证数据的正确性。
其实,多线程这里的内容远没有结束,但作为入门系列教程暂时只到这里,剩余内容会在后边的项目实践中逐步涉及,包括线程优先级队列、线程池管理、开源消息队列等。