文章目录
多任务优点:充分利用CPU资源,提高程序执行效率
并发:在同一时间内交替执行任务。
并行:多核CPU是并行的执行多任务,始终有多个软件一起执行。
一、进程和线程的概念
1.进程
进程是程序的基本执行实体,即正在运行的程序,会占用对应的内存区域;而程序只是一个静态的指令集合。
线程是操作系统os能够进行运算调度的最小单位,被包含在进程中,是进程中的实际运作单位。也是进程中的控制单元,负责进程中的程序执行。
一个进程可以开启多个线程,其中由一个主线程来调用本进程中的其他线程。
进程与线程的关系:
线程和进程的区别:
1)根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
2)资源开销:进程都有独立的代码和数据空间,程序之间的切换开销大;线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,线程也都有独立的运行栈和程序计数器,线程之间切换的开销小。
3)进程包含线程,一个进程中有一个主线程,或一个主线程+多个其他线程。
4)内存分配:同一个进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
5)影响关系:一个进程崩溃后,基本不会对其他进程产生影响,但是一个线程崩溃则会导致整个进程挂掉。
6)执行情况:每个独立的进程由程序运行的入口、顺序执行序列和程序出口。线程不是独立执行的,必须依赖应用程序。
二、进程
一个正在运行的程序或者软件就是进程。它是操作系统进行资源分配的基本单位。每启动一个进程,操作系统就会分配一定的运行资源(内存)保证进程的运行。
创建进程就是在内存中申请一块内存空间将需要运行的代码丢进去
一个进程对应在内存中就是一块独立的内存空间
多个进程对应在内存中就是多块独立的内存空间
进程与进程之间数据默认情况下是无法直接交互,如果想交互可以借助于第三方工具、模块。
注意:并不是所有情况下多线程都能够实现并发的效果:GIL。
GIL是否的情况:
- 执行c代码
- 执行IO
- C模块进行释放
不释放的情况: - 执行python字节码
- 充分使用多核CPU并行性能。
所以将任务分为2类:
- IO密集型(多线程)
- CPU密集型(多进程)
进程使用三步骤:
1.导入进程包
2.创建子进程
3.启动进程执行对应的任务
主进程和子进程代码实现:
# 1.导入进程包
import multiprocessing
import time
#创建任务
def dance():
for i in range(3):
print("舞蹈中。。。")
time.sleep(3)
def sing():
for i in range(3):
print("唱歌中。。。")
time.sleep(3)
# 2.创建子进程
'''
group:进程组,目前只能使用None,一般不需要设置
target:进程执行的目标任务
name:进程名,如果不设置,默认是Process-1
'''
if __name__ == '__main__':
dance_pro=multiprocessing.Process(target=dance)
# 3.启动进程执行对应的任务
dance_pro.start()
#主进程执行唱歌
sing()
多进程多任务代码:
dance_pro=multiprocessing.Process(target=dance)
sing_pro=multiprocessing.Process(target=sing)
# 3.启动进程执行对应的任务
if __name__ == '__main__':
dance_pro.start()
sing_pro.start()
获取进程编号:
获取当前进程编号:
os.getpid()
获取父进程编号:
os.getppid()
'''
1.导入进程包
2.创建子进程
3.启动进程执行对应的任务
'''
# 1.导入进程包
import multiprocessing
import os
import time
#创建任务
def dance():
#获取当前进程pid
dance_pid=os.getpid()
print("dance_pid:",dance_pid,multiprocessing.current_process())
for i in range(3):
print("舞蹈中。。。")
time.sleep(3)
def sing():
#获取当前进程pid
sing_pid=os.getpid()
print("sing_pid:",sing_pid,multiprocessing.current_process())
sing_parent_pid=os.getppid()
print("sing的父进程pid:",sing_parent_pid)
for i in range(3):
print("唱歌中。。。")
time.sleep(3)
#根据pid强制杀死指定进程,该进程只会执行一次
#os.kill(sing_pid,9)
# 2.创建子进程
dance_pro=multiprocessing.Process(target=dance,name='dance')
sing_pro=multiprocessing.Process(target=sing,name='sing')
# 3.启动进程执行对应的任务
if __name__ == '__main__':
dance_pro.start()
sing_pro.start()
# 获取主进程编号
main_pid = os.getpid()
print('main_pid:', main_pid, multiprocessing.current_process())
result:
main_pid: 17784 <_MainProcess name='MainProcess' parent=None started>
dance_pid: 16208 <Process name='dance' parent=17784 started>
舞蹈中。。。
sing_pid: 13308 <Process name='sing' parent=17784 started>
sing的父进程pid: 17784
唱歌中。。。
舞蹈中。。。
唱歌中。。。
舞蹈中。。。
唱歌中。。。
执行带参数的任务
Process类执行任务并给任务传参数有以下两种方式:
- args表示以元组的方式给执行任务传参
- kwargs以字典方式给执行任务传参
# 1.导入进程包
import multiprocessing
import os
import time
#创建任务
def dance(name,active):
for i in range(1):
print("主题:",name,";动作:",active)
time.sleep(3)
# 2.创建子进程
#dance_pro=multiprocessing.Process(target=dance,args=('黑天鹅','旋转'))
#result:主题: 黑天鹅 ;动作: 旋转
# dance_pro=multiprocessing.Process(target=dance,kwargs={"name":'芭蕾',"active":"踮起脚尖"})
#result:主题: 芭蕾 ;动作: 踮起脚尖
dance_pro=multiprocessing.Process(target=dance,args=('恰恰',),kwargs={"active":"左摆右摆,前摆后摆~"})
#result:主题: 恰恰 ;动作: 左摆右摆,前摆后摆~
# 3.启动进程执行对应的任务
if __name__ == '__main__':
dance_pro.start()
创建子进程,相当于对主进程执行的代码拷贝执行,就会导致无限递归创建子进程,会报错。
设置主进程主动结束
主进程会等待子进程执行完成以后程序再退出,但是如果执行的数量多,就会出现问题,如死循环。
需求:只让主进程执行1s就自动结束
解决方法:
1.让子进程设置为守护主进程,主进程退出子进程销毁,子进程会依赖主进程
2.主进程退出之前先让子进程销毁
# 1.导入进程包
import multiprocessing
import os
import time
#创建任务
def dance():
while True:
print("子进程hhhhhhhh")
time.sleep(1)
if __name__ == '__main__':
dance_pro=multiprocessing.Process(target=dance)
#1.让子进程设置为守护主进程,主进程退出子进程销毁,子进程会依赖主进程
#dance_pro.daemon=True
dance_pro.start()
time.sleep(1)
2.主进程退出之前先让子进程销毁
dance_pro.terminate()
print("主进程over")
进程池
用法跟线程池是一样的。
with ProcessPoolExecutor() as pool:
pool.map(fun,参数)
三、线程
线程是进程中执行代码的一个分支,每个线程想要工作执行代码需要CPU进行调度。线程是CPU调度的基本单位。每个进程至少有一个线程,这个线程就是常说的主线程。
多线程效果图:
线程代码实现:
'''
@pROJECT:multipro
@Author:pengqi
@Date:2022/5/29
'''
import os
import threading
def threadmethod():
thread_pid=os.getpid()
print("`thread1线程pid:",thread_pid,threading.current_thread())
print("thread11111....")
def threadmethod1():
thread_pid = os.getpid()
print("`thread2线程pid:", thread_pid, threading.current_thread())
print("thread22222....")
thread1=threading.Thread(target=threadmethod,daemon=True)#创建线程是守护线程
thread2=threading.Thread(target=threadmethod1)
if __name__=='__main__':
thread_pid=os.getpid()
print("线程pid:",thread_pid,threading.current_thread())
thread2.start()
thread1.start()
result:
线程pid: 17172 <_MainThread(MainThread, started 17168)>
`thread2线程pid: 17172 <Thread(Thread-2 (threadmethod1), started 1032)>
thread22222....
`thread1线程pid: 17172 <Thread(Thread-1 (threadmethod), started 16484)>
thread11111....
线程的传参方式跟进程一样,参考以上进程的代码即可,不一一列举。
注意点:
1.线程执行是无序的
2.主线程会等待所有的线程执行完再结束
3.线程之间共享全局变量
4.线程之间共享全局变量时,会出现数据错误问题
'''
共享全局变量
'''
import os
import threading
import time
sing=[]
def threadmethod():
for i in range(5):
sing.append(i)
print('thread111',sing)
def threadmethod1():
print("thread22222....",sing)
thread1=threading.Thread(target=threadmethod)
thread2=threading.Thread(target=threadmethod1)
if __name__=='__main__':
thread1.start()
thread2.start()
# time.sleep(0.3)
#让当前线程执行完,再执行主线程
thread2.join()
print("main thread over")
result:
thread111 [0, 1, 2, 3, 4]
thread22222.... [0, 1, 2, 3, 4]
main thread over
需求:让两个线程循环执行100万次,每次循环加1。按照常规思想来说,最后的结果是200w,但是实际上比200w少。
分析思路:
'''
数据错误,
'''
import os
import threading
import time
sing=0
def threadmethod():
for i in range(1000000):
global sing
sing+=1
# print("thread1:",sing)
def threadmethod1():
for i in range(1000000):
global sing
sing+=1
# print("thread2:",sing)
thread1=threading.Thread(target=threadmethod)
thread2=threading.Thread(target=threadmethod1)
if __name__=='__main__':
thread1.start()
thread2.start()
# time.sleep(0.3)
print("main:", sing)
解决方法:在任一个线程添加join,主线程等待该线程执行完,再执行。
如:thread1.joini()
线程池
from concurrent.futures import ThreadPoolExecutor
from threading import *
import time
def f(*args):
time.sleep(1)
print("线程之执行")
with ThreadPoolExecutor(max_workers=6) as pool:
print("创建线程后,活跃的线程数:",active_count())
#通过遍历的形式,将一系列的任务提交到线程池中
pool.map(f,range(5))
print("激活线程后,活跃的线程数:",active_count())
a=input("请输入内容:")
print(a*3)
print("线程任务完成后,活跃的线程数:",active_count())
线程同步
使线程同步有两种方法:
1.线程等待(join)
2.互斥锁
终止线程
有以下几种方式:
- 如线程中有循环,终止循环,线程就停止
- 如果run反复出现异常,则线程停止
- 如果线程是守护线程,那么主线程结束得时候,线程会停止。
- 如果线程是非守护线程,那么主线程结束得时候,python持续会等到线程执行完毕后,再结束
- 主线程创建线程,默认是非守护线程
锁
为什么要用锁?
解决多个线程同时执行同一个资源,可能会出问题,为了保护资源,也为了保障数据的准确性,对资源进行同步操作。
互斥锁
互斥锁就是对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意:
- 互斥锁是多个线程去抢资源,抢到锁的线程先执行,没有抢到的线程需要等待,等互斥锁使用完释放后,其他等待的线程再去抢这个锁。
小结:
- 互斥锁就是保证同一时刻只有一个线程操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处:确保某段关键代码只能由一个线程从头到尾完整区执行
- 使用互斥锁会影响代码的执效率,多任务改成了单任务执行
- 互斥锁如果没有用好容易出现死锁问题
步骤:
1.创建锁
2.获取锁,等待直到获取到锁。通过获取锁,可以让其他线程暂时停下来。
2.释放锁,让锁可以备货区,获取到锁的线程继续执行;没有获取到的线程继续等待。
with lock: 上下文管理器,自动获取,释放锁
_i=i
logging.info(f'get {i=}')
time.sleep(0.5)
i=_i-1
logging.info(f'put {i=})
'''
@pROJECT:multipro
@Author:pengqi
@Date:2022/5/29
'''
import os
import threading
import time
sing=0
# 创建互斥锁lock,本质上是一个函数,通过调用函数创建锁
lock=threading.Lock()
def threadmethod():
#上锁
lock.acquire()
for i in range(1000000):
global sing
sing=sing+1
print("thread1:",sing)
#释放锁
lock.release()
def threadmethod1():
#上锁
lock.acquire()
for i in range(1000000):
global sing
sing+=1
print("thread2:",sing)
#释放锁
lock.release()
thread1=threading.Thread(target=threadmethod)
thread2=threading.Thread(target=threadmethod1)
if __name__=='__main__':
thread1.start()
thread2.start()
result:
thread1: 1000000
thread2: 2000000
RLock可重用锁
- 同一时刻,只能被一个线程获取
- 在一个线程中可以被多次获取
- 获取了多少次,就应该释放多少次
Semaphore信号量
- 同一时刻,只能被一些线程获取
- 保护资源同时只被指定数量的线程操作
lock=threading.RLock()
def threadmethod():
#上锁
lock.acquire()
lock.acquire()
for i in range(1000000):
global sing
sing=sing+1
print("thread1:",sing)
#释放锁
lock.release()
lock.release()
死锁
一直等待对方释放锁的情况
- 对同一个锁,获取多次,从第二次开锁阻塞,导致没有机会释放
- 互相等待: 获取A锁,等待B锁,但是B锁在A释放后才能释放B
'''
@pROJECT:multipro
@Author:pengqi
@Date:2022/5/29
'''
import os
import threading
import time
sing=[1,2,3]
# 创建互斥锁lock,本质上是一个函数,通过调用函数创建锁
lock=threading.Lock()
def getValues(index):
#上锁
lock.acquire()
global sing
if i >=len(sing):
print("越界下标:",index)
#释放锁,若该位置没有释放锁,则会导致死锁
lock.release()
return
print(sing[index])
#释放锁
lock.release()
if __name__=='__main__':
for i in range(10):
new_thread=threading.Thread(target=getValues,args=(i,))
new_thread.start()
四、线程与进程的关系
1.线程是依附在进程里的,没有进程就没有线程
2.一个进程至少有一个线程。
区别:
对比项目 | 线程 | 进程 |
---|---|---|
全局变量 | 相互共享,需要注意资源竞争问题。解决方法:互斥锁或线程同步 | 不共享 |
基本单位 | CPU调度 | 操作系统资源分配 |
是否独立执行 | 否,需要依赖在进程中执行 | 是 |
优点 | 资源开销小 | 可以用CPU的多核 |
缺点 | 不能使用多核 | 资源开销大 |
多进程开发比单进程多线程开发稳定性强,某个进程挂掉不会影响其他进程。
如在线听音乐app开发:音乐播放是一个进程,音乐下载是一个进程,若一个进程挂了不会影响另一个功能的使用。若是单进程开发,那整个功能都不能用了。缺点是资源消耗会更大。
只有跟计算密切相关的,想要提高计算能力,这时候可以开发多进程,并且可以运用CPU的多核。