1.多任务
1.1多任务的执行方式
并发:指两个或多个事件在同一时间段内发生。指的是任务数多于cpu核数时,通过操作系统的各种任务调用算法,来实现多个任务“一起”(看上去是一起)执行的效果。
并行:指两个或多个事件在同一时刻发生(同时发生)。指的是任务数小于cpu核数时真的一起执行。
1.2进程与线程
进程:打开一个程序就至少会有一个进程。
一个正在运行的程序或者是一个软件就是一个进程,他是操作系统进行资源分配的基本单位。
一个进程默认有一个线程,进程里可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。
程序启动时默认会有一个主线程(因为线程是程序执行的最小单位)
程序员自己创建的线程可以称为子线程,多线程可以完成多任务
2.线程
2.1单线程
单线程示例:
import time
def speak():
print('hello world')
time.sleep(2) #睡眠2s
print('你好,世界')
def play():
print('劳逸结合')
time.sleep(2)
print('多做运动')
if __name__ == '__main__':
speak()
play()
__name__ == '__main__'表示如果程序文件直接运行,则执行以下代码
1.防止别人导入文件的时候执行main里面的方法
2.防止wiondows系统递归创建子进程
2.2多线程创建
使用threading模块里的Thread类创建出示例对象,然后通过start()方法真正的去产生一个新的线程
程序启动会默认有一个主线程
通过start()方法来启动子线程
注意:导入模块时可以导入import threading模块
创建线程时需要用threading.Thread()
也可以导入from threading import Thread类,创建线程时,直接使用Thread()
2.2.1线程类Thread的参数
group:线程组
target:执行目标的任务名 (也就是函数名)
args:以元组的方式给执行任务进行传参
*args:传任意多个参数
kwargs:以字典方式给执行任务传参
name:线程名
2.2.2步骤
1.导入模块 threading
2.创建子线程 Thread()类
3.守护线程 setDaemon --设置守护线程,主线程执行完了,子线程也会跟着结束
4.启动子线程 start()
5.阻塞主线程 join()a.join--a是子线程,阻塞主线程,主线程会等待子线程a执行完在执行下面的代码
2.2.3多线程的优势
单线程实现:
import time
t1 = time.time()
def sing(name):
print('%s在唱歌' % name)
time.sleep(1) #睡眠2s
if __name__ == '__main__':
for i in range(4):
sing('panda')
t2 = time.time()
print('执行时间是',t2-t1) #执行时间是 4.042494535446167
多线程实现:多个线程同时进行,减少运行时间
import time
import threading #创建多线程
t1 = time.time()
def sing(name):
print('%s在唱歌' % name)
time.sleep(1) #睡眠2s
if __name__ == '__main__':
for i in range(4):
t = threading.Thread(target=sing,args=('panda',)) #一个元素的元组要加,
t.start() #启动线程
t2 = time.time()
print('执行时间是',t2-t1) #0.0010912418365478516
2.2.4多线程的应用
import time
from threading import Thread
def speak():
print('hello world')
time.sleep(2) #睡眠2s
print('你好,世界')
def play():
print('劳逸结合')
time.sleep(2)
print('多做运动')
if __name__ == '__main__':
#创建子线程
f1 = Thread(target=speak)
f2 = Thread(target=play)
#设置守护线程
f1.setDaemon(True)
f2.setDaemon(True)
#开启子线程
f1.start()
f2.start()
print("这是主线程")
这里我们可以发现很多想要的数据没有打印出来,因为这些线程同时进行,设置了守护线程,主线程结束,子线程也结束,停留了两秒才输出的就打印不出来,我们就能用join()方法来阻塞主线程
import time
from threading import Thread
def speak():
print('hello world')
time.sleep(2) #睡眠2s
print('你好,世界')
def play():
print('劳逸结合')
time.sleep(2)
print('多做运动')
if __name__ == '__main__':
#创建子线程
f1 = Thread(target=speak)
f2 = Thread(target=play)
#设置守护线程
f1.setDaemon(True)
f2.setDaemon(True)
#开启子线程
f1.start()
f2.start()
#阻塞主线程
f1.join() #主线程等f1执行完在执行
f2.join() #f2也进行设置所以f1和f2是同时执行的
#修改线程名字
f1.setName('线程一')
f2.setName('线程二')
#获取名字
print(f1.getName())
print(f2.getName())
print("这是主线程")
2.2.5多线程的意义及应用场景
多线程并不是多个线程并发在同一时间点运行,而是cpu有计划的交替执行多个线程
磁盘网络为主的程序 IO密集型,多线程的使用有很大的优势
计算为主的程序 cpu密集型程序,不建议使用多线程操作
2.3线程执行代码的封装
1.继承Thread
2.重构run方法
启动线程start会调用run方法
import time
from threading import Thread
class MyThread(Thread): #定义一个新子类来继承Thread类
def run(self) -> None: #重构run方法,规定run这个名字表示线程活动的方法
print('panda')
time.sleep(2)
print('play')
if __name__ == '__main__':
#创建实例线程
t = MyThread()
t.start() #panda play
start和run方法的区别:
start方法是声明分到一个子线程的函数已经就绪,等待被cpu执行
run方法是执行这个线程,自动调用的方法
from threading import Thread
import threading
class MyThread(Thread): #定义一个新子类来继承Thread类
def run(self) -> None: #重构run方法,规定run这个名字表示线程活动的方法
print(f"当前线程的名字是{threading.current_thread().name}")
if __name__ == '__main__':
#创建实例线程
t1 = MyThread()
t2 = MyThread()
#启动线程
t1.start() #当前线程的名字是Thread-1
t2.start() #当前线程的名字是Thread-2
#run方法启动
t1.run() #当前线程的名字是MainThread
t2.run() #当前线程的名字是MainThread
2.4线程执行是无序的
他是由cpu调用来决定的,cpu调用哪个线程,哪个线程就先执行,没有调用的线程就不执行
import time
from threading import Thread
import threading
def task():
time.sleep(1)
print(f"当前线程的名字是{threading.current_thread().name}\n",end='')
if __name__ == '__main__':
for i in range(5):
t = Thread(target = task)
t.start()
线程美化,手动添加换行
2.5线程之间资源共享(全局变量)
import time
from threading import Thread
li = []
#写入数据
def wdate():
for i in range(5):
li.append(i)
time.sleep(1)
print("写入的数据是:",li)
#读取数据
def rdate():
print("读取的数据是:",li)
if __name__ == '__main__':
#创建子线程
t1 = Thread(target=wdate)
t2 = Thread(target=rdate)
#开启子线程
t1.start()
t2.start()
输出结果:读取的数据是[0]
写入的数据是[0,1,2,3,4]
这里有两种解决办法能读到完整的数据:
1.阻塞线程,加入join等t1任务执行结束,在继续主线程的运行(建议使用)
import time
from threading import Thread
li = []
#写入数据
def wdate():
for i in range(5):
li.append(i)
time.sleep(1)
print("写入的数据是:",li)
#读取数据
def rdate():
print("读取的数据是:",li)
if __name__ == '__main__':
#创建子线程
t1 = Thread(target=wdate)
t2 = Thread(target=rdate)
#开启子线程
t1.start()
#阻塞线程
t1.join()
t2.start()
2.在t1开始后t2开始前,是时间暂停一会,到t1运行完,在执行
import time
from threading import Thread
li = []
#写入数据
def wdate():
for i in range(5):
li.append(i)
time.sleep(1)
print("写入的数据是:",li)
#读取数据
def rdate():
print("读取的数据是:",li)
if __name__ == '__main__':
#创建子线程
t1 = Thread(target=wdate)
t2 = Thread(target=rdate)
#开启子线程
t1.start()
#休眠
time.sleep(5)
t2.start()
2.6资源竞争
a=0
b=1000000
#循环一次就给全局变量a+1
def add():
for i in range(b):
global a
a += 1
print("第一次",a)
def add2():
for i in range(b):
global a
a += 1
print("第二次",a)
add()
add2()
from threading import Thread
a=0
b=1000000
#循环一次就给全局变量a+1
def add():
for i in range(b):
global a
a += 1
print("第一次",a)
def add2():
for i in range(b):
global a
a += 1
print("第二次",a)
# add()
# add2()
if __name__ == '__main__':
t1 = Thread(target=add)
t2 = Thread(target=add2)
t1.start()
t2.start()
放到一个多线程中就会造成资源竞争(当数量一定大时)
2.7线程同步
两种方式:join和互斥锁
2.7.1.join:等待第一个子线程执行完成之后,代码再继续往下执行,开始执行第二个子线程
from threading import Thread
a = 0
b = 1000000
#循环一次就给全局变量a+1
def add():
for i in range(b):
global a
a += 1
print("第一次",a)
def add2():
for i in range(b):
global a
a += 1
print("第二次",a)
# add()
# add2()
if __name__ == '__main__':
t1 = Thread(target=add)
t2 = Thread(target=add2)
t1.start()
t1.join()
t2.start()
2.7.2.互斥锁
概念:对共享数据进行锁定,保证多个线程访问共享数据不会出现错误问题:保证同一时刻只能有一个线程去操作
方法:
acquire():上锁
release():释放锁
注意:这两个方法是成对出现的,否则容易形成死锁(死锁:一直等待对方释放锁的情景,死锁会造成应用程序停止响应,不在处理其他任务)
导入模块,也是导入threading模块,导入的是Lock类
from threading import Thread,Lock
a = 0
b = 1000000
#创建互斥锁
lock = Lock()
#循环一次就给全局变量a+1
def add():
lock.acquire() #上锁
for i in range(b):
global a
a += 1
print("第一次",a)
lock.release() #释放锁
def add2():
lock.acquire() # 上锁
for i in range(b):
global a
a += 1
print("第二次",a)
lock.release() # 释放锁
# add()
# add2()
if __name__ == '__main__':
t1 = Thread(target=add)
t2 = Thread(target=add2)
t1.start()
#t1.join()
t2.start()
注意:互斥锁时多个线程一起去抢,抢到锁的线程先执行
3.进程
3.1.含义:是操作系统进行资源分配和调度的基本单位,是操作系统结构的基础
一个正在运行的程序或者一个软件就是一个进程
程序跑起来就是进程
注意:进程里面可以创建多个线程,多进程可以完成多任务
3.2进程的状态
1.就绪状态:运行条件都已经满足,正在等待cpu执行
2.执行状态:cpu正在执行其功能
3.等待(阻塞)状态:等待某些条件满足,如果一个程序sleep了,此时就处于等待状态
3.3进程的语法结构
multiprocessing模块提供了Process类代表进程对象
3.3.1Process参数
1.target:执行目标任务名,即子进程要执行的任务
2.args:以元组的形式传参
3.kwargs:以字典的形式传参
3.3.2常用的方法
1.start():开启子进程
2.is_alive():判断子进程是否存活,存活返回True,死亡返回False
3.join():等待子进程执行结束
3.3.3常用的属性
name:当前进程的别名。默认Process-N
pid:当前进程的进程编号
在进程中获取进程名只需要 (对象名).name,在线程中需要用getName
1.修改进程名的第一种方法,传参的时候修改
from multiprocessing import Process
def sing():
print("唱歌")
def dance():
print("跳舞")
if __name__ == '__main__':
#创建子进程
p1 = Process(target=sing) #修改进程名的第一种方法
p2 = Process(target=dance,name='子进程二')
#开启
p1.start()
p2.start()
#访问进程名
print(p1.name) #Process-1
print(p2.name) #子进程二
2.修改进程名的第二种方法,设立对象后,直接进行修改
from multiprocessing import Process
def sing():
print("唱歌")
def dance():
print("跳舞")
if __name__ == '__main__':
#创建子进程
p1 = Process(target=sing)
p2 = Process(target=dance)
#开启
p1.start()
p2.start()
#修改进程名
p1.name = '子进程1'
p2.name = '子进程2'
#访问进程名
print(p1.name) #子进程1
print(p2.name) #子进程2
获取进程编号:导入os模块可以获得主进程的进程编号
os.getpid:获取当前进程的进程编号
os.getppid:获取当前进程的父进程编号
子进程的父进程的pid就是当前py文件主进程的pid
当前py文件主进程的父进程的pid就是pycharme软件中的进程编号
from multiprocessing import Process
import os
def sing():
os.getpid() #获取当前进程的进程编号
print(f"sing子进程编号{os.getpid()},父进程编号{os.getppid()}") #父进程的pid就是py文件主进程的pid
print("唱歌")
def dance():
os.getpid()
print(f"dance子进程编号{os.getpid()},父进程编号{os.getppid()}")
print("跳舞")
if __name__ == '__main__':
#创建子进程
p1 = Process(target=sing)
p2 = Process(target=dance)
#开启
p1.start()
p2.start()
#访问进程名
print(p1.name)
print(p2.name)
#查看子进程的进程编号
print(f"p1子进程编号{p1.pid}")
print(f"p2子进程编号{p2.pid}")
print(f"主进程pid{os.getpid()} 父进程pid{os.getppid()}")
查看pycharme软件中的进程编号
cmd命令提示符窗口输入tasklist可以查看电脑里面的进程命令
Ctrl+F查找
pycharm64软件的编号就是主进程的父进程的pid
process方法的进一步使用
from multiprocessing import Process
def sing(name):
print(f"{name}在唱歌")
def dance(name):
print(f"{name}在跳舞")
if __name__ == '__main__':
#创建子进程
p1 = Process(target=sing,args=('panda',))
p2 = Process(target=dance,args=('monkey',))
p1.start()
p2.start()
print('p1的存活状态',p1.is_alive()) #True
print('p2的存活状态',p2.is_alive()) #True
存活是因为主进程先执行再执行子进程,所以在主进程中判断是否存活的时候,紫禁城还没有运行完。
使用完毕后需要使子进程死亡,写在主进程中判断存活状态的时候需要加入join阻塞一下
加入join,主进程处于等待状态,子进程处于运行状态
from multiprocessing import Process
def sing(name):
print(f"{name}在唱歌")
def dance(name):
print(f"{name}在跳舞")
if __name__ == '__main__':
#创建子进程
p1 = Process(target=sing,args=('panda',))
p2 = Process(target=dance,args=('monkey',))
p1.start()
p1.join() #这一步执行完的时候相当于p1已经执行完了(在主进程中,所以是死亡)
p2.start()
p2.join()
print('p1的存活状态',p1.is_alive()) #False
print('p2的存活状态',p2.is_alive()) #False
3.4进程间不共享全局变量
import time
from multiprocessing import Process
li = []
#写入数据
def wdate():
for i in range(5):
li.append(i)
time.sleep(1)
print("写入的数据是:",li)
#读取数据
def rdate():
print("读取的数据是:",li)
if __name__ == '__main__':
#创建子进程
p1 = Process(target=wdate)
p2 = Process(target=rdate)
#开启子进程
p1.start()
p2.start()
加入join方法读取数据输出的结果也始终为空
3.5进程间的通信
Queue(队列)
q.put():放入数据
q.get():取出数据,获取队列中的一条消息,然后将他从队列中移除
q.empty():判断队列是否为空
q.qsize():返回当前队列包含的消息数量
q.full():判断队列是否满了
使用时需要导入模块from queue import Queue
from queue import Queue
q = Queue(3) #最多可以三条消息,没有写或者负值就代表没有上线,直到内存尽头
q.put("我爱世界")
q.put("我爱python")
print(q.qsize()) #2
print(q.full()) #False
q.put("我爱中国")
print(q.full()) #True
print(q.qsize()) #3
print(q.get())
print(q.get())
print(q.empty()) #Flase
print(q.get())
print(q.empty()) #True
可以借助队列完成导入和取出数据的结合
import time
from multiprocessing import Process
from queue import Queue
li = ['panda','monkey','bear']
#写入数据
def wdate(q1):
for i in range(5):
print(f'{i}已经被放入')
q1.put(i)
time.sleep(0.2)
print("写入的数据是:",li)
#读取数据
def rdate(q2):
while True:
#判断队列是否为空,为空就退出循环
if q2.empty():
break
else:
print("输出数据",q2.get())
print("读取的数据是:",li)
if __name__ == '__main__':
#创建队列对象
q = Queue()
#创建子进程
p1 = Process(target=wdate,args=(q,))
p2 = Process(target=rdate,args=(q,))
#开启子进程
p1.start()
p1.join()
p2.start()
4.协程 Coroutine
协程,单线程下的开发,又称微线程,协程时Python中另外一种实现多任务的方式,只不过比线程更小,占用更小的执行单元(理解为需要的资源)。它自带cpu上下文。这样只要在合适的时机,我们可以把一个协程切换到另一个协程。只要这个过程中保存和恢复cpu上下文,那么程序还是可以运行的
注意:线程和进程的操作是由程序触发系统接口,最后的执行者是系统,协程的操作则是程序员
4.1简单实现协程(控制操作时间来控制输出顺序,或者控制代码顺序来控制输出顺序)
import time
def task1():
while True:
yield 'welcome to python'
time.sleep(1)
def task2():
while True:
yield 'nice to meet you'
time.sleep(2)
if __name__ == '__main__':
t1 = task1()
t2 = task2()
while True:
print(next(t1))
print(next(t2))
4.2应用场景
1.如果一个线程里IO(Input/Outout)操作比较多的时候,可以用协程,常见的IO操作:文件操作,网络请求(爬虫)
2.适合高并发处理
4.3greenlet
使用时也需要导入greenlet模块
from greenlet import greenlet
4.3.1greenlet是一个由C语言实现的协程模块。通过设置switch()来实现任意函数之间的转换
为了更好的使用协程来完成任务,Python中的greenlet模块对其封装,从而使得切换任务变得更简单
安装命令:pip install greenlet
卸载:pip install 模块名
查看已安装模块pip list
如果python解释器安装版本过多或者写在不够彻底可能会产生冲突,这时候就需要在前面加上python -m
4.3.2注意:greenlet属于手动切换,当遇到IO操作,程序会阻塞,而不能进行自动切换
4.3.3通过greenlet实现任务间的切换
from greenlet import greenlet
def sing():
print("在唱歌")
g2.switch()
print("唱完歌了")
def dance():
print("在跳舞")
print("跳完舞了")
if __name__ == '__main__':
#创建协程对象
g1 = greenlet(sing)
g2 = greenlet(dance)
g1.switch()
from greenlet import greenlet
def sing():
print("在唱歌")
g2.switch()
print("唱完歌了")
def dance():
print("在跳舞")
g1.switch()
print("跳完舞了")
if __name__ == '__main__':
#创建协程对象
g1 = greenlet(sing)
g2 = greenlet(dance)
g1.switch()
g2.switch()
同一条switch语句跳转过的就不再将执行
4.4gevent
使用时也需要先安装,与上一步一样
greenlet实现了协程,但是需要人工切换,gevent是比greenlet更强大而且能够自动切换的第三方库。属于主动切换
使用时需要导入gevent模块
import gevent
4.4.1使用
gevent.spawn(函数名):创建协程对象
gevent.sleep(): 耗时操作
gevent.join(): 阻塞,等待某个协程执行结束
gevent.joinall(): 等待所有协程对象都执行结束在退出,参数是一个协程对象列表
4.4.2grvent自带耗时操作
import gevent
import time
def sing():
print("在唱歌")
gevent.sleep(2)
print("唱完歌了")
def dance():
print("在跳舞")
gevent.sleep(3)
print("跳完舞了")
if __name__ == '__main__':
#创建协程对象
g1 = gevent.spawn(sing)
g2 = gevent.spawn(dance)
#阻塞,等待协程执行结束
g1.join()
g2.join()
gevent.sleep()模拟了gevent实现的io阻塞,使在唱歌和在跳舞同时执行,如果用time.sleep就会执行完sing函数中的语句,在执行dance函数中的语句(本来加入阻塞应该等待协程执行结束,在跳转,实现了自动跳转)
4.4.3joinall()
import gevent
import time
def sing(name):
for i in range(3):
gevent.sleep(1)
print(f"{name}在唱歌,被送走的第{i}次")
if __name__ == '__main__':
gevent.joinall([gevent.spawn(sing,'panda'),gevent.spawn(sing,'monkey')])
gevent.sleep,进行任务切换,实现了并发的效果。
输出结果:
没有gevent,sleep的输出结果:
4.4.4monkey补丁:拥有在运行模块时替换的功能
使用时需要导入模块from gevent import monkey
monkey.patch_all() 是将time.sleep()代码替换成gevent里面自己实现耗时操作的gevent.sleep()代码(需要放在被打补丁者前)
import gevent
import time
from gevent import monkey
monkey.patch_all()
def sing(name):
for i in range(3):
time.sleep(1)
print(f"{name}在唱歌,被送走的第{i}次")
if __name__ == '__main__':
gevent.joinall([gevent.spawn(sing,'panda'),gevent.spawn(sing,'monkey')])
5.总结
5.1线程是cpu调度的基本单位,进程是资源分配的基本单位
5.2进程,线程,协程对比
进程:需要切换的资源最大,效率最低
线程:切换需要的资源一般,效率一般
协程:切换需要的资源最小,效率最高