python入门基础系列八_Python入门系列(八)——多线程

目录:

一、函数方式

二、类的方式

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

大家可以看到,所有子线程是一个接一个执行的,前一个子线程结束,后一个子线程开始,而非并发执行。线程同步问题正是为了解决多个线程共同对某个数据修改的情况,避免出现不可预料的结果,保证数据的正确性。

其实,多线程这里的内容远没有结束,但作为入门系列教程暂时只到这里,剩余内容会在后边的项目实践中逐步涉及,包括线程优先级队列、线程池管理、开源消息队列等。

你可能感兴趣的:(python入门基础系列八)