前言

本节讲python的多线程,多线程可以实现高并发,但是在python中多线程不是真正的多线程,不同线程之间不能够并行处理,同一个时间片段内只有一个线程在运行,这是由于python自身的GIL(全局解释器锁)导致的,由于历史原因,难以更改,关于GIL等知识点在其它章节我们介绍,本节只介绍python多线程的使用。

threading库

python3中实现多线程的库为threading库,threading库使用非常简单,使用多线程,我们可以同时执行多个相同或者不同的任务,提高程序运行效率。

python主线程

首先需要知道什么是python的主线程,可以把主线程看作是一个进程,因为线程是程序执行的最小单位,一个进程开启的时候默认开启一个线程,这个时候可以把这个线程看成是一个进程,这个线程也是其它新产生的线程的始祖线程,祖先线程。

当我们要运行一段程序或者的时候,系统会开启一个进程,并产生一个主线程,主线程在运行程序的过程中,可以开启新的子线程,把任务拆开交给子线程去运行,可以提高程序的运行效率以及前端用户体验。

对于python的线程,主要有以下几种形式,我们接下来一一介绍;

默认线程(非守护线程 setDaemon(False))

默认情况下,python产生的线程是非守护线程,具体什么是非守护线程,可以这样来理解,在这种默认情况下,主线程不会等待子线程完成任务,而是异步执行程序,把任务交给子线程之后,主线程继续往下执行,遇到新的任务后,主线程可以选择自己执行,也可以再次产生新的子线程去执行,当主线程执行完所有的程序后,这个时候主线程并不回退出,也就是不会销毁,直到所有的子线程完成了各自的任务后才自动销毁。

看一段代码,这种情况下是非守护线程,也即setDaemon(False),默认为False,如果设置为True则为守护线程。

import threading
import time


def thread():
    time.sleep(2)
    print('---子线程结束---')


def main():
    t1 = threading.Thread(target=thread)
    t1.start()
    print('---主线程---结束')


if __name__ == '__main__':
    main()

这段代码输出为:

---主线程---结束
---子线程结束---

Process finished with exit code 0

解释一下,我们运行main()函数,这时候产生了一个主线程,开始从上往下执行main()函数的代码,运行过程中创建了一个子线程t1,这个时候默认t1为非守护线程,即t1.setDaemon(False)参数为False,由于是默认情况,所以这段代码可以不用写出来,主线程创建了这个子线程后,交给它一个任务,让它去执行thred()函数,然后使用t1.start()函数开启这个线程,开启后,主线程继续往下执行代码,直到代码执行完毕,主线程结束了自己所有的任务,但是这个时候主线程并没有退出,也就是没有自动销毁,它在等待自己的子线程执行完毕,直到自己所有的子线程执行完毕后,主线程才会从程序中销毁,至此,所有的任务结束,程序实现了自己的功能。这里看到主线程先执行完毕,是因为子线程的任务中睡眠了两秒钟,耗时远远大于主线程,所以我们可以把耗时的任务交给子线程去做,这样也就做到了异步的效果。

守护线程 setDaemon(True)

前面讲的是默认情况下的非守护线程,当我们把t1.setDaemon(False)的参数改为True时,即t1.setDaemon(True),这个线程就变成了守护线程,设置为守护线程之后,主线程一旦结束,就会退出,也就是自动销毁,不会等待子线程,主线程一旦销毁,则所有的子线程也会跟着销毁,无论任务是否执行完成,可以理解为一个大将军带领一群小兵去打仗,所谓擒贼先擒王,一旦将军牺牲了,则其它的小兵也不会继续作战,直接退出战场,因为他们是来守护主线程(将军)的。

看下面一段代码,和上面代码几乎一样,只是设置了t1为守护线程,即t1.setDaemon(True)

import threading
import time


def thread():
    time.sleep(2)
    print('---子线程结束---')


def main():
    t1 = threading.Thread(target=thread)
    t1.setDaemon(True)
    t1.start()
    print('---主线程---结束')


if __name__ == '__main__':
    main()

输出结果为:

---主线程---结束

Process finished with exit code 0

可以看到,主线程结束后,没有等待子线程,直接自动销毁,主线程自我销毁之后,子线程还没来得及执行完自己的任务就被强制销毁,就像将军被抓了之后,退出了战场,小兵们也不得已退出战场。

同步线程(join)

