多任务:线程
多任务:简介
- 操作系统可以同时运行多个任务。操作系统轮流让各个任务交替执行,实现的多任务效果
- 并发:任务数多余CPU核数,通过操作系统的各个任务调度算法,实现用多个任务“一起”执行(多个任务交替执行)
- 并行:任务数小于或等于CPU核心数,即任务是一起执行的
线程:简介
- 线程是程序的最小执行流单元,是程序中一个单一的顺序控制流程
threading 模块
- Python的thread模块比较底层,而threading模块是对thread进行封装,使便于使用
- 当调用start()时,才会真正创建线程,并且开始执行。主线程会等待所有子线程结束后才结束
线程:语法
import threading
t = threading.Thread(target="函数名")
t.start()
查看线程数量
-
len(threading.enumerate())
:查看当前线程数量
线程执行代码的封装
- 通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承
threading.Thread
,然后重写run方法
线程的执行顺序
- 多线程的执行顺序是不确定的。当执行到sleep语句时,线程将会被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度
- 每个线程默认有一个名字,如果不指定线程对象的名字,解释器会自动为线程指定名字
- 当线程的run()方法结束时,该线程完成
- 无法控制线程调度程序,但可以通过其他方式影响线程调度方式
共享全局变量
- 在一个进程内,所有线程共享全局变量,很方便多个线程之间的数据共享。缺点是:线程是对全局变量随意更改,可能会造成多个线程之间对全局变量的混乱(线程不安全)
多线程开发问题
- 如果多个线程同时对同一个全局变量操作,会造成资源竞争,从而导致数据结果异常
互斥锁
- 当多个线程同时修改一个共享数据时,需要进行同步控制,线程同步能够保证多个线程安全访问,最简单方式,就是引入互斥锁
- 互斥锁状态:锁定/非锁定
- 当线程更改共享数据时,会先进行锁定,防止其他线程同时更改;直到线程释放资源,才会解除锁定状态
- 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性
互斥锁:语法
# 创建锁
mutex = threading.Lock()
# 锁定
mutext.acquire()
# 释放
mutex.release()
上锁解锁过程
- 当一个线程调用锁的qcquire()方法获得锁时,就会进入“locked状态”
- 每次只有一个线程可以获得锁,如果此时另一个线程试图获取这个锁,该线程就会变为“blocked”状态,称之为“阻塞”,直到拥有锁的线程调用锁的“release()”方法释放锁之后,锁进入“unlocked”状态
优缺点
- 优点:确保了某段代码只能由一个线程调用
- 缺点:阻止了多线程并发执行,包含锁的某段代码,实际上只能以单线程方式执行,降低效率。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,易造成死锁
死锁
- 在线程间,共享多个资源的时候,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁。一旦发生死锁,程序就会停止响应
避免死锁
GIL
- 全局解释器锁。每个线程执行都必须先获取GIL,保证同一时刻只有一个线程可以执行代码
多任务:进程
进程:简介
- 一个程序运行起来后,代码使用的系统资源称之为“进程”,它是操作系统分配资源的基本单元。不仅可以通过线程完成多任务,进程也可以
进程 :状态
- 就绪态:满足运行条件,等待CPU执行
- 执行态:CPU只在运行其功能
- 等待态:等待某些条件满足
multiprocessing 模块
- multoprocessing模块是跨平台版本的多进程模块,提供一个Process类来代表一个进程对象,这个对象可以理解是一个独立的程序,可以执行其他功能
语法
from multiprocessiong import Process
p = Process(target="函数名")
p.start()
Process
Process([group[,name[,args[,kwargs]]]]) 参数
- target:如果传递了函数的引用,可以引用这个子进程就执行这里的代码
- args:给target指定函数传递的参数,以元组的方式传递
- kwargs:给target指定的函数传递命名参数
- name:给进程设定一个名字,可以不设定,但系统会自动指定一个进程名
- group:指定进程组
Process 常用方法
- start():启动子进程实例(创建子进程)
- is_alive():判断进程或子进程是否存活
- join([timeout]):是否等待子进程执行结束,或等待多少秒
- terminate():不管任务是否完成,立即终止子进程
Process 常用属性
- name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
- pid:当前进程的pid(进程号)
Process获取进程号
进程间通信
Queue
- 可以使用multiprocessing模块的Queue方法来实现多进程之间的数据传递,Queue本身就是一个消息队列程序
- 初始化Queue()对象时,若没有指定最大可接收的消息数量,那么就表示可接收的消息数量没有上线(直到内存用完)
方法
- Queue.qsize():返回当前队列包含的消息数量
- Queue.empty():如果队列为空,返回True,反之False
- Queue.full():如果队列满了,返回True,反之False
- Queue.get([block[,timeout]]):获取队列中的一条消息,然后将其从队列中移除,block默认值为True
- 如果block使用默认值,且没有设置timeout(单位秒),消息队列如果为空,此时程序将被阻塞(停在读取状态),直到从消息队列读完消息为止,如果设置了timeout,则会等待timeout秒,若什么都读取不到,则抛出“Queue.Empty”异常
- 如果block值为False,消息队列为空,则立即抛出“Queue.Empty”异常
- Queue.get_nowait():相当于Queue.get(False)
- Queue.put(item,[block[,timeout]]):将item消息写入队列block默认值为True
- 如果block使用默认值,并且没有设置timeout(单位秒),消息队列没有空间写入,此时程序将会被阻塞(停在写入状态),直到消息队列有空间,如果设置timeout,则会等待timeout秒,若还没空间,则会抛出“Queue.Full”异常
- 如果block值为False,消息队列没有空间写入,则会抛出“Queue.Full”异常
- Queue.put_nowait(item):相当于Queue.put(item,False)
进程池
- 当需要创建的进程数量非常多的时候,可以使用multiprocessing模块提供的Pool方法
- 初始化Pool时,可以指定一个最大进程数,当有新的请求时,提交到Pool中,如果池没满,那么就会创建一个新的进程用来执行该请求。如果池中的进程数达到最大值,那么该请求就会等待,直到池中存在进程结束,才会用来执行新的进程
- 如果使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会报错
multiprocessing.Pool 常用函数
- apply_async(func[,args[,kwds]]):使用非阻塞方式调用func(并行执行,阻塞方式必须等待上一个进程推出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表
- close():关闭Pool,使其不再接受新的任务
- terminate():不管任务是否完成,立即终止
- join():主进程阻塞,等待子进程退出,必须在close或terminate之后使用
多任务:协程
协程:简介
- 协程,又被称为微线程,纤程。协程是Python中另外一种实现多任务的方式,只不过比线程更小,占用更小的执行单元。它自带CPU上下文,在合适的时候可以把一个协程切换到另一个协程中,这个过程中恢复CPU上下文,程序还可以继续运行
简单实现协程
import time
def work1():
while True:
print("-------work1------")
yield
time.sleep(0.5)
def work2():
while True():
print("--------work2------")
yield
time.sleep(0.5)
def main():
w1 = work1()
w2 = work2()
while True:
next(w1)
next(w2)
if __name__ == '__main__':
main()
greenlet
- greenlet模块对协程进行封装,而使切换任务变得更加简单,遇到IO操作时,会等待IO操作完成,才会在适当的时候切换回来,继续运行,非常耗时
安装:greenlet
sudo pip3 install greenlet
greenlet简单实现
from greenlet import greenlet
import time
def test1():
while True:
print("----------A------------")
gr2.switch()
time.sleep(0.5)
def test2():
while True:
print("---------------B-----------")
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
# 切换到gr1中运行
gr1.switch()
gevent
- 能够自动切换任务的模块
gevent
,保证总有greenlet一直在运行
gevent: 安装
pip3 install gevent
gevent:简单实现
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(),i)
g1 = gevent.spawn(f,5)
g2 = gevent.spawn(f,5)
g3 = gevent.spawn(f,5)
g1.join()
g2.join()
g3.join()
Monkey给程序打补丁
from gevent import monkey
import gevent
import random
import time
# 有耗时操作时,需要将程序用到的耗时操作代码,换成gevent中自己实现的模块
monkey.patch_all()
def coroutine_work(coroutime_name):
for i in range(10):
print(coroutime_name,i)
time.sleep(random.random())
gevent.joinall([
gevent.spawn(coroutine_work,"work1"),
gevent.spawn(coroutine_work,"work2")
])
线程和线程
- 在实现多任务时,线程切换非常消耗性能,需要保存很多数据,而协程的切换只需要操作CPU的上下文,速度快
迭代
- 迭代是一个可以记住遍历位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完,才会结束
可迭代对象
- 使用for...in... 这类语句迭代读取一条数据,并可以使用的对象称之为可迭代对象(lterble)。可以使用
isinstance()
判断一个对象是否是可迭代对象
可迭代对象的本质
- 迭代过程:使用
for...in...
或者其他循环来执行迭代,进行所有的数据获取,一般数据都是连续的
- 帮助进行迭代的工具,称之为迭代器。迭代器可以帮助遍历所有数据
- 可迭代对象通过
__iter__
方法提供一个迭代器。在迭代对象时,实际上是获取该对象的迭代器,然后通过这个迭代器来依次获取对象中的每个数据
- 可以通过
iter()
函数获取这些可迭代对象的迭代器,然后可以对获取到的迭代器不断使用next()
函数来获取下一条数据。当迭代完成后,再调用next()函数会抛出Stoplteration的异常,表示所有数据已迭代完成,不再执行next()函数
for ...in...循环的本质
-
for item in lterable
循环的本质就是通过iter()函数获取可迭代对象的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值,并将其赋值给item,当遇到Stoplteration的异常后循环结束
生成器
生成器:简介
- 生成器是一类特殊的迭代器。在实现一个迭代器的时候,当前迭代器的状态需要自己记录,才能根据当前状态生成下一个数据。在def中有yield函数被称为生成器
- 将原本在迭代器
__next__
方法中实现的基本逻辑放在一个函数中实现,但是将每次迭代返回数值的return换成yield,此时新定义的函数便不再是函数,而是一个生成器
创建生成器
A = [x*2 for in range(5)] # 列表生成式
B = (x*2 for in range(5)) #
next(B) # 使用
生成器实现斐波那切数列
def fib(n):
current = 0
num1,num2 = 0 , 1
while current < n:
num = num1
num1,num2 = num2,num1+num2
current += 1
yield num
return 'done'
使用生成器
for n in fib(5):
print(n)
捕获生成器错误
- 使用for循环调用generator时,发现拿不到generator的return语句的返回值。返回值包含在Stoplteration的value中
g = fib(5)
while True:
try:
x = next(g)
print("value:%d"%x)
except StopIteration as Stop:
print("生成器返回值:%s"%Stop.value)
break
send唤醒
- 除了使用next()函数进行生成器唤醒继续执行外,还可以通过send()函数来唤醒执行。
- 使用send()函数的好处是,可以在唤醒时同时向断点处传入一个附加数据
使用send
def gen():
i = 0
while i<5:
temp = yield i
print(temp)
i += 1
f =gen()
f.send("附加数据")
进程、线程、协程对比
- 进程是资源分配单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很大,并且效率低
- 线程切换需要的资源一般,并且效率一般
- 协程切换任务资源很小,效率高
- 多进程、多线程,是根据CPU核心数一样,可能是并行,但是协程是在一个线程中,所以是并发
网友评论