多任务的概念:就是操作系统可以同时运行多个任务
单核CPU可不可以执行多任务:
- 也可以执行多任务:由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,
再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,
由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样- 真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,
所以,操作系统也会自动把很多任务轮流调度到每个核心上执行
什么是并发和并行
- 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已),并发是包含并行的
- 并行:指的是在同一时刻去执行,任务数小于等于cpu核数,即任务真的是一起执行
使用python实现同时执行多个任务(并发:进程、线程、协程都可以实现)
并发能做什么呢,一个简单的例子,假如一个人要同时完成两件事情,需要多久?
用代码实现如下:
import time
def func1():
for i in range(5):
print("------正在做事情1------")
time.sleep(1)
def func2():
for i in range(6):
print("------正在做事情2------")
time.sleep(1)
st = time.time()
func1()
func2()
et = time.time()
print("一共需要完成的时间",et - st)
# ======》 一共需要11秒
# 先做事情1
# 在做事情2
假如2个人要同时完成两件事情,需要多久,这就需要用到线程模块,多线程一起执行。
一 、线程模块的详细使用
threading模块介绍:
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
快速使用:from threading import Thread ,使用的是Thread类
创建线程对象: threading.Thread(target=任务函数)
其中参数target指定线程执行的任务 (函数)
线程参数的使用
Thread类有以下参数:
1、target参数: 指定任务函数
import time
from threading import Thread
def func1():
for i in range(5):
print("-正在做事情1------")
time.sleep(1)
# 创建一个线程对象
aa = Thread(target=func1)
2、name参数: 设置线程名
# 创建一个线程对象
aa = Thread(target=func1,name="henry")
3.、args参数: 给任务函数传参(参数传递的是元祖类型)
def func1(aa):
for i in range(5):
print("---{}---正在做事情1------".format(aa)
time.sleep(1)
# 创建一个线程对象
aa = Thread(target=func1,args=('张三',))
4、kwargs参数:给任务函数传参(参数传递的是字典类型)
def func1(aa):
for i in range(5):
print("---{}---正在做事情1------".format(aa)
time.sleep(1)
# 创建一个线程对象
aa = Thread(target=func1,kwargs={'aa': "李四"})
5、daemon参数:设置是否作为守护线程(bool值)
- 设置子线程守护主线程执行(主线程结束,守护线程自动结束)
- 如果没有设置守护线程,主线程跑完,子线程没有跑完,还会继续跑下去,不会有影响
- 如果设置了守护线程,主线程跑完,子线程没有跑完,当前线程全部结束(包括子线程,不管是否跑完)
- 如果多个子线程,一个设置了守护线程,一个没有设置守护线程,主线程结束,子线程还会继续跑下去,不会有影响
def func1(aa):
for i in range(5):
print("---{}---正在做事情1------".format(aa))
# print("---正在做事情1------")
time.sleep(1)
def func2(aa):
for i in range(6):
print("----{}--正在做事情2------".format(aa))
time.sleep(1)
# 创建一个线程对象
t1 = Thread(target=func1, args=('张三',),daemon=True) # 设置守护线程
t2 = Thread(target=func2, kwargs={'aa': "李四"},daemon=True) # 设置守护线程
# 启动线程执行
t1.start()
t2.start()
time.sleep(2)
print("----主线程执行结束-----------")
线程方法的使用
Thread类提供了以下方法:
1、start()方法: 启动线程执行
# 启动线程执行
t1.start()
t2.start()
2、join([time]): 设置主线程等待的时间
- 设置主线程会等待time秒后再往下执行,不设置time,则默认为子线程结束,多个子线程之设置的值会叠加
- 多个子线程同时使用join设置的时间会叠加
def func2():
for i in range(6):
print("------正在做事情2------")
time.sleep(1)
# 创建一个线程对象
t1 = Thread(target=func2) # 子线程t1
# # 子线程t1启动执行
t1.start()
# 主线程等待时间
t1.join() # 让主线程等待t1执行完,再往下执行
# 主线程等待子线程执行完
for i in range(10):
print("主线程全部执行完成——————————————————————————")
3、run方法 :设置线程执行的逻辑代码
# 如何实现线程类的实现实现多线程
1. 创建一个类需要继承 Thread类
2. 设置执行函数(run方法)
3. 设置线程参数
4. 启动线程
# 继承类来创建线程
class GetDataThread(Thread): # 继承Thread类
def run(self):
"""线程执行的逻辑代码"""
for i in range(5):
time.sleep(1)
print("---{}--data-----{}".format(self.name, i))
# 多线程
t1 = GetDataThread(name='线程1')
t2 = GetDataThread(name='线程2')
# 启动线程
t1.start()
t2.start()
4、isDaemon: 判断是否为守护线程
5、isAlive: 判断线程支持存活(处于执行状态)
6、is_alive:判断线程支持存活(处于执行状态)
7、getName:获取线程名
print("线程名为:",t1.getName())
print("是否为守护线程:",t1.isDaemon())
print("线程是否处于执行状态:",t1.isAlive())
print("线程是否处于执行状态:",t1.is_alive())
多线程共享全局变量的问题
1、什么是多线程-共享全局变量
- 线程之间是共用同一块内存的,那么线程可以共享全局变量
- 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
2、多线资源竞争的问题
- 如果多个线程操作同一个全局资源,在频繁的切换线程会出现资源竞争问题,而导致数据丢失或者被覆盖
3、在多线程的时候,什么情况下会触发切换线程:
- 执行时间达到指定的阈值:0.005,(python中默认为0.005)
- 线程执行遇到IO(input,output)输入输出操作:如网络IO,文件IO,等待(time.sleep)
- 获取python线程切换的阈值: print(sys.getswitchinterval())
例如:在下面的函数中,a为全局变量,两个函数中都有,两个线程都去执行,导致a的值不正确
from threading import Thread
a = 0 # 全局变量
def work1():
global a
for i in range(500000):
a += 1
print("work1执行完,a的值:", a)
def work2():
global a
for i in range(500000):
a += 1
print("work2执行完,a的值:", a)
t1 = Thread(target=work1)
t2 = Thread(target=work2)
t1.start()
t2.start()
# 执行结果
work执行完,a的值: 500000
work2执行完,a的值: 824157
a: 824157
多线程共享全局资源竞争如何解决
1、多线程操作时的解决思路:
- 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引⼊互斥锁
- 互斥锁为资源引⼊⼀个状态:锁定/⾮锁定
- 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的线程才能再次锁定该资源
- 互斥锁为资源引⼊⼀个状态:锁定/⾮锁定
- 斥锁保证了每次只有⼀个线程进⾏写⼊操作,从⽽保证了多线程情况下数据的正确性
- threading模块中定义了Lock类,可以方便的处理锁定
2、如何解决:
- 关键性的代码进行加锁,控制线程的执行,避免同时获取数据,对于全局数据修改的那行关键性的代码使用锁锁起来,防止执行到这行代码的时候进行线程切换
- 使用队列来存储数据(队列在设置和获取数据时都实现了锁的功效)
3、Threading模块中定义了Lock类,可以方便的处理锁定:
- 创建锁之前需要实例化: 变量 = threading.Lock()
- 锁定方法 - acquire(): 变量.acquire()
- 释放锁,待锁定 - release(): 变量.release()
4、注意点:
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞
- 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
5、锁的好处:
- 保了某段关键代码只能由一个线程从头到尾完整地执行
6、锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
例如:还是这个函数,a为全局变量,两个函数中都有,两个线程都去执行,去给两个函数分别加上锁,在执行看:
from threading import Thread,Lock
a = 0 # 全局变量
# 实例化一个锁对象
lockA = Lock()
def work1():
global a
for i in range(500000):
lockA.acquire() # 锁定
a += 1
lockA.release() # 释放锁定
print("work1执行完,a的值:", a)
def work2():
global a
for i in range(500000):
lockA.acquire() # 锁定
a += 1
lockA.release() # 释放锁定
print("work2执行完,a的值:", a)
t1 = Thread(target=work1)
t2 = Thread(target=work2)
t1.start()
t2.start()
t1.join()
t2.join()
# 执行结果
work执行完,a的值: 867609
work2执行完,a的值: 1000000
a: 1000000