写点什么

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

发布于: 2021 年 02 月 19 日
15. Python 程序运行速度如何提高十倍?第一遍滚雪球学 Python 收工


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


<center><font color=red>缓解一下视疲劳</font></center>



<center><font color=red>缓解一下视疲劳</font></center>


已完成的文章


  • 1.这才是 Python 学习的正确起手姿势,滚雪球学 Python

  • 2. 无门槛学会数据类型与输入、输出函数,滚雪球学 Python

  • 3. 无转折不编程,滚雪球学 Python

  • 4. 列表一学完,Python 会一半,滚雪球学 Python

  • 5. Python 循环的本质就是一段代码懒得重复写

  • 6. Python 元组,不可变的列表,滚雪球学 Python

  • 7. ✎ 会查新华字典不?会。Python 字典已经掌握了

  • 8. ㊙ Python 集合三板斧,滚雪球学 Python

  • 9. Python 学习过程的第一个山坡,99%的人都倒在了山坡下

  • 10. 比找女朋友还难的技术点,Python 面向对象

  • 11. 用别人写好的代码,完成我的工作,剩下的时间去摸鱼

  • 12. 数据放在本地,心里才更踏实,滚雪球学 Python

  • 13. 如果自己写的 Python 程序出错了,怎么办?

  • 14. Python 与数据库那点事儿,滚雪球学 Python


**本系列文章将在 2021 年春节前完成,欢迎关注,点赞,评论 --- 梦想橡皮擦**


**想学 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 threadingimport 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 threadingimport 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 threadingimport 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 threadingimport 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 threadingimport 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 threadingimport 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 threadingimport 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 = 0lock = 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_numlock.acquire()my_num += 10lock.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 收官。下期见。**


**想学 Python 爬虫,可以订阅橡皮擦专栏哦~** 🈲🈲🈲🈲 点击发现惊喜 🈲🈲🈲🈲


🈚🈚🈚🈚🈚




如果你想跟博主建立亲密关系,可以关注同名公众号 <font color="red">梦想橡皮擦</font>,近距离接触一个逗趣的互联网高级网虫。

博主 ID:梦想橡皮擦,希望大家<font color="red">点赞</font>、<font color="red">评论</font>、<font color="red">收藏</font>。


发布于: 2021 年 02 月 19 日阅读数: 15
用户头像

爬虫 100 例作者,蓝桥签约作者,博客专家 2021.02.06 加入

6 年产品经理+教学经验,3 年互联网项目管理经验; 互联网资深爱好者; 沉迷各种技术无法自拔,导致年龄被困在 25 岁; CSDN 爬虫 100 例作者。 个人公众号“梦想橡皮擦”。

评论

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