- 多任务:
- 一、进程
- 导入进程包 import multiprocessing
- 5.获取进程编号
- 跳舞任务
- 唱歌任务
- 6.进程执行带有参数的任务
- 带有参数的任务
- 小结
- 7.进程的注意点
- 定义全局变量
- 添加数据的任务
- 总结
- 二、线程
- 多线程的使用
- 导入线程模块
- 唱歌任务
- 跳舞任务
- 线程执行带有参数的任务
- 带有参数的任务
- 线程的注意点
- 测试主线程是否会等待子线程执行完成以后程序再退出
- 定义全局变量
- 写入数据任务
- 读取数据任务
- 定义全局变量
- 循环一次给全局变量加1
- 循环一次给全局变量加1
- 定义全局变量
- 创建全局互斥锁
- 循环一次给全局变量加1
- 循环一次给全局变量加1
- 死锁
- 创建互斥锁
- 根据下标去取值, 保证同一时刻只能有一个线程去取值
- 进程和线程的对比
- 信号量
- GIL全局解释器锁
- ⽣产者消费者模式
- 协程
多任务:
1.利用现学知识能够让两个函数或者方法同时执行吗?
不能,因为之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成另外一个函数或者方法才能执行,要想实现这种操作就需要使用多任务。
多任务的最大好处是充分利用CPU资源,提高程序的执行效率。
2. 多任务的概念
多任务是指在同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。
多任务效果图:
3. 多任务的执行方式
- 并发
- 并行
并发:
在一段时间内交替去执行任务。
例如:
对于单核cpu处理多任务,操作系统轮流让各个软件交替执行,假如:软件1执行0.01秒,切换到软件2,软件2执行0.01秒,再切换到软件3,执行0.01秒……这样反复执行下去。表面上看,每个软件都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像这些软件都在同时执行一样,这里需要注意单核cpu是并发的执行多任务的。
并行:
对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行。
4. 小结
- 使用多任务就能充分利用CPU资源,提高程序的执行效率,让你的程序具备处理多个任务的能力。
- 多任务执行方式有两种方式:并发和并行,这里并行才是多个任务真正意义一起执行。
一、进程
1. 进程的介绍
在Python程序中,想要实现多任务可以使用进程来完成,进程是实现多任务的一种方式。2. 进程的概念
一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
比如:现实生活中的公司可以理解成是一个进程,公司提供办公资源(电脑、办公桌椅等),真正干活的是员工,员工可以理解成线程。
注意:
一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。3. 进程的作用
单进程效果图:
多进程效果图:
说明:
多进程可以完成多任务,每个进程就好比一家独立的公司,每个公司都各自在运营,每个进程也各自在运行,执行各自的任务。4.多进程的使用
1 导入进程包
导入进程包 import multiprocessing
2. Process进程类的说明
Process([group [, target [, name [, args [, kwargs]]]]])
- group:指定进程组,目前只能使用None
- target:执行的目标任务名
- name:进程名字
- args:以元组方式给执行任务传参
- kwargs:以字典方式给执行任务传参
Process创建的实例对象的常用方法:
- start():启动子进程实例(创建子进程)
- join():等待子进程执行结束
- terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
3. 多进程完成多任务的代码
import multiprocessingimport time# 跳舞任务def dance():for i in range(5):print("跳舞中...")time.sleep(0.2)# 唱歌任务def sing():for i in range(5):print("唱歌中...")time.sleep(0.2)if __name__ == '__main__':# 创建跳舞的子进程# group: 表示进程组,目前只能使用None# target: 表示执行的目标任务名(函数名、方法名)# name: 进程名称, 默认是Process-1, .....dance_process = multiprocessing.Process(target=dance, name="myprocess1")sing_process = multiprocessing.Process(target=sing)# 启动子进程执行对应的任务dance_process.start()sing_process.start()
执行结果:
唱歌中...跳舞中...唱歌中...跳舞中...唱歌中...跳舞中...唱歌中...跳舞中...唱歌中...跳舞中...
小结
- 导入进程包
- import multiprocessing
- 创建子进程并指定执行的任务
- sub_process = multiprocessing.Process (target=任务名)
- 启动进程执行任务
- 获取当前进程编号
- 获取当前父进程编号
2. 获取当前进程编号
os.getpid() 表示获取当前进程编号
示例代码: ```python import multiprocessing import time import os
跳舞任务
def dance():
# 获取当前进程的编号print("dance:", os.getpid())# 获取当前进程print("dance:", multiprocessing.current_process())for i in range(5):print("跳舞中...")time.sleep(0.2)# 扩展:根据进程编号杀死指定进程os.kill(os.getpid(), 9)
唱歌任务
def sing():
# 获取当前进程的编号print("sing:", os.getpid())# 获取当前进程print("sing:", multiprocessing.current_process())for i in range(5):print("唱歌中...")time.sleep(0.2)
if name == ‘main‘:
# 获取当前进程的编号
print("main:", os.getpid())
# 获取当前进程
print("main:", multiprocessing.current_process())
# 创建跳舞的子进程
# group: 表示进程组,目前只能使用None
# target: 表示执行的目标任务名(函数名、方法名)
# name: 进程名称, 默认是Process-1, .....
dance_process = multiprocessing.Process(target=dance, name="myprocess1")
sing_process = multiprocessing.Process(target=sing)
# 启动子进程执行对应的任务
dance_process.start()
sing_process.start()
**执行结果:**
```python
main: 70763
main: <_MainProcess(MainProcess, started)>
dance: 70768
dance: <Process(myprocess1, started)>
跳舞中...
sing: 70769
sing: <Process(Process-2, started)>
唱歌中...
唱歌中...
唱歌中...
唱歌中...
唱歌中...
3. 获取当前父进程编号
os.getppid() 表示获取当前父进程编号
示例代码:
import multiprocessing
import time
import os
# 跳舞任务
def dance():
# 获取当前进程的编号
print("dance:", os.getpid())
# 获取当前进程
print("dance:", multiprocessing.current_process())
# 获取父进程的编号
print("dance的父进程编号:", os.getppid())
for i in range(5):
print("跳舞中...")
time.sleep(0.2)
# 扩展:根据进程编号杀死指定进程
os.kill(os.getpid(), 9)
# 唱歌任务
def sing():
# 获取当前进程的编号
print("sing:", os.getpid())
# 获取当前进程
print("sing:", multiprocessing.current_process())
# 获取父进程的编号
print("sing的父进程编号:", os.getppid())
for i in range(5):
print("唱歌中...")
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前进程的编号
print("main:", os.getpid())
# 获取当前进程
print("main:", multiprocessing.current_process())
# 创建跳舞的子进程
# group: 表示进程组,目前只能使用None
# target: 表示执行的目标任务名(函数名、方法名)
# name: 进程名称, 默认是Process-1, .....
dance_process = multiprocessing.Process(target=dance, name="myprocess1")
sing_process = multiprocessing.Process(target=sing)
# 启动子进程执行对应的任务
dance_process.start()
sing_process.start()
main: 70860
main: <_MainProcess(MainProcess, started)>
dance: 70861
dance: <Process(myprocess1, started)>
dance的父进程编号: 70860
跳舞中...
sing: 70862
sing: <Process(Process-2, started)>
sing的父进程编号: 70860
唱歌中...
唱歌中...
唱歌中...
唱歌中...
唱歌中...
小结
- 获取当前进程编号
- os.getpid()
- 获取当前父进程编号
- os.getppid()
-
6.进程执行带有参数的任务
1. 进程执行带有参数的任务的介绍
前面我们使用进程执行的任务是没有参数的,假如我们使用进程执行的任务带有参数,如何给函数传参呢?
Process类执行任务并给任务传参数有两种方式: args 表示以元组的方式给执行任务传参
- kwargs 表示以字典方式给执行任务传参
2. args参数的使用
示例代码: ```python import multiprocessing import time
带有参数的任务
def task(count): for i in range(count): print(“任务执行中..”) time.sleep(0.2) else: print(“任务执行完成”)
if name == ‘main‘:
# 创建子进程
# args: 以元组的方式给任务传入参数
sub_process = multiprocessing.Process(target=task, args=(5,))
sub_process.start()
**执行结果:**
```python
任务执行中..
任务执行中..
任务执行中..
任务执行中..
任务执行中..
任务执行完成
3. kwargs参数的使用
示例代码:
import multiprocessing
import time
# 带有参数的任务
def task(count):
for i in range(count):
print("任务执行中..")
time.sleep(0.2)
else:
print("任务执行完成")
if __name__ == '__main__':
# 创建子进程
# kwargs: 表示以字典方式传入参数
sub_process = multiprocessing.Process(target=task, kwargs={"count": 3})
sub_process.start()
执行结果:
任务执行中..
任务执行中..
任务执行中..
任务执行完成
小结
- 进程执行任务并传参有两种方式:
定义全局变量
g_list = list()
添加数据的任务
def add_data(): for i in range(5): g_list.append(i) print(“add:”, i) time.sleep(0.2)
# 代码执行到此,说明数据添加完成
print("add_data:", g_list)
def read_data(): print(“read_data”, g_list)
if name == ‘main‘:
# 创建添加数据的子进程
add_data_process = multiprocessing.Process(target=add_data)
# 创建读取数据的子进程
read_data_process = multiprocessing.Process(target=read_data)
# 启动子进程执行对应的任务
add_data_process.start()
# 主进程等待添加数据的子进程执行完成以后程序再继续往下执行,读取数据
add_data_process.join()
read_data_process.start()
print("main:", g_list)
# 总结: 多进程之间不共享全局变量
**执行结果:**
```python
add: 0
add: 1
add: 2
add: 3
add: 4
add_data: [0, 1, 2, 3, 4]
main: []
read_data []
进程之间不共享全局变量的解释效果图:
3. 进程之间不共享全局变量的小结
创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。
4. 主进程会等待所有的子进程执行结束再结束
假如我们现在创建一个子进程,这个子进程执行完大概需要2秒钟,现在让主进程执行0.5秒钟就退出程序,查看一下执行结果,示例代码如下:
import multiprocessing
import time
# 定义进程所需要执行的任务
def task():
for i in range(10):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
# 创建子进程
sub_process = multiprocessing.Process(target=task)
sub_process.start()
# 主进程延时0.5秒钟
time.sleep(0.5)
print("over")
exit()
# 总结: 主进程会等待所有的子进程执行完成以后程序再退出
执行结果:
任务执行中...
任务执行中...
任务执行中...
over
任务执行中...
任务执行中...
任务执行中...
任务执行中...
任务执行中...
任务执行中...
任务执行中...
说明:
通过上面代码的执行结果,我们可以得知: 主进程会等待所有的子进程执行结束再结束
假如我们就让主进程执行0.5秒钟,子进程就销毁不再执行,那怎么办呢?
- 我们可以设置守护主进程 或者 在主进程退出之前 让子进程销毁
守护主进程:
- 守护主进程就是主进程退出子进程销毁不再执行
子进程销毁:
- 子进程执行结束
保证主进程正常退出的示例代码:
import multiprocessing
import time
# 定义进程所需要执行的任务
def task():
for i in range(10):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
# 创建子进程
sub_process = multiprocessing.Process(target=task)
# 设置守护主进程,主进程退出子进程直接销毁,子进程的生命周期依赖与主进程
# sub_process.daemon = True
sub_process.start()
time.sleep(0.5)
print("over")
# 让子进程销毁
sub_process.terminate()
exit()
# 总结: 主进程会等待所有的子进程执行完成以后程序再退出
# 如果想要主进程退出子进程销毁,可以设置守护主进程或者在主进程退出之前让子进程销毁
执行结果:
任务执行中...
任务执行中...
任务执行中...
over
5. 主进程会等待所有的子进程执行结束再结束的小结
- 为了保证子进程能够正常的运行,主进程会等所有的子进程执行完成以后再销毁,设置守护主进程的目的是主进程退出子进程销毁,不让主进程再等待子进程去执行。
- 设置守护主进程方式: 子进程对象.daemon = True
-
总结
进程是操作系统进行资源分配的基本单位。
-
二、线程
线程:实现多任务的另一种方式
一个进程中,也经常需要同时做多件事,就需要同时运行多个‘子任务’,这些子任务,就是线程
- 线程又被称为轻量级进程(lightweight process),是更小的执行单元
- 一个进程可拥有多个并行的(concurrent)线程,当中每一个线程,共享当前进程的资源
- 一个进程中的线程共享相同的内存单元/内存地址空间可以访问相同的变量和对象,而且它们从同一堆中分配对象通信、数据交换、同步操作
- 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快线程与进程的区别一般来讲:我们把进程用来分配资源,线程用来具体执行(CPU调度)
1. 线程的介绍
在Python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另外一种方式。
2. 线程的概念
线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度 ,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。
3. 线程的作用
小结
线程是Python程序中实现多任务的另外一种方式,线程的执行需要cpu调度来完成。
多线程的使用
1. 导入线程模块
导入线程模块
import threading
2. 线程类Thread参数说明
Thread([group [, target [, name [, args [, kwargs]]]]])
- group: 线程组,目前只能使用None
- target: 执行的目标任务名
- args: 以元组的方式给执行任务传参
- kwargs: 以字典方式给执行任务传参
- name: 线程名,一般不用设置
3. 启动线程
启动线程使用start方法
第一:通过 threading.Thread 直接在线程中运行函数; ```python import threading, time
def saySorry(): print(“子线程%s启动” % (threading.current_thread().name)) # 当前线程的名字 time.sleep(1) print(“我能吃饭了吗?”)
if name == “main“: print(‘主线程%s启动’ % (threading.current_thread().name)) for i in range(5): t = threading.Thread(target=saySorry) # Thread():指定线程要执行的代码 t.start()
第二:通过继承 threading.Thread类来创建线程
- 这种方法只需要重载 threading.Thread 类的 run 方法,然后调用 start()开启线程就可以了
```python
import threading
class MyThread(threading.Thread):
def run(self):
for i in range(5):
print(i)
if __name__ == "__main__":
t1 = MyThread()
t2 = MyThread()
t1.start()
t2.start()
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
线程5种状态
- 1、新状态:线程对象已经创建,还没有在其上调用start()方法。
- 2、可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
- 3、运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
- 4、等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的(可运行的),但是当前没有条件运行。但是如果某件事件出现,他可能返回到可运行状态。
- 5、死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出异常。
4. 多线程完成多任务的代码
```python import threading import time
唱歌任务
def sing():
# 扩展: 获取当前线程
# print("sing当前执行的线程为:", threading.current_thread())
for i in range(3):
print("正在唱歌...%d" % i)
time.sleep(1)
跳舞任务
def dance():
# 扩展: 获取当前线程
# print("dance当前执行的线程为:", threading.current_thread())
for i in range(3):
print("正在跳舞...%d" % i)
time.sleep(1)
if name == ‘main‘:
# 扩展: 获取当前线程
# print("当前执行的线程为:", threading.current_thread())
# 创建唱歌的线程
# target: 线程执行的函数名
sing_thread = threading.Thread(target=sing)
# 创建跳舞的线程
dance_thread = threading.Thread(target=dance)
# 开启线程
sing_thread.start()
dance_thread.start()
**执行结果:**
```python
正在唱歌...0
正在跳舞...0
正在唱歌...1
正在跳舞...1
正在唱歌...2
正在跳舞...2
小结
- 导入线程模块
- import threading
- 创建子线程并指定执行的任务
- sub_thread = threading.Thread(target=任务名)
- 启动线程执行任务
带有参数的任务
def task(count): for i in range(count): print(“任务执行中..”) time.sleep(0.2) else: print(“任务执行完成”)
if name == ‘main‘:
# 创建子线程
# args: 以元组的方式给任务传入参数
sub_thread = threading.Thread(target=task, args=(5,))
sub_thread.start()
**执行结果:**
```python
任务执行中..
任务执行中..
任务执行中..
任务执行中..
任务执行中..
任务执行完成
3. kwargs参数的使用
示例代码:
import threading
import time
# 带有参数的任务
def task(count):
for i in range(count):
print("任务执行中..")
time.sleep(0.2)
else:
print("任务执行完成")
if __name__ == '__main__':
# 创建子线程
# kwargs: 表示以字典方式传入参数
sub_thread = threading.Thread(target=task, kwargs={"count": 3})
sub_thread.start()
执行结果:
任务执行中..
任务执行中..
任务执行中..
任务执行完成
小结
- 线程执行任务并传参有两种方式:
- 线程之间执行是无序的
- 主线程会等待所有的子线程执行结束再结束
- 线程之间共享全局变量
- 线程之间共享全局变量数据出现错误问题
2. 线程之间执行是无序的
```python import threading import time
def task(): time.sleep(1) print(“当前线程:”, threading.current_thread().name)
if name == ‘main‘:
for _ in range(5): sub_thread = threading.Thread(target=task) sub_thread.start()
**执行结果:**
```python
当前线程: Thread-1
当前线程: Thread-2
当前线程: Thread-4
当前线程: Thread-5
当前线程: Thread-3
说明:
- 线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。
- 进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
3. 主线程会等待所有的子线程执行结束再结束
假如我们现在创建一个子线程,这个子线程执行完大概需要2.5秒钟,现在让主线程执行1秒钟就退出程序,查看一下执行结果,示例代码如下: ```python import threading import time
测试主线程是否会等待子线程执行完成以后程序再退出
def show_info(): for i in range(5): print(“test:”, i) time.sleep(0.5)
if name == ‘main‘: sub_thread = threading.Thread(target=show_info) sub_thread.start()
# 主线程延时1秒
time.sleep(1)
print("over")
**执行结果:**
```python
test: 0
test: 1
over
test: 2
test: 3
test: 4
说明:
通过上面代码的执行结果,我们可以得知: 主线程会等待所有的子线程执行结束再结束
假如我们就让主线程执行1秒钟,子线程就销毁不再执行,那怎么办呢?
- 我们可以设置守护主线程
守护主线程:
- 守护主线程就是主线程退出子线程销毁不再执行
设置守护主线程有两种方式:
- threading.Thread(target=show_info, daemon=True)
- 线程对象.setDaemon(True)
设置守护主线程的示例代码:
import threading
import time
# 测试主线程是否会等待子线程执行完成以后程序再退出
def show_info():
for i in range(5):
print("test:", i)
time.sleep(0.5)
if __name__ == '__main__':
# 创建子线程守护主线程
# daemon=True 守护主线程
# 守护主线程方式1
sub_thread = threading.Thread(target=show_info, daemon=True)
# 设置成为守护主线程,主线程退出后子线程直接销毁不再执行子线程的代码
# 守护主线程方式2
# sub_thread.setDaemon(True)
sub_thread.start()
# 主线程延时1秒
time.sleep(1)
print("over")
执行结果:
test: 0
test: 1
over
3. 线程之间共享全局变量
需求:
- 定义一个列表类型的全局变量
- 创建两个子线程分别执行向全局变量添加数据的任务和向全局变量读取数据的任务
- 查看线程之间是否共享全局变量数据 ```python import threading import time
定义全局变量
my_list = list()
写入数据任务
def write_data(): for i in range(5): my_list.append(i) time.sleep(0.1) print(“write_data:”, my_list)
读取数据任务
def read_data(): print(“read_data:”, my_list)
if name == ‘main‘:
# 创建写入数据的线程
write_thread = threading.Thread(target=write_data)
# 创建读取数据的线程
read_thread = threading.Thread(target=read_data)
write_thread.start()
# 延时
# time.sleep(1)
# 主线程等待写入线程执行完成以后代码在继续往下执行
write_thread.join()
print("开始读取数据啦")
read_thread.start()
**执行结果:**
```python
write_data: [0, 1, 2, 3, 4]
开始读取数据啦
read_data: [0, 1, 2, 3, 4]
4. 线程之间共享全局变量数据出现错误问题
需求:
- 定义两个函数,实现循环100万次,每循环一次给全局变量加1
- 创建两个子线程执行对应的两个函数,查看计算后的结果 ```python import threading
定义全局变量
g_num = 0
循环一次给全局变量加1
def sum_num1(): for i in range(1000000): global g_num g_num += 1
print("sum1:", g_num)
循环一次给全局变量加1
def sum_num2(): for i in range(1000000): global g_num g_num += 1 print(“sum2:”, g_num)
if name == ‘main‘:
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
# 启动线程
second_thread.start()
**执行结果:**
```python
sum1: 1210949
sum2: 1496035
注意点:
多线程同时对全局变量操作数据发生了错误
错误分析:
两个线程first_thread和second_thread都要对全局变量g_num(默认是0)进行加1运算,但是由于是多线程同时操作,有可能出现下面情况:
- 在g_num=0时,first_thread取得g_num=0。此时系统把first_thread调度为”sleeping”状态,把second_thread转换为”running”状态,t2也获得g_num=0
- 然后second_thread对得到的值进行加1并赋给g_num,使得g_num=1
- 然后系统又把second_thread调度为”sleeping”,把first_thread转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
- 这样导致虽然first_thread和first_thread都对g_num加1,但结果仍然是g_num=1
全局变量数据错误的解决办法:
线程同步: 保证同一时刻只能有一个线程去操作全局变量 同步: 就是协同步调,按预定的先后次序进行运行。如:你说完,我再说, 好比现实生活中的对讲机
线程同步的方式:
- 当多个线程⼏乎同时修改某⼀个共享数据的时候, 需要进⾏同步控制
- 线程同步能够保证多个线程安全访问竞争资源, 最简单的同步机制是引⼊互
斥锁 - 互斥锁保证了每次只有⼀个线程进⾏写⼊操作,从⽽保证了多线程情况下数据的正确性(原子性)
- 互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间
import threading
import time
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print("开启线程: " + self.name)
# 获取锁,用于线程同步
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁,开启下一个线程
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
threadLock = threading.Lock()
threads = []
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启新线程
thread1.start()
thread2.start()
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有线程完成
for t in threads:
t.join()#阻塞
print("退出主线程")
所有线程完成后,主线程才退出。
def test1():
global num
if mutex.acquire(): # acquire()方法获得锁
for i in range(1000):
num += 1
mutex.release() # release()方法释放锁
def test2():
global num
if mutex.acquire(): # acquire()方法获得锁
for i in range(1000):
num += 1
mutex.release() # release()方法释放锁
mutex = threading.Lock()
p1 = threading.Thread(target=test1)
p1.start()
p2 = threading.Thread(target=test2)
p2.start()
print(num)
两个线程彼此之间争抢同一把锁,谁获得,谁先执行,另一个等待。
同步和异步
同步调⽤:确定调用的顺序
提交一个任务,自任务开始运行直到此任务结束,我再提交下一个任务
异步调⽤:不确定顺序 一次提交多个任务,然后我就直接执行下一行代码
线程同步-多个线程有序执⾏
比如 线程1 线程2 线程3 各有一把自己的锁,线程1获取锁,释放线程2的锁,线程2获取锁,释放线程3的锁,这样可以按照顺序执行。
#同步
class Task1(threading.Thread):
def run(self):
while True:
if lock1.acquire():
print('-----Task1-----')
time.sleep(1)
lock2.release()
class Task2(threading.Thread):
def run(self):
while True:
if lock2.acquire():
print('-----Task2-----')
time.sleep(1)
lock3.release()
class Task3(threading.Thread):
def run(self):
while True:
if lock3.acquire():
print('-----Task3-----')
time.sleep(1)
lock1.release()
#创建锁,并且锁上
lock1 = threading.Lock()
lock2 = threading.Lock()
lock3 = threading.Lock()
lock3.acquire()
lock2.acquire()
#再创建另外一把锁,并且锁上
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
线程等待的示例代码:
import threading
# 定义全局变量
g_num = 0
# 循环1000000次每次给全局变量加1
def sum_num1():
for i in range(1000000):
global g_num
g_num += 1
print("sum1:", g_num)
# 循环1000000次每次给全局变量加1
def sum_num2():
for i in range(1000000):
global g_num
g_num += 1
print("sum2:", g_num)
if __name__ == '__main__':
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
# 主线程等待第一个线程执行完成以后代码再继续执行,让其执行第二个线程
# 线程同步: 一个任务执行完成以后另外一个任务才能执行,同一个时刻只有一个任务在执行
first_thread.join()
# 启动线程
second_thread.start()
执行结果:
sum1: 1000000
sum2: 2000000
小结
- 线程执行执行是无序的
- 主线程默认会等待所有子线程执行结束再结束,设置守护主线程的目的是主线程退出子线程销毁。
- 线程之间共享全局变量,好处是可以对全局变量的数据进行共享。
线程之间共享全局变量可能会导致数据出现错误问题,可以使用线程同步方式来解决这个问题。
互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
1. 互斥锁的使用
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
互斥锁使用步骤:
# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...
# 释放锁
mutex.release()
注意点:
- acquire和release方法之间的代码同一时刻只能有一个线程去操作
- 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。
2. 使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
```python import threading
定义全局变量
g_num = 0
创建全局互斥锁
lock = threading.Lock()
循环一次给全局变量加1
def sum_num1():
# 上锁
lock.acquire()
for i in range(1000000):
global g_num
g_num += 1
print("sum1:", g_num)
# 释放锁
lock.release()
循环一次给全局变量加1
def sum_num2():
# 上锁
lock.acquire()
for i in range(1000000):
global g_num
g_num += 1
print("sum2:", g_num)
# 释放锁
lock.release()
if name == ‘main‘:
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
second_thread.start()
# 提示:加上互斥锁,那个线程抢到这个锁我们决定不了,那线程抢到锁那个线程先执行,没有抢到的线程需要等待
# 加上互斥锁多任务瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行
**执行结果:**
```python
sum1: 1000000
sum2: 2000000
说明:
通过执行结果可以地址互斥锁能够保证多个线程访问共享数据不会出现数据错误问题
小结
- 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
- 使用互斥锁会影响代码的执行效率,多任务改成了单任务执行
-
死锁
1. 死锁的概念
死锁: 一直等待对方释放锁的情景就是死锁
为了更好的理解死锁,来看一个现实生活的效果图:
说明:
现实社会中,男女双方一直等待对方先道歉的这种行为就好比是死锁。
死锁的结果 -
2. 死锁示例
需求:
根据下标在列表中取值, 保证同一时刻只能有一个线程去取值 ```python 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()
<a name="posQn"></a>
### 3. 避免死锁
- 在合适的地方释放锁
```python
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)
# 当下标越界需要释放锁,让后面的线程还可以取值
lock.release()
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()
小结
2. 区别对比
- 进程之间不共享全局变量
- 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步
- 创建进程的资源开销要比创建线程的资源开销要大
- 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
- 线程不能够独立执行,必须依存在进程中
- 多进程开发比单进程多线程开发稳定性要强
3. 优缺点对比
- 进程优缺点:
- 优点:可以用多核
- 缺点:资源开销大
线程优缺点:
进程和线程都是完成多任务的一种方式
- 多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。
- 多进程可以使用cpu的多核运行,多线程可以共享全局变量。
- 线程不能单独执行必须依附在进程里面
信号量
信号量semaphore:用于控制一个时间点内线程进入数量的锁,信号量是用来控制线程并发数的
跟生活中的红绿灯一个性质。控制每次人流量。 ```python import threading import time
s1 = threading.Semaphore(5)
def foo(): s1.acquire() time.sleep(2) print(“ok”) s1.release()
for i in range(20): t1 = threading.Thread(target=foo, args=()) t1.start() # 此时可以控制同时进入的线程数
控制此时只有5个线程去执行print("ok") 剩下的线程等这信号量释放,如果不使用信号量,输出的时候20个ok差不多同时打印,如果使用了信号量,此时只是5个5个的打印
<a name="lR0dF"></a>
## 线程池
在使用多线程处理任务时也不是线程越多越好。因为在切换线程的时候,需要切换上下文环境,线程很多的时候,依然会造成CPU的大量开销。为解决这个问题,线程池的概念被提出来了。<br />预先创建好一个数量较为优化的线程组,在需要的时候立刻能够使用,就形成了线程池。在Python中,没有内置的较好的线程池模块,需要自己实现或使用第三方模块。
```python
import queue
import time
import threading
class MyThreadPool:
def __init__(self, maxsize=5):
self.maxsize = maxsize
self._pool = queue.Queue(maxsize) # 使用queue队列,创建一个线程池
for _ in range(maxsize):
self._pool.put(threading.Thread)
def get_thread(self):
return self._pool.get()
def add_thread(self):
self._pool.put(threading.Thread)
def run(i, pool):
print('执行任务', i)
time.sleep(1)
pool.add_thread() # 执行完毕后,再向线程池中添加一个线程类
if __name__ == '__main__':
pool = MyThreadPool(5) # 设定线程池中最多只能有5个线程类
for i in range(20):
t = pool.get_thread() # 每个t都是一个线程类
obj = t(target=run, args=(i, pool)) # 这里的obj才是正真的线程对象
obj.start()
print("活动的子线程数: ", threading.active_count() - 1)
创建5个线程存在的池子,每次调用从线程池获取一个,使用完成后在往池子里面存放一个线程。
GIL全局解释器锁
- Cpython独有的锁,牺牲效率保证数据安全
- GIL锁是一把双刃剑,它带来优势的同时也带来一些问题
首先:执行Python文件是什么过程?谁把进程起来的?操作系统将你的应用程序从硬盘加载到内存。运行python文件,在内存中开辟一个进程空间,将你的Python解释器以及py文件加载进去,解释器运行py文件Python解释器分为两部分,先将你的代码通过编译器编译成C的字节码,然后你的虚拟机拿到你的C的字节码,输出机器码,再配合操作系统把你的这个机器码扔给cpu去执行你的py文件中有一个主线程,主线程做的就是这个过程。如果开多线程,每个线程都要进行这个过程
- 理想的情况:
三个线程,得到三个机器码,然后交由CPU,三个线程同时扔给三个CPU,然后同时进行,最大限度的提高效率,但是CPython多线程应用不了多核 - CPython到底干了一件什么事情导致用不了多核?
Cpython在所有线程进入解释器之前加了一个全局解释器锁(GIL锁)。这个锁是互斥锁,是加在解释器上的,导致同一时间只有一个线程在执行所以你用不了多核 - 为什么这么干?
之前写python的人只有一个cpu。。。所以加了一个锁,保证了数据的安全,而且在写python解释器时,更加好写了为什么不取消这个锁?解释器内部的管理全部是针对单线程写的 - 那我该怎么办?
虽然多线程无法应用多核,但是多进程可以应用多核(开销大) - Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么我还要学互斥锁?首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据
然后,我们可以得出结论:保护不同的数据就应该加不同的锁。所以,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理⽣产者消费者模式
- 理想的情况:
在线程世界⾥, ⽣产者就是⽣产数据的线程, 消费者就是消费数据的线程(做包子,吃包子) 经常会出现生产数据的速度大于消费数据的速度,或者生产速度跟不上消费速度
- ⽣产者消费者模式是通过⼀个容器(缓冲区)来解决⽣产者和消费者的强耦合问题
- 例如两个线程共同操作一个列表,一个放数据,一个取数据
- ⽣产者和消费者彼此之间不直接通讯, ⽽通过阻塞队列来进⾏通讯
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不直接找生产者要数据,而是从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力,解耦了生产者和消费者。
import threading, time, queue
q = queue.Queue()
def Produce(name):
count = 0 # conut表示做的包子总个数
while count < 10:
print('厨师%s在做包子中...'%name)
time.sleep(2)
q.put(count) # 容器中添加包子
print('produce%s已经做好了第%s个包子'%(name, count))
count += 1
# q.task_done() # 当做完一个包子后就要给顾客发送一个信号,表示已经做完,让他们吃包子
q.join() #等待接收信号,
print('ok...')
def Consumer(name):
count = 0 # count表示包子被吃的总个数
while count < 10:
time.sleep(2)
data = q.get() # 取包子, 吃包子
print('%seating...'%name)
time.sleep(4) #包子用了4s然后给厨师发送一个信号
q.task_done()
print('\033[32;1mConsumer %s已经把第%s个包子吃了...\033[0m' % (name, data))
count += 1
if __name__ == '__main__':
p1 = threading.Thread(target=Produce, args=('A君',))
c1 = threading.Thread(target=Consumer, args=('B君',))
c2 = threading.Thread(target=Consumer, args=('C君',))
c3 = threading.Thread(target=Consumer, args=('D君',))
p1.start()
c1.start()
c2.start()
c3.start()
协程
- ⽐线程更⼩的执⾏单元(微线程)
- ⼀个线程作为⼀个容器⾥⾯可以放置多个协程
- 只切换函数调用即可完成多线程,可以减少CPU的切换
- 协程⾃⼰主动让出CPU
python还有⼀个⽐greenlet更强⼤的并且能够⾃动切换任务的模块 gevent 原理是当⼀个greenlet遇到IO(指的是input output 输⼊输出)操作时, ⽐如访问⽹络, 就⾃动切换到其他的greenlet, 等到IO操作完成, 再在适当的时候切换回来继续执⾏
进程线程的任务切换是由操作系统自行切换的,你自己不能控制
协程可以通过自己的程序(代码)来进行切换,自己能够控制
gevent只有遇到模块能够识别的IO操作的时候,程序才会进行任务切换,实现并发效果,如果所有程序都没有IO操作,那么就基本属于串行执行了
import gevent
def A():
while True:
print(".........A.........")
gevent.sleep(1) # 用来模拟一个耗时操作
# gevent中:当一个协程遇到耗时操作会自动交出控制权给其他协程
def B():
while True:
print(".........B.........")
gevent.sleep(1) # 每当遇到耗时操作,会自用转到其他协程
g1 = gevent.spawn(A) # 创建一个gevent对象(创建了一个协程),此时就已经开始执行A
g2 = gevent.spawn(B)
g1.join() # 等待协程执行结束
g2.join() # 会等待协程运行结束后再退出

