等一等,你的多线程可别再乱 join 了。

简介: 等一等,你的多线程可别再乱 join 了。

摄影:产品经理水天一色

如果你在网上搜索“Python 多线程”,那么你会看到很多文章里面用到了一个关键词,叫做.join()。但是很多人的代码里面都在乱用 join(),例如:

import time
import threading
def sleep_5_seconds():
    time.sleep(5)
    print('睡眠5秒结束')
def sleep_3_seconds():
    time.sleep(3)
    print('睡眠3秒结束')
def sleep_8_seconds():
    time.sleep(8)
    print('睡眠8秒结束')
thread_1 = threading.Thread(target=sleep_8_seconds)
thread_2 = threading.Thread(target=sleep_5_seconds)
thread_3 = threading.Thread(target=sleep_5_seconds)
thread_1.start()
thread_2.start()
thread_3.start()
thread_1.join()
thread_2.join()
thread_3.join()

运行效果如下图所示:

更有甚者,这样写代码:

thread_1.start()
thread_1.join()
thread_2.start()
thread_2.join()
thread_3.start()
thread_3.join()

运行效果如下图所示:

发现三个线程是串行执行的,要运行一共8+5+3=16秒才能结束,于是得出结论——Python 由于有 GIL 锁的原因,所以多线程是一个线程运行完才运行另一个线程。

抱有这种想法的人,是根本不知道.join()有什么用,就在跟着别人乱用,以为只要使用多线程,那么每个线程都必须要 join。

实际上,根本不是这样的,你只需要 join运行时间最长的那个线程就可以了:

你会发现这样的运行效果,跟每个线程 join 一次是完全一样的。

要理解这个问题,我们需要知道,join 有什么作用。

当我们没有 join 的时候,我们会发现子线程似乎也能正常运行,如下图所示:

三个子线程启动以后,主线程会继续运行后面的代码。

那 join 到底有什么用呢?join 会卡住主线程,并让当前已经 start 的子线程继续运行,直到调用.join的这个线程运行完毕。

所以,如果代码写为:

thread_1.start()
thread_1.join()
thread_2.start()
thread_2.join()
thread_3.start()
thread_3.join()

当代码运行到thread_1.join()时,主线程就卡住了,后面的thread_2.start()根本没有执行。此时当前只有 thread_1执行过.start()方法,所以此时只有 thread_1再运行。这个线程需要执行8秒钟。等8秒过后,thread_1结束,于是主线程才会运行到thread_2.start(),第二个线程才会开始运行。所以这个例子里面,三个线程串行运行,完全是写代码的人有问题,而不是什么 GIL 锁的问题。

而当我们把代码写为:

thread_1.start()
thread_2.start()
thread_3.start()
thread_1.join()
thread_2.join()
thread_3.join()

当代码执行到thread_1.join()时,当前三个子线程均已经执行过.start()方法了,所以此时主线程虽然卡住了,但是三个子线程会继续运行。其中线程3先结束,然后线程2结束。此时线程1还剩3秒钟,所以此时thread_1.join()依然是卡住的状态,直到线程1结束,thread_1.join()解除阻塞,代码运行到thread_2.join()中,但由于thread_2早就结束了,所以这行代码一闪而过,不会卡住。同理,thread_3.join()也是一闪而过。所以整个过程中,thread_2.join()thread_3.join()根本没有起到任何作用。直接就结束了。

所以,你只需要 join 时间最长的这个线程就可以了。时间短的线程没有 join 的必要。根本不需要把这么多个 join 堆在一起。

为什么会有 join 这个功能呢?我们设想这样一个场景。你的爬虫使用10个线程爬取100个 URL,主线程需要等到所有URL 都已经爬取完成以后,再来分析数据。此时就可以通过 join 先把主线程卡住,等到10个子线程全部运行结束了,再用主线程进行后面的操作。

那么可能有人会问,如果我不知道哪个线程先运行完,那个线程后运行完怎么办?这个时候是不是就要每个线程都执行 join 操作了呢?

确实,这种情况下,每个线程使用 join是合理的:

thread_list = []
for _ in range(10):
    thread = threading.Thread(target=xxx, args=(xxx, xxx)) 换行thread.start()
    thread_list.append(thread)
for thread in thread_list:
    thread.join()


目录
相关文章
|
Java 程序员 调度
如何用Java编写代码来等待一个线程join()??
如何用Java编写代码来等待一个线程join()??
43 0
|
7月前
|
Java
在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()
这篇内容讨论了如何在Java中通过多线程控制特定顺序的打印任务。
56 0
|
程序员 调度
多线程的创建,复习匿名内部类,Thread的一些方法,以及lambda的变量捕捉,join用法(二)
多线程的创建,复习匿名内部类,Thread的一些方法,以及lambda的变量捕捉,join用法
|
前端开发 Java 程序员
多线程的创建,复习匿名内部类,Thread的一些方法,以及lambda的变量捕捉,join用法(一)
多线程的创建,复习匿名内部类,Thread的一些方法,以及lambda的变量捕捉,join用法
|
7月前
|
Java
学习多线程之join方法
学习多线程之join方法
83 0
|
7月前
|
NoSQL Java 程序员
多线程并发之线程池Executor与Fork/Join框架
多线程并发之线程池Executor与Fork/Join框架
95 0
|
Java 调度
69. 对并发熟悉吗?谈谈线程间的协作(wait/notify/sleep/yield/join)
69. 对并发熟悉吗?谈谈线程间的协作(wait/notify/sleep/yield/join)
59 1
69. 对并发熟悉吗?谈谈线程间的协作(wait/notify/sleep/yield/join)
【多线程】一文图解wait()、notify()、join()源码
这一篇我们主要是对wait()、notify()、join()进行图解,可能有些粗糙,不足之处多多指出。
|
Java 调度
join线程执行结束之后,并没有看到哪里有notify方法,请问此时谁去唤醒等待池中的线程
Java中的join方法,阻塞当前线程,直到join线程结束后才继续执行。底层是通过wait来实现的,join线程执行结束之后,并没有看到哪里有notify方法,请问此时谁去唤醒等待池中的线程(join之前的那个“当前”线程)呢?
105 0
join线程执行结束之后,并没有看到哪里有notify方法,请问此时谁去唤醒等待池中的线程
|
设计模式 安全 Java
多线程的创建、线程的状态和调度and同步、join和yield以及单例设计模式的种类
多线程的创建、线程的状态和调度and同步、join和yield以及单例设计模式的种类
107 0