美文网首页
12-Python之路-进阶-多任务

12-Python之路-进阶-多任务

作者: 程序记录日志 | 来源:发表于2022-05-01 22:12 被阅读0次

多任务:线程

多任务:简介

  • 操作系统可以同时运行多个任务。操作系统轮流让各个任务交替执行,实现的多任务效果
  • 并发:任务数多余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获取进程号
  • os.getpid()获取进程号

进程间通信

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核心数一样,可能是并行,但是协程是在一个线程中,所以是并发

相关文章

网友评论

      本文标题:12-Python之路-进阶-多任务

      本文链接:https://www.haomeiwen.com/subject/snzpyrtx.html