Python入门系列(八)——多线程

目录:
一、函数方式
二、类的方式

Python中使用线程有两种方式:函数或类。
Python3 通过两个标准库_threadthreading提供对线程的支持。前者是将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

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

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

    原文作者:RabbitMask
    原文地址: https://www.jianshu.com/p/95e254f66ed5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