一、为什么要有多任务?
平时唱歌跳舞都是一起进行的,如果先唱歌后跳舞,或者先跳舞后唱歌,感觉很不协调,别扭—所以需要一个多任务
案例:
from time import sleep def sing(): for i in range(3): print("正在唱歌....%d"%i) sleep(1) def dance(): for i in range(3): print("正在跳舞....%d"%i) sleep(1) if __name__ == '__main__': sing() dance()
二、多任务介绍
概念:多任务就是可以一边听歌,一边上网冲浪,或者在干点其它什么的,可以同时有2个以上的任务在执行
注意:
并发:某个时间段内,多个任务交替执行。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态(暂停)。
并行:同一时间处理多任务的能力,多有多个线程在操作时,CPU同时处理这些线程请求的能力。
三、threading介绍
Python的thread模块是比较底层的,Python的threading模块是对thread做了包装,使用更加方便。
threading常用方法:
方法名 解释
threading.active_count() 返回当前处于active状态的Thread对象
threading.current_thread() 返回当前Thread对象
threading.get_ident() 返回当前线程的线程标识符,线程标识符是一个非负整数,并无特殊含义,知识用来标识线程,该证书可能会被循环利用。Python3.3版本后支持该方法
threading.enumerate() 返回当前处于active解释器的线程对象。Python3.4版本以后支持该方法
threading.main_thread() 返回主线程对象,即启动Python解释器的线程对象。Python3.4版本以后支持该方法
threading.stack_size() 返回创建线程时使用的栈的大小,如果指定size参数,则用来指定后续创建的线程使用的栈大小,size必须是0(标识使用系统默认值)或大于32K正整数
1、Thread类使用说明
threading模块提供了Thread、Lock、RLock、Conditon、Event、Timer和Semaphore等类来支持多线程,Thread是其中最重要也是最基本的一个类,通过该类创建线程并控制线程的运行。
使用Thread创建线程的方法:
- 为构造函数传递一个可调用对象
- 集成Thread类并在子类中重写__init__()和run()方法
- 语法格式:threading.Thread(group=None,target=None,name=None,args=(),kwargs={},*,daemon=None)
- 参数说明:
- group:通常默认即可,作为日后扩展ThreadGroup类实现而保留。
- target:用于run()方法调用的可调用对象,默认为None
- name:线程名,默认是Thread-N格式构成唯一名称,N是十进制数(正整数)
- args:用于调用目标函数的关键字参数字典,默认为{}
- daemon:设置线程是否为守护模式,默认None
threading.Thread的方法和属性:
|方法名 | 解释 |
|–|–|
| start() | 启动线程 |
|run()|线程代码,用来实现线程的功能与业务逻辑,可以在子类中重写该方法来自定义线程的行为|
|init(self,group=None,target=None,name=None,args=(),kwargs=None,daemon=None)|构造函数|
|is_alive()|判断线程是否存活|
|getName()|返回线程名|
|setName()|设置线程名|
|isDaemon()|判断线程是否为守护线程|
|setDaemon()|设置线程是否为守护线程|
|name|用来读取或设置线程的名字|
|ident|线程标识,用非0数字或None(线程未被启动)|
|daemon|线程是否为守护线程,默认false|
|join(timeout=None)|当timeout=None时,会等待至线程结束;当非None时,会等待timeout时间结束,单位秒|
2、实例化threading.Thread(重点)
1)单线程执行 --时间间隔
from time import sleep import datetime def sing(): print("正在唱歌....") sleep(1) if __name__ == '__main__': for i in range(3): sing() print(datetime.datetime.now())
2)使用threading模块
import threading from time import sleep import datetime def sing(): print("正在唱歌....") sleep(1) if __name__ == '__main__': for i in range(3): t = threading.Thread(target=sing) t.start() print(datetime.datetime.now())
看执行速度能对比出来threading多线程速度飞快
调用start()才会创建真正的多线程,开始执行代码
3)主线程等待所有子线程结束后才结束
import threading from time import sleep, ctime def sing(): for i in range(3): print("正在唱歌....%d" % i) sleep(1) def dance(): for i in range(3): print("正在跳舞....%d" % i) sleep(1) if __name__ == '__main__': print("开始:", ctime()) t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) t1.start() t2.start() print("结束: ", ctime())
4)查看线程数
import threading from time import sleep, ctime def sing(): for i in range(3): print("正在唱歌....%d" % i) sleep(1) def dance(): for i in range(3): print("正在跳舞....%d" % i) sleep(1) if __name__ == '__main__': print("开始:", ctime()) t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) t1.start() t2.start() while True: num = len(threading.enumerate()) print("线程数: %d" % num) if (num) < 1: break sleep(1) print("结束: ", ctime())
四、继承threading.Thread
1、线程执行封装代码
使用threading线程模块封装时,定义一个新的子类class,继承threading.Thread,重写run方法
import threading from time import sleep class MyClass(threading.Thread): def run(self): for i in range(3): sleep(1) msg = self.name + "--->" + str(i) print(msg) if __name__ == '__main__': t = MyClass() t.start()
Python的threading.Thread类有一个run方法,定义线程功能函数,可以覆盖该方法,创建自己的线程实例后,调用start启动,交给虚拟机进行调度,有执行机会就会调用run方法执行线程。
2、线程执行顺序
import threading from time import sleep class MyClass(threading.Thread): def run(self): for i in range(3): sleep(1) msg = self.name + "--->" + str(i) print(msg+'\n') def test(): for i in range(5): t = MyClass() t.start() if __name__ == '__main__': test()
多线程执行顺序是不确定的,sleep时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。线程调度选择一个线程执行。以上代码能保证运行每个线程都运行完整的run函数,但是线程启动顺序、run函数每次循环执行的顺序不能确定。
总结:
每个线程默认有一个名字
当线程run方法结束时,该线程完成。
无法控制线程调度顺序,可以通过其它办法影响线程调度方式。
五、多线程共享全局变量(重点)
from threading import Thread from time import sleep gl_num = 10 def sing(): global gl_num for i in range(3): gl_num += 1 print("sing------>%d"%gl_num) def dance(): global gl_num print("sing------>%d" % gl_num) t1 = Thread(target=sing) t1.start() sleep(1) t2 = Thread(target=dance) t2.start()
列表当做参数传递:
from threading import Thread from time import sleep gl_num = [10, 20, 30] def sing(num): num.append(33) print("sing------>", num) def dance(num): sleep(1) print("sing------>", num) t1 = Thread(target=sing, args=(gl_num,)) t1.start() t2 = Thread(target=dance, args=(gl_num,)) t2.start()
同进程内的所有线程共享全局变量,方便多个线程共享数据
缺点,线程一单被修改造成多线程之间全局变量混乱现象(使用时保证全局变量不能被修改,线程非安全的)
六、线程同步和锁(重点)
1、线程同步
概念:协同步调,按预定的先后顺序执行,多线程修改全局变量,可能会出现意外结果,为了保证数据的正确性,需要对多个线程进行同步。
方法:使用Thread对象的Lock和Rlock实现简单线程同步,acquire和release方法对只允许一个线程操作的数据,可以将其放到acquire和release之间。
2、互斥锁
某个线程需要修改共享数据时,先将其锁定,此时资源状态为锁定,其它线程不允许修改;直到该线程释放资源,将资源状态调整非锁定,其它线程才能再次锁定该共享数据。互斥锁保证每次只有一个线程进行数据修改,能够保证多线程数据正确性。
threading模块中定义Lock类,方便的处理锁:
import threading # 创建锁 mutex = threading.Lock() # 锁定 mutex.acquire() # 解锁 mutex.release()
调用acquire之前没有锁,不会堵塞;已锁,会堵塞,直到这个锁解锁
案例:互斥锁两个线程对统一变量累加10万次
import threading from time import sleep gl = 0 mutex = threading.Lock() def sing(num): global gl for i in range(num): mutex.acquire() gl += 1 mutex.release() print("唱歌-----> %d" % gl) def dance(num): global gl for i in range(num): mutex.acquire() gl += 1 mutex.release() print("跳舞------>%d" % gl) t1 = threading.Thread(target=sing, args=(100000,)) t1.start() t2 = threading.Thread(target=dance, args=(100000,)) t2.start() while len(threading.enumerate()) != 1: sleep(1) print("最终结果: %d" % gl
锁的拆解过程:
当一个线程调用acquire()方法时锁定,状态是locked
-只允许有一个线程获得锁,另一个线程想使用,该线程变成blocked状态,阻塞,直至有锁的线程调用release()方法释放锁,此时变成unlocked状态。
-线程调度从阻塞线程中选择一个来调用锁,并使此线程进入运行running状态。
总结:
锁好处:确保了代码可以由某一线程从开始到结束独立运行
锁坏处:阻止了多线程并发执行,单线程执行效率低下;使用多个锁可能会造成死锁
3、死锁
解释:线程间共享多个资源时,两个线程并各站一部分资源,同时等待对方释放资源,会造成死锁。
如何避免死锁:
程序设计时尽量避免
添加超时时间等