多任务编程——多线程
本文主讲:在Python内多任务编程中的多线程实现方法…..
多任务介绍
什么是多任务?
多任务就是操作系统同一时间执行多个任务,现在多核CPU已经很普及了,但是即便单核CPU也可以实现多任务(单核CPU采用时间片轮询方法实现多任务,因为CPU没秒计算速度特别快!,2.6GHZ的CPU每秒可运行26亿次)
多任务效率极高,在文件下载、爬虫等应用都很广泛!!
多任务优势
单任务与多任务的区别
1:多任务是操作系统同一时间执行多个任务,单任务是同一时间执行单个任务
2:多任务相较于单任务效率快,广泛应用在文件下载、python爬虫等
python程序默认是单任务执行
线程——基本使用【重中之重】
什么是线程?主线程与子线程的关系?
什么是线程?线程可以理解为程序执行的一条分支,也是程序执行流的最小单元,线程是被系统独立调动的和分派的基本单元,线程不拥有自己的系统资源,只拥有一点在运行中必不可少的资源,但是它可以与同属于同一个进程的其他线程共享所拥有的资源
主线程:当一个程序启动时,就会建立一个主进程,这个主进程内又包含了一个主线程,简而言之,系统启动就会建立一个主进程,主进程包含主线程,因此程序启动就会建立一个主线程!!
子线程:子线程是由主线程(程序启动自动生成主线程)创建的,建立之后子线程与主线程一起同时向下执行
理解主线程与子线程的关系:
主线程重要的方面:
1:主线程创建子线程
2:主线程通常最后执行结束(子线程全部结束执行主线程才结束),打扫战场,如各种关闭操作
threading模块的Thread类创建线程(子线程)步骤
1:导入threading模块
2:利用threading模块的Thread类创建子线程对象
3:利用类的target参数为子线程指定分支任务(例如 target = 函数名,注意函数没有括号)
4:启动创建的子线程,创建子线程对象 . start()
创建步骤代码演示
注意事项:
1:在利用 Thread 类创建的子线程对象用targrt参数指定任务是函数,且没有括号!!!
2:创建的子线程对象只有调用start()方法,子线程才会执行!
3:主线程只有在所有子线程全部执行结束后才结束执行!
4:创建多个子线程对象,利用start()方法启动,因为计算机运算速度超快,故可看做多个子线程是同时启动运行的!
线程实例应用(唱歌跳舞):
线程——查看线程名称、总数量(活跃)【重点】
目标:掌握如何查看正在活跃(执行)的线程数量(名称)
threading.enumerate()函数
功能:查看程序中正在活跃的线程数量及其名称,并存放至列表内
语法:thread_list = threading.enumerate( )
注意事项:threading.enumerate()函数只能查看正在活跃(运行)的线程(主线程与子线程)
快速代码体验(查看数量)
查看线程名称
线程——线程函数传参、线程执行顺序【重点】
向线程函数传参数方法1——args元组传参
功能:向子线程target指定的函数任务传递参数,因为有的函数是要有参数的
语法:thread_obj = threading.Thread(target = 函数名 , agrs = (函数参数1,函数参数2,函数参数3))
快速代码体验
线程函数传递参数方法2——kwargs字典传参
功能:向子线程target指定的函数任务传递参数,因为有的函数是要有参数的
语法:thread_obj = threading.Thread(target = 函数名 , kwagrs = {“函数参数1”:参数1的值,…….}
快速代码体验
线程函数传递参数方法2——args元组传参以及kwargs字典混合搭配
功能:向子线程target指定的函数任务传递参数,因为有的函数是要有参数的
语法:thread_obj = threading.Thread(target = 函数名 ,args=(参数1,) kwargs = {“函数参数2”:参数2的值,…….}
注意事项:混合传参,也是总共传递那几个参数
快速代码体验
子线程的执行顺序
答:子线程是由系统独立调动的和分派的基本单元,所以子线程的执行顺序是无序的,是cpu决定的,不是程序员决定的
总结:
1:每一个线程(子线程、主线程)都有自己的名字,它们由python自动指定
2:线程的run()方法结束时该线程结束执行
3:我们无法控制线程的执行顺序,但是我们可以通过其他方式影响调度方式
线程——守护线程【重点】
什么是守护线程?
答:即子线程与主线程的一种约定!将子线程设置为守护主线程后,主线程结束运行后,守护线程(子线程)也会自动结束,反之,主线程没有结束运行,守护线程也不会结束运行。可以将主线程比作皇上,设置为守护线程的子线程比作妃子,皇上驾崩(主线程结束),则妃子们全部殉葬(全部守护线程全部结束执行)
答:如果不将子线程设置为守护线程,在主线程意外结束执行后,子线程还会继续执行,这样是缺乏逻辑的,因此要设置守护线程!
子线程设置为守护线程—— 创建的子线程对象.setDaemon(True)
功能:将子线程设置为守护线程,在主线程结束运行后(意外结束等),守护线程(子线程)也全部结束运行!
语法:创建的子线程对象 . setDaemon(True)
注意事项:
1:将子线程设置为守护线程要在 thread.start()之前设置!
2:如果不将子线程设置为守护线程,主线程意外结束后,子线程会继续执行,这样是不允许的!
3:什么情况下主程序执行完毕?就是在if name == ‘main‘:这行代码下面除了守护线程的代码外,其余代码均执行完毕即主程序执行结束!!
快速代码体验(没将子线程设置为守护线程的结果
将子线程设置为守护线程后的结果截图
pycharm软件按住结束程序按钮显示骷髅头是为什么?
答:因为按下程序结束按钮,主程序结束,但因为多线程,故子线程没有结束执行,子线程还在继续执行,这是没有将子线程设置为守护线程的缘故!!!
线程——并发和并行【了解】
多任务的底层原理
并发与并行概念及区别
并发:当操作系统需要执行的任务数大于计算机CPU数量时,计算机通过系统的各种跳读算法(时间片轮询),实现多个任务一起“执行”(其实不是一起执行的,只不过是计算机运行速度飞快,看上去所有的任务一起执行)
并行:操作系统需要执行的任务数小于或等于cpu内核数,一个cpu会最多执行一个任务,即任务真的是一起执行!
注意事项:计算机一般多任务方式都为并发
线程——自定义线程类【重中之重】
目标:通过继承 threading . Thread 类来自定义线程类(应用于多线程下载、爬虫等)
你问我答:已经有现成的子线程类 threading.Thread 可创建子线程,为啥还要自定义线程类创建子线程呢?
答:为了让每个线程的封装更加完美,所以使用 threading 模块时,需要自定义线程类,这样才会更加完美!!!
通过继承 threading . Thread 类来自定义线程类的步骤
1:class新建一个类,并且继承 threading.Thread 父类
2:重写父类 threading.Thread 的 run()方法
3:通过实例化对象(子线程)的 start()方法(继承父类的start()方法)启动这个自定义线程类
注意事项:
1:父类的start()方法只能调用一次
2:子类在重写父类的__init__方法时,一定要先调用父类的__init__方法,即super().__init__继承父类的属性
3:不管是什么问题,只要是子类继承了父类,那么在给子类实例化属性时,一定要继承父类的实例化属性,即super().init( )
快速代码体验
注意事项代码演示:子类在重写父类的__init__方法时,一定要先调用父类的__init__方法,即super().init
线程——多线程之间共享全局变量【重点】
函数中修改全局变量的注意事项
答:在函数内修改全局变量之前,要先声明这个变量为全局变量,方法为 global 全局变量名
多个线程之间(子线程、主线程)是可以共享全局变量的!
证明逻辑步骤:
1:定义全局变量
2:定义函数1(子线程1)修改全局变量的值
3:定义函数2(子线程2)读取全局变量的值,看读取的全局变量值,是否是被函数1修改后的全局变量值!是的话就证明全局变量是可以在多线程之间共享的!
快速代码体验
线程——多线程共享一个全局变量产生的问题【重点】
多线程间共享同一个全局变量(同时处理这个全局变量)会产生资源竞争等问题
你问我答:多线程什么情况会产生资源竞争的问题呢?
答:两个(多个)子线程同时处理一个公共资源时(比如说同一个全局变量),就会产生资源竞争的问题
图片详解
问题代码解释
解决多线程间资源竞争的方法—— join()
功能:可以有效解决多线程间资源竞争问题,即让一个指定线程先执行完在执行其他线程,即从多线程变为单线程但是会造成程序效率变低
语法:指定线程 . join()
注意事项:
1:子线程的join方法要在子线程的start方法后加入!
2:给某个子线程加入join方法后,这个子线程执行任务时另一个子线程则不启动了,等待这个子线程执行结束,另一个子线程才启动继续执行任务
快速代码体验
线程——同步与异步【重点】
同步与异步
同步:多任务执行时要求有先后顺序,必须一个先执行完毕,另一个线程才能继续执行,只有一个主线!(一个子线程执行,另一个子线程等待这个子线程执行结束,等待期间什么也不做)
异步:多任务执行时没有先后顺序,可以同时执行,存在多条运行主线!
解决多线程同时修改同一个全局变量产生的资源竞争问题(线程锁)
答:可以通过线程同步(同一时间只能有一个线程对全局变量进行修改)的方式(线程锁)来修改多线程间资源竞争问题!思路如下
线程锁机制(同步):两个子线程同时对一个全局变量进行修改,会产生资源竞争问题,采用线程锁机制可以解决此问题,线程锁是在一个子线程修改全局变量时,子线程会对这个全局变量上一把锁,这样其他子线程无法对这个全局变量进行修改,修改完全局变量后,在把锁打开,让其他线程进行修改(线程锁同理)!这就是线程锁机制
图解:
线程——互斥锁【重点】
什么是互斥锁?
答:互斥锁是将线程同步思想落实的一种机制(锁机制),多个线程几乎同时执行同一任务时(注意一定是同一任务!!!),需要进行同步控制!
互斥锁是最简单的线程同步机制!
互斥锁两种状态:锁定/非锁定
互斥锁实现原理:当一个线程执行某个任务时,该线程对这个任务进行锁定,锁定期间不允许其他线程访问执行这个任务,直到该线程执行完当前任务,将锁打开,变为非锁定状态,其他线程才可以对这个任务进行访问执行,这样就避免了资源竞争问题,互斥锁实现了每次只有一个线程执行任务,保证了多线程情况下数据的正确性
创建互斥锁方法—— threading模块的 Lock()类创建互斥锁
功能:对资源进行锁定以及非锁定,解决多任务(多线程)间资源竞争问题
创建互斥锁步骤:
1:创建互斥锁 mutex = threading . Lock( )
2:对资源上锁 mutex . acquire( )
3:对资源进行解锁 mutex . release( )
注意事项:
1:threading模块下的Lock是一个类
2:子线程访问某个资源竞争任务(全局变量)时,为了避免资源竞争问题,要先对这个资源进行上锁,访问结束进行解锁
3:给多任务加互斥锁时,多个线程全部启动,只是一个线程在处理资源时,另一个线程等待这个资源处理完毕后再次处理,这是和join方法的最大区别(join只启动一个线程)
4:利用互斥锁锁资源时,要尽可能少锁竞争资源(代码)
5:互斥锁应用于多个线程几乎同时执行一个任务时才用互斥锁!!这是使用互斥锁条件!!
快速代码体验
线程——死锁【重点】
什么是死锁?
答:死锁发生在多线程间,比如两个子线程同时处理一个共同任务,每个子线程都会占用这个任务的一部分资源,且两个子线程间都在等待对方释放其占有的那部分资源,这样就会造成线程死锁(一个子线程将资源锁住,未释放,另一个子线程还在等待这个子线程释放其资源,造成程序无响应、这就叫死锁)
尽管在多线程间死锁很少发生,但是一旦发生就会造成程序无法响应
死锁案例代码截图
死锁原理截图
多线程怎么避免死锁情况
答:在函数任务退出之前,就要将锁住的数据进行释放,这样就避免了死锁发生!
上面案例解决办法:将数据释放语句mutex.release(),放在return语句前面执行即可完成退出程序前对锁住的数据进行释放
代码演示
未完待续……