同步跟异步正好是相反的,当主线程遇到同步线程的时候,即遇到join()的时候会等待(即主线程进入堵塞状态,无法继续前行)所有的同步线程执行完毕之后,主线程才继续往下走,这里说的同步是针对主线程而言,意思是遇到join的时候,主线程会被堵塞,不会继续往下执行其它程序,而是等待join的线程执行完毕后再往下执行,而多个join的子线程之间是并行的,即互不影响。

具体地,看以下代码:

import threading
import time


def thread1():
    for i in range(2):
        print('子线程1睡眠第{}秒'.format(i + 1))
        time.sleep(1)
    print('---子线程1结束---')


def thread2():
    for i in range(5):
        print('子线程2睡眠第{}秒'.format(i + 1))
        time.sleep(1)
    print('---子线程2结束---')


def main():
    t1 = threading.Thread(target=thread1)
    t1.start()

    t2 = threading.Thread(target=thread2)
    t2.start()

    t1.join()
    t2.join()

    print('---主线程---结束')


if __name__ == '__main__':
    main()

代码解释,在main()函数中,主线程遇到t1和t2,两个join之后,主程序知道自己需要等待这两个线程都结束后才能继续往下执行,也就是等到t1和t2的任务都结束了才能执行最后的一段代码print(’—主线程—结束’),而t1和t2之间是并发进行的,所以最终程序输出的结果为:

子线程1睡眠第1秒
子线程2睡眠第1秒
子线程1睡眠第2秒
子线程2睡眠第2秒
子线程2睡眠第3---子线程1结束---
子线程2睡眠第4秒
子线程2睡眠第5---子线程2结束---
---主线程---结束

Process finished with exit code 0

可以看到,主线程是最后才结束的。

非守护线程 + 同步线程

当我们设置了子线程为非守护线程并设置其为同步线程的时候,将会是怎样的情况呢?当我们设置某个子线程为非守护线程并设置其为同步线程的时候,也就是上面所说的同步线程join()是一样的,主线程会等待所有的同步线程执行完毕再往下执行,那么如果我们在join()中添加了timeout参数后,情况就有所不同了,这个时候,主线程会等待子线程timeout的时间,然后继续往下执行,主线程执行完后不会退出,而是等待子线程完成任务后再退出。

import threading
import time


def thread1():
    for i in range(2):
        print('子线程1睡眠第{}秒'.format(i + 1))
        time.sleep(1)
    print('---子线程1结束---')


def main():
    t1 = threading.Thread(target=thread1)
    t1.setDaemon(False)
    t1.start()
    t1.join()
    print('---主线程---结束')


if __name__ == '__main__':
    main()

输出结果为:

子线程1睡眠第1---主线程---结束
子线程1睡眠第2---子线程1结束---

Process finished with exit code 0

守护线程 + 同步线程join

如果是守护线程这种情况下,join不设置timeout的话,主线程会等待子线程完成任务再继续往下执行,如下代码:

import threading
import time


def thread1():
    for i in range(2):
        print('子线程1睡眠第{}秒'.format(i + 1))
        time.sleep(1)
    print('---子线程1结束---')


def main():
    t1 = threading.Thread(target=thread1)
    t1.setDaemon(True)
    t1.start()
    t1.join()
    print('---主线程---结束')


if __name__ == '__main__':
    main()

输出为:

子线程1睡眠第1秒
子线程1睡眠第2---子线程1结束---
---主线程---结束

Process finished with exit code 0

如果join设置了timeout参数的话,等待子线程的时间后,主线程继续往下进行,主线程结束了自己的任务后,发现子线程还是个守护线程,但是这时候子线程还没完成自己的任务,而主线程也不会等待它,而是直接自动销毁,可怜的子线程也随之销毁。

如下代码:

import threading
import time


def thread1():
    for i in range(2):
        print('子线程1睡眠第{}秒'.format(i + 1))
        time.sleep(1)
    print('---子线程1结束---')


def main():
    t1 = threading.Thread(target=thread1)
    t1.setDaemon(True)
    t1.start()
    t1.join(timeout=1)
    print('---主线程---结束')


if __name__ == '__main__':
    main()

输出为:

子线程1睡眠第1---主线程---结束

Process finished with exit code 0

结语

至此,python的多线程的基本情况也就这么几种,具体在使用的过程中需要对这几种情况比较熟悉,并且对概念了解的清晰,使用起来才会游刃有余。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