本篇文章将给大家介绍 Python 多线程与多进程相关知识,学习完该知识点之后,你的 Python 程序将进入另一个高峰。
十五、Python 多线程与多进程
先尝试理解线程与进程的概念,进程范围大,一个进程可能会包含多个线程,OK,了解到这一步就可以了,知道谁包含谁已经很不错了,细节的地方慢慢研究。
打开你电脑上的任务管理器,注意这里面以前说的叫做杀掉进程。
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.Thread
是 threading
模块内的一个类,我们可以继承这个类,定义自己的线程类,定义的时候有两个需要注意的地方,第一个需要在构造函数中调用 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
对象的两个方法 acquire
与 release
。
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()
以上代码没有使用 acquire
与 release
方法,出现的结果无规律可循,是因为各线程无法预期谁会优先取得资源,专业描述叫做 线程以不可预知的速度向前推进
,当然有的地方叫做线程竞速,一个意思。
稍微修改一下就可以让线程按照规矩执行了,在使用全局变量的时候,先锁定资源,使用之后在释放资源。
# 调用全局变量
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.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:梦想橡皮擦,希望大家点赞、评论、收藏。