15. Python 程序运行速度如何提高十倍?第一遍滚雪球学 Python 收工

601 阅读12分钟

本篇文章将给大家介绍 Python 多线程与多进程相关知识,学习完该知识点之后,你的 Python 程序将进入另一个高峰。

缓解一下视疲劳

15. Python 程序运行速度如何提高十倍?第一遍滚雪球学 Python 收工

缓解一下视疲劳

十五、Python 多线程与多进程

先尝试理解线程与进程的概念,进程范围大,一个进程可能会包含多个线程,OK,了解到这一步就可以了,知道谁包含谁已经很不错了,细节的地方慢慢研究。

打开你电脑上的任务管理器,注意这里面以前说的叫做杀掉进程

15. Python 程序运行速度如何提高十倍?第一遍 滚雪球学 Python 收工

15.1 Python 多线程

让我们把视角转换一下,先从进程中抽离出来,看一下线程,在学习这部分内容的时候,这两个概念一定不要弄错,弄错就翻车了。

15.1.1 简单的多线程

如果一个线程只完成一个事情,那程序会变得特别呆板,例如现在你正在给编写一段代码,那你在编写代码的过程中,你使用的 IDE(代码编辑器)就完全不能做其它事情了,必须等到编写完所有代码之后才可以执行其它操作,所有的事情只能一件挨着一件的做。而且在这个线程会将资源霸占住,例如让其操作一个文件,必须等到它完成操作其它程序才可以使用,这叫做单线程。

如何实现多线程呢,通过导入 Python 内置的 threading 模块可以解决该问题。

import threading

# 定义一个函数,在线程中运行
def thread_work():
    pass

# 在 Python 中运行线程
# 建立线程对象
my_thread = threading.Thread(target=thread_work)
# 启动线程
my_thread.start()

建立一个线程使用的是 threading 模块中的 Thread 方法,该方法会创建一个 Thread 对象(线程对象),使用该方法的时候需要注意方法的参数值是一个函数名称,该参数为 target,后面是线程要调用的函数名称,没有小括号。返回的线程对象在上述代码中叫做 my_thread,自己定义的任意名称都是可以的,遵循变量命名规则即可。

线程的启动需要调用线程对象的 start 方法。

import threading
import time


# 定义一个函数,在线程中运行
def thread_work():
    # 函数内部方法
    print(" my_thread 线程开始工作")
    time.sleep(10)  # 暂停十秒,为了方便模拟操作
    print("时间到了,线程继续工作")


print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread = threading.Thread(target=thread_work)
# 启动线程
my_thread.start()

time.sleep(1)  # 主线程停止 1 秒
print("主线程结束")

代码运行之后重点注意输出的顺序。

主线程开始运行
my_thread 线程开始工作
主线程结束
时间到了,线程继续工作

主线程结束 输出之后,需要等待几秒钟的时间,我们定义的子线程才会开始运行,即输出 时间到了,线程继续工作

15.1.2 子线程传递参数

在创建线程的时候,除了直接调用某函数,也可以向子线程中的函数里传递参数,具体语法格式如下:

my_thread = threading.Thread(target=函数名称,args=['参数1','参数2',....])

具体案例如下,像 thread_work 函数中传递一个 橡皮擦

import threading
import time

# 定义一个函数,在线程中运行
def thread_work(name):
    # 函数内部方法
    print(" my_thread 线程开始工作")
    print("我是从主线程传递进来的参数:", name)
    time.sleep(10)  # 暂停十秒,为了方便模拟操作
    print("时间到了,线程继续工作")

print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread = threading.Thread(target=thread_work, args=["橡皮擦"])
# 启动线程
my_thread.start()

time.sleep(1)  # 主线程停止 1 秒
print("主线程结束")

参数在传递的时候,需要与函数定义时参数匹配。多线程中不建议使用相同的变量,很容易出现问题,建议每个线程使用自己的局部变量,互相之间不要产生干扰。

