美文网首页
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