之前学习的python编程只能实现程序的单一运行,如果碰到同时执行多个功能,运行多个任务,这样就使用多任务编程了。
一、多任务的介绍:
1.多任务的概念:
多任务是指在同一时间内执行多个任务。
2.生活中的多任务:
<1>操作系统可以同时多个任务
<2>单核cpu如何运行多个软件(并行与并发):
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
二、多线程:
1.线程的概念:
线程就是在程序运行过程中,执行程序代码的一个分支,每个运行的程序至少都有一个线程。多线程是完成多任务方法之一。
python的多线程是操作提供的接口,因为Python自带的数据结构不能保证线程安全,所以需要解释器级别的锁,即:全局解释器锁(GIL锁)。同一时刻一个解释器进程只能有一个线程正在运行,多个线程就需要多个解释器,所以python的多线程并不是真正意义上的多线程。
2.单线程任务:
实力代码:
#!/usr/bin/python3
# coding=utf-8
# 定义一个唱歌方法,表示唱歌任务
def sing():
for _ in range(15):
print("唱歌...")
# 定义一个跳舞方法,表示跳舞任务
def dance():
for _ in range(15):
print("跳舞...")
if __name__ == '__main__':
sing()
dance()
运行结果:
唱歌...
唱歌...
...
唱歌...
跳舞...
跳舞...
...
跳舞...
这是一般形式的python代码,通过执行结果便可以看出,唱歌任务和跳舞任务是先后执行的。
3.多线程执行:
<1>导入线程模块:
import threading
<2>创建线程对象:
sub_thread = threading.Thread(group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None)
线程对象Thread类参数说明:
group:线程组名,当前不需要设置,目前只能为None
target:线程实行的函数名(方法名)
nane:线程名
args:以元组类型,想要执行的函数(方法)中传参
kwargs:以字典类型,想要执行的函数(方法)中传参
daemon:是否设置守护主线程(True / False )
注意:
*后的参数只能使用关键字传参,不能使用位置参数传参
<3>启动子线程:
# 调用Thread.start(),启动子线程
sub_thread.start()
# 查看当前执行的线程名:
thread_obj = threading.current_thread() # Return the current Thread object
<4>完成多线程执行多任务的代码:
#!/usr/bin/python3
# coding=utf-8
import time
import threading
# 唱歌任务
def sing():
# 查看当前代码执行的任务
current_thread = threading.current_thread()
print("singthread:", current_thread)
for i in range(5):
print("唱歌中...")
time.sleep(0.1)
# 跳舞任务
def dance():
# 查看当前代码执行的任务
current_thread = threading.current_thread()
print("dancethread:", current_thread)
for i in range(5):
print("跳舞中...")
time.sleep(0.1)
if __name__ == '__main__':
# 查看当前代码执行的任务
current_thread = threading.current_thread()
print("mainthread:", current_thread)
# 使用线程完成多任务,主线程负责创建两个子线程,子线程负责执行对应的任务,多线程完成多任务
# group: 线程组,不需要设置,目前只能使用None
# target: 线程执行的任务名(函数名/方法名)
sing_thread = threading.Thread(target=sing ,name="mythread-1")
dance_thread = threading.Thread(target=dance ,name="mythread-2")
print("打印线程名:", sing_thread, dance_thread)
# 启动线程, 必须启动线程才能执行任务
sing_thread.start()
dance_thread.start()
运行结果:
mainthread: <_MainThread(MainThread, started 17208)>
打印线程名:
singthread:
唱歌中...
dancethread:
跳舞中...
跳舞中...
唱歌中...
跳舞中...
唱歌中...
唱歌中...
跳舞中...
跳舞中...
唱歌中...
<5> 多线程执行带参数的任务:
#!/usr/bin/python3
# coding=utf-8
import threading
import time
# 定义唱歌任务
def sing(count):
for i in range(count):
print("在唱第%d句歌..." % i)
time.sleep(0.2)
#定义跳舞任务
def dance(count):
for i in range(count):
print("在完成第%d个动作..." % i)
time.sleep(0.2)
# 定义打印个人信息的任务
def show_info(name, age):
print("姓名:%s, 年龄:%d" %(name, age))
if __name__ == '__main__':
# 创建三个子线程
sing_thread = threading.Thread(target=sing,args=(5,))
dance_thread = threading.Thread(target=dance,args=(5,))
show_thread = threading.Thread(target=show_info, args=("小明",), kwargs={"age":18})
# 启用线程
show_thread.start()
show_thread.join() # 线程等待
sing_thread.start()
dance_thread.start()
执行结果:
姓名:小明, 年龄:18
在唱第0句歌...
在完成第0个动作...
在唱第1句歌...
在完成第1个动作...
在唱第2句歌...
在完成第2个动作...
在唱第3句歌...
在完成第3个动作...
在完成第4个动作...
在唱第4句歌...
注意:必须启动线程,才能执行任务
<4>获取线程列表:
thread_list = threading.enumerate() # 获取了线程对象列表
三、线程的注意点:
1.线程的执行是无序的, 是由cpu调度决定的.
代码:
#!/usr/bin/python3
# coding=utf-8
import threading
import time
def task():
# 任务执行完成大概需要1秒钟
time.sleep(1)
# 查看当前执行代码的线程
current_thread = threading.current_thread()
print(current_thread)
if __name__ == '__main__':
# 模拟大量线程执行任务,查看线程的执行顺序
for i in range(10):
sub_thread = threading.Thread(target=task)
sub_thread.start()
运行结果:
2. 主线程会等待所有子线程结束之后才退出:
代码:
#!/usr/bin/python3
# coding=utf-8
import threading
import time
def task():
time.sleep(2)
print(threading.current_thread())
print("函数执行完成...")
if __name__ == '__main__':
# 创建线程
sub_tread = threading.Thread(target=task)
sub_tread.start()
print(threading.current_thread())
# 主线程等待0.2秒
time.sleep(0.2)
print("程序执行完了...")
exit() #强制程序退出
运行结果:
<_MainThread(MainThread, started 16352)>
程序执行完了...
函数执行完成...
3.守护进程:
守护进程概念:当主线程结束后,守护进程随即被销毁,不再执行后面的代码.
代码:
#!/usr/bin/python3
# coding=utf-8
import threading
import time
def task():
time.sleep(2)
print(threading.current_thread())
print("函数执行完成...")
if __name__ == '__main__':
# 创建线程
sub_tread = threading.Thread(target=task)
# 2.设置守护线程,主线程退出,子线程直接销毁,子线程不在执行后边的代码
sub_tread.setDaemon(True)
# 或者在创建线程时直接设置守护线程
# sub_tread = threading.Thread(target=task, daemon=True)
sub_tread.start()
print(threading.current_thread())
# 主线程等待0.2秒
time.sleep(0.2)
print("程序执行完了...")
exit() #强制程序退出
运行结果:
<_MainThread(MainThread, started 6028)>
程序执行完了...
由运行结果可以看出,主程序执行到exit()后,程序直接结束,并没有之前的,主进程等待紫禁城完成后再退出的情况.
四、自定义线程:
1.自定义线程代码:
<1>继承threading.Thread类
<2>封装线程执行相关复杂的任务,好处是代码进行封装,完成代码的解耦.
<3>自定义线程,以后任务的执行,统一在run方法中执行(重写的Thread中的run的方法)
<4>运行子线程不能直接调用run()方法,应该使用start()方法.
自定义线程代码:
#!/usr/bin/env python
# coding=utf-8
import threading
import time
# 定义线程类
class MyThread(threading.Thread):
# 初始化对象
def __init__(self, var1, var2):
# 调用父类方法
# 根据指定类在类继承链中找到下一个类的对应方法, Thread.__init__()
# super().__init__() ==> super(MyThread, self).__init__()
super(MyThread, self).__init__()
self.var1 = var1
self.var2 = var2
# 定义一个不需要参数的任务方法
# 如果没有使用当前对象,可以吧方法定义成静态,此处忽略
def task1(self):
print("打印当前线程:",threading.current_thread())
print("哈哈哈,这是第一个复杂的无参的任务...")
time.sleep(2)
# 定义一个需要参数的任务方法
def task2(self):
print("打印当前线程:", threading.current_thread())
print("哈哈哈,这是第一个复杂的有参的任务...",self.var1)
time.sleep(2)
def task3(self):
print("打印当前线程:", threading.current_thread())
print("哈哈哈,这是第二个复杂的有参的任务...",self.var2)
time.sleep(2)
# 重写父类的run()方法
def run(self):
# 执行线程任务
self.task1()
self.task2()
self.task3()
if __name__ == '__main__':
# 创建线程对象
custom_thread = MyThread("参数1", "参数2")
#打印一下线程对象的继承关系
print("MyThread类的继承关系:",MyThread.mro())
# 启动线程
custom_thread.start()
运行结果:
MyThread类的继承关系: [, , ]
打印当前线程:
哈哈哈,这是第一个复杂的无参的任务...
打印当前线程:
哈哈哈,这是第一个复杂的有参的任务... 参数1
打印当前线程:
哈哈哈,这是第二个复杂的有参的任务... 参数2
五、共享全局变量:
1.多线程共享全局变量:
代码:
#!/usr/bin/env python
# coding=utf-8
import threading
import time
# 定义全局变量
g_list = list()
# global:加global表示全局变量要修改内存地址
# 定义添加数据的任务
def add_data():
for i in range(5):
g_list.append(i)
print("添加的数据:", i)
time.sleep(0.2)
print("打印全局变量列表:", g_list)
def read_data():
print("readData:", g_list)
if __name__ == '__main__':
# 创建线程
add_thread = threading.Thread(target=add_data)
read_thread = threading.Thread(target=read_data)
# 启动线程
add_thread.start()
read_thread.start()
运行结果:
添加的数据: 0
readData: [0]
添加的数据: 1
添加的数据: 2
添加的数据: 3
添加的数据: 4
打印全局变量列表: [0, 1, 2, 3, 4]
从运行结果中可以看出,read这个任务在add没有完成的时候就执行了,这不是我们想要的结果,我们的目的时add任务添加完所有数据之后,再由read任务读出.
2.多线程共享全局变量:存在的问题:
代码演示:
import threading
# 定义全局变量
g_num = 0
# 循环1000000次,每次给全局变量加1
def task1():
# 声明要修改全局变量
global g_num
for i in range(1000000):
g_num += 1
print("task1:", g_num)
# 循环1000000次,每次给全局变量加1
def task2():
# 声明要修改全局变量
global g_num
for i in range(1000000):
g_num += 1
print("task2:", g_num)
if __name__ == '__main__':
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
# 启动线程执行对应的任务
first_thread.start()
second_thread.start()
执行结果:
task1: 1207978
task2: 1329645
由执行结果可以看出:当数据量达到一定数量级,任务有一定的复杂度之后,出现了资源竞争,数据错乱的情况.
解决办法:线程等待、使用互斥锁
线程同步: 保证同一时刻只能有一个线程去操作全局变量
同步: 就是协同步调,按预定的先后次序进行运行。
2.线程等待: sub_thread.join():
代码:
if __name__ == '__main__':
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
# 启动线程执行对应的任务
first_thread.start()
# 主线程等待第一个线程执行完成以后程序再继续执行
first_thread.join()
second_thread.start()
运行结果:
task1: 1000000
task2: 2000000
提示: 让一个线程先执行完成,再让另外一个线程执行。
使用join多线程变成单任务,是一个线程执行完成另外一个线程再去执行,执行效率下降了,数据安全了。
七、互斥锁:(抢占式锁)
1.线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
2.互斥锁为资源引入一个状态:锁定/非锁定
3.某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
4.threading模块中定义了Lock变量,这个变量本质上是一个函数,可以方便的处理锁定
具体那个线程抢到这个锁我们决定不了,是由cpu调度决定的
5.作用:保障全局共享数据在一个时刻只有一个时刻在使用,保证数据安全
创建锁:
lock = threading.Lock(): 创建全局的互斥锁对象,Lock其实就是变量指向一个函数
# 创建全局的互斥锁 , Lock 其实就是变量指向一个函数
lock = threading.Lock()
锁定:
# 上锁
lock.acquire()
释放锁:
# 释放锁
lock.release()
6.使用互斥锁完成对共享数据分别进行100w的次的加法
互斥锁是全局的
代码:
#!/usr/bin/env python
# coding=utf-8
import threading
import time
# 创建全局的互斥锁,Lock其实是变量指向一个函数
lock = threading.Lock()
# 定义全局变量
g_num = 0
# 循环100w次,每次给全局变量+1
def task1():
# 上锁
lock.acquire()
global g_num
for i in range(1000000):
g_num += 1
time.sleep(1)
print("task1:", g_num)
# 释放锁
lock.release()
# 循环100w次,每次给全局变量+1
def task2():
# 上锁
lock.acquire()
global g_num
for i in range(1000000):
g_num += 1
time.sleep(1)
print("task2:", g_num)
# 释放锁
lock.release()
if __name__ == '__main__':
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
# 启动线程
first_thread.start()
second_thread.start()
运行结果:
task1: 1000000
task2: 2000000
7.使用互斥锁的目的
能够保证多个线程访问共享数据不会出现资源竞争及数据错误。
8.上锁、解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
9.总结
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
多线程执行变成了包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
锁使用不好就容易出现死锁情况
八、死锁:
1.死锁的概念:
死锁:一直等待对方释放所得情景。
2.死锁的实例:
生产者消费者问题:
代码:
import threading
import time
# 创建互斥锁
lock = threading.Lock()
# 根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):
# 上锁
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
# 判断下标释放越界
if index >= len(my_list):
print("下标越界:", index)
return
value = my_list[index]
print(value)
time.sleep(0.2)
# 释放锁
lock.release()
if __name__ == '__main__':
# 模拟大量线程去执行取值操作
for i in range(30):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
运行结果:
3
6
8
1
下标越界: 4
# 执行到此,程序既不向下执行,也不退出
看运行结果,出现了死锁的情况,程序既不向下执行,也不退出。也就是说被进程占用的资源没有被释放。
解除死锁,既需要再合适的位置释放锁,放开权限。
# 根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):
# 上锁
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
# 判断下标释放越界
if index >= len(my_list):
print("下标越界:", index)
# 判断下标越界,释放锁,跳出函数
lock.release()
return
value = my_list[index]
print(value)
time.sleep(0.2)
# 释放锁
lock.release()