15.1.3 线程命名

每个线程在启动之后,如果没有手动命名,系统会自动给其命名为 Thread-n,在程序中可以使用 currentThread().getName() 获取线程的名称。随着 Python 版本的迭代,currentThread 方法已经逐步被 current_thread 替代。

import threading
import time

# 定义一个函数,在线程中运行
def thread_work1(name):
    # 函数内部方法
    print(threading.currentThread().getName()," 线程启动")
    time.sleep(2)
    print(threading.currentThread().getName()," 线程启动")


# 定义一个函数,在线程中运行
def thread_work2(name):
    # 函数内部方法
    print(threading.currentThread().getName(), " 线程启动")
    time.sleep(2)
    print(threading.currentThread().getName(), " 线程启动")


print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread1 = threading.Thread(target=thread_work1, args=["橡皮擦"])
my_thread2 = threading.Thread(target=thread_work2, args=["橡皮擦"])
# 启动线程
my_thread1.start()
# 启动线程
my_thread2.start()
time.sleep(1)  # 主线程停止 1 秒
print("主线程结束")

代码运行结果如下,可以重点看一下线程默认的名称。

主线程开始运行
Thread-1  线程启动
Thread-2  线程启动
主线程结束
Thread-2  线程启动
Thread-1  线程启动

如果想要给线程起一个独特的名字,可以在通过 Thread 方法建立线程时,使用参数 name = "线程名称",该名称就是为线程单独命名。

import threading
import time

# 定义一个函数,在线程中运行
def thread_work1(name):
    # 函数内部方法
    print(threading.currentThread().getName()," 线程启动")
    time.sleep(2)
    print(threading.currentThread().getName()," 线程启动")


# 定义一个函数,在线程中运行
def thread_work2(name):
    # 函数内部方法
    print(threading.currentThread().getName(), " 线程启动")
    time.sleep(2)
    print(threading.currentThread().getName(), " 线程启动")


print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread1 = threading.Thread(name="我是线程1(不建议用中文)",target=thread_work1, args=["橡皮擦"])
my_thread2 = threading.Thread(name="work thread",target=thread_work2, args=["橡皮擦"])
# 启动线程
my_thread1.start()
# 启动线程
my_thread2.start()
time.sleep(1)  # 主线程停止 1 秒
print("主线程结束")

除了上述办法以外,还可以使用 currentThread().setName() 给函数命名,自己可以尝试下哦~

15.1.4 Daemon 守护线程

默认创建的线程都不是 Daemon 线程,正常情况下,一个程序建立了主线程和子线程,那程序结束需要等待所有的线程工作结束,因为如果主线程先结束了,那子线程会因为没有可用资源而导致程序崩溃。

如果我们希望主线程结束了,子线程自行终止,那这时就要设置一下 Daemon 线程的属性了,设置之后,主线程若是想要结束运行,需要检查一下 Daemon 线程的属性。

  • 如果 Daemon 线程的属性是 True,其它非 Daemon 线程执行结束,不会等待 Daemon 线程,主线程会自动结束。
  • 如果 Daemon 线程属性是 False,那主线程必须等待 Daemon 线程结束才会将程序结束运行。

以上内容翻译成大白话就是可以把一个线程设置为 Daemon 线程,而且还可以设置一个属性,如果属性设置为 True,那该线程就不受重视了,其它线程结束,它就被结束了,如果设置为 False,那它就是最重要的了,主线程需要等着它结束运行,才可以进行下一步操作。

import threading
import time

# 定义一个函数,在线程中运行
def thread_work1():
    # 函数内部方法
    print(threading.currentThread().getName()," 线程启动")
    # 等待 5 秒,如果被重视,那主线程将等待,如果不被重视,很快就会执行完毕
    time.sleep(5)
    print(threading.currentThread().getName()," 线程启动")


# 定义一个函数,在线程中运行
def thread_work2():
    # 函数内部方法
    print(threading.currentThread().getName(), " 线程启动")
    print(threading.currentThread().getName(), " 线程启动")


print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread1 = threading.Thread(name="我是守护线程 Daemon",target=thread_work1)
my_thread1.setDaemon(True) # 先设置为 True,该线程将不被重视
my_thread2 = threading.Thread(name="work thread",target=thread_work2)
# 启动线程
my_thread1.start()
# 启动线程
my_thread2.start()

print("主线程结束")

以上代码运行之后发现瞬间执行完毕了,并没有等待 5 秒钟,充分证明了不被重视的线程的处境。 接下来修改一个属性,可以再看一下效果。

my_thread1.setDaemon(False)

运行之后发现程序等待 5 秒之后才结束运行,你是否发现了其中的差异呢?

15.1.5 堵塞主线程

主线程在工作的时候,如果希望子线程先运行,直到该子线程运行结束,主线程才继续工作。

import threading
import time

# 定义一个函数,在线程中运行
def thread_work1():
    # 函数内部方法
    print(threading.currentThread().getName()," 线程启动")
    time.sleep(5)
    print(threading.currentThread().getName()," 线程启动")


print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread1 = threading.Thread(name="work thread",target=thread_work1)
# 启动线程
my_thread1.start()
print("join 开始......")
my_thread1.join() # 等待 work thead 线程运行结束
print("join 结束....")

print("主线程结束")

join 方法可以增加一个参数,该参数表示等待的秒数,当秒数到了,主线程恢复工作。

my_thread.join(3) # 子线程运行 3 秒。

15.1.6 is_alive 检验子线程是否在工作

使用 join 方法之后,一般在后面需要加上一个 is_alive 方法,该方法会简称子线程是否工作结束了,如果子线程结束则返回 False,仍在工作则会返回 True。

import threading
import time

# 定义一个函数,在线程中运行
def thread_work1():
    # 函数内部方法
    print(threading.currentThread().getName()," 线程启动")
    time.sleep(5)
    print(threading.currentThread().getName()," 线程启动")


print("主线程开始运行")
# 在 Python 中运行线程
# 建立线程对象
my_thread1 = threading.Thread(name="work thread",target=thread_work1)
# 启动线程
my_thread1.start()
print("join 开始......")
my_thread1.join(2) # 等待 work thead 线程运行结束
print("join 结束....")

print("子线程是否仍在工作?",my_thread1.is_alive())
time.sleep(3)
print("子线程是否仍在工作?",my_thread1.is_alive())
print("主线程结束")

有的教程或者书籍中还会使用 isAlive 方法来进行判断,这是因为 Python 版本的问题,后续建议使用 is_alive 方法。

15.1.7 自定义线程类

threading.Threadthreading 模块内的一个类,我们可以继承这个类,定义自己的线程类,定义的时候有两个需要注意的地方,第一个需要在构造函数中调用 threading.Thread.__init()__ 方法,第二个是需要在类内容定义好 run 方法。 之前的内容中,通过 threading.Thread 声明一个线程对象时,执行 start 方法可以建立一个线程,start 方法就是在调用类中的 run 方法。

import threading


class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        print(threading.Thread.getName(self))
        print("橡皮擦定义好的线程")


my_thread = MyThread()
my_thread.run()

you_thread = MyThread()
you_thread.run()

15.1.8 资源锁定与解锁

在多线程程序中经常碰到多个线程使用一个共享资源的情况,为了确保共享资源在多线程共享时不出现问题,需要使用 theading.Lock 对象的两个方法 acquirerelease

import threading

my_num = 0
lock = threading.Lock()


class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        print(threading.Thread.getName(self))

        # 调用全局变量
        global my_num
        my_num += 10
        print("现在的数字是:", my_num, "\n")


# 线程列表
ts = []
# 批量创建 10 个线程
for i in range(0, 10):
    my_thread = MyThread()
    ts.append(my_thread)

# 启动 10 个线程
for t in ts:
    t.start()

# 等待所有线程结束
for t in ts:
    t.join()

以上代码没有使用 acquirerelease 方法,出现的结果无规律可循,是因为各线程无法预期谁会优先取得资源,专业描述叫做 线程以不可预知的速度向前推进,当然有的地方叫做线程竞速,一个意思。

稍微修改一下就可以让线程按照规矩执行了,在使用全局变量的时候,先锁定资源,使用之后在释放资源。

# 调用全局变量
global my_num
lock.acquire()
my_num += 10
lock.release()
print("现在的数字是:", my_num, "\n")

以上内容如果使用 acquire 连续使用两次就会导致死锁。

关于死锁问题与资源锁定 Threading.RLock,还有高级锁定相关的知识,在以后的滚雪球中继续学习,先阶段掌握基本的锁定就可以啦。

15.1.9 未来要学习的知识

进展到现在你已经可以实现简单的多线程开发了,但是对于线程类的学习只揭示了最简单的一部分,后续我们将学习到如下内容,都在第二遍滚雪球时学习。

  • queue 模块,也叫做队列模块
  • Semaphore 信号量,高级锁机制
  • Barrier 栅栏
  • Event 线程通讯机制

15.2 subprocess 模块

subprocess 是 Python 中用于建立子进程的模块,注意是子进程。导入该模块使用 import subprocess

15.2.1 Popen 方法

该方法可以打开计算机内部的应用程序,也可以打开自己写好的程序,文件路径写对即可。

import subprocess

# 打开计算机
calc_pro = subprocess.Popen('calc.exe')
# 打开画板
mspaint_pro = subprocess.Popen('mspaint.exe')

打开的子进程,主程序已经结束了。

15.2.2 Popen 方法携带参数

可以在 Popen 方法打开程序的时候,传递一个参数进去,该参数为列表类型,第一个元素是要打开的应用程序,第二个则是传递进去的文件。

例如打开画图程序。

import subprocess

# 打开计算机
# calc_pro = subprocess.Popen('calc.exe')
# 打开画板
mspaint_pro = subprocess.Popen(['mspaint.exe','./pic.jpg'])

文件的路径不要写错,以上代码会打开画板程序并且在画板打开一个图片。

15. Python 程序运行速度如何提高十倍?第一遍 滚雪球学 Python 收工

15.2.3 通过 start 打开程序

在电脑上通过双击就可以打开某种文件,这是因为 Windows 系统已经给我们做好了关联,那能不能在 Python 中也模拟出该方式呢,很简单,通过 subprocess.Popen 方法的参数即可实现。

import subprocess

# 打开图片
mspaint_pro = subprocess.Popen(['start','./pic.jpg'],shell = True)

使用该代码打开图片是使用你默认的图片预览程序,满足了刚才所说的场景。该方法核心使用的有两个地方一个是原程序位置使用的是 start 关键字(仅在 Windows 上有效),第一个是 shell = True 参数。

15.2.4 通过 run 方法调用子进程

该方法属于新增方法,通过 subprocess.run 方法即可调用子进程。具体内容可以自行尝试即可。

15.3 这篇博客的总结

本篇博客主要内容是 Python 的多线程应用,顺带着说了一点点关于进程的相关知识,对于多线程,很多学习 Python 很久的同学都不一定可以搞清楚,在这里希望大家第一次学习先有概念支撑即可,能掌握多少在本阶段不重要,学习是需要时间积累的,一遍就会那是天才或者是吹牛的,有很多工作 2~3 年的还不一定能把多线程多进程说清楚呢,所以不要着急哦,继续往后面看,往后面学就好了。

第一遍滚雪球学 Python 收官。下期见。


博主 ID:梦想橡皮擦,希望大家点赞、评论、收藏。