python-多任务

  • 多任务 : 操作系统可以同时运行多个任务

  • 并发 : 任务数 > cpu核数, 通过操作系统任务调度算法, 实现用多个任务在同一时间段执行(事实上只有cpu核数个在执行)

  • 并行 : 多核cpu情况下,多个任务的一些任务往往是在同一时间点执行的

    在实际的场景中往往既有并发又有并行的多任务。

一、多线程

线程 :一个进程内部的一条代码执行流程(线程)

​ 代码默认在默认线程上执行

1. threading模块

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用

  • 函数实现多线程

    import threading
    import time
    
    
    def say_sorry():
        print("I am sorry")
        time.sleep(1)
    
    
    if __name__ == '__main__':
        for i in range(5):
            t = threading.Thread(target=say_sorry)
            t.start()
    
  • 程序会等待所有的子线程结束后才结束

  • 线程数量

    threading.enumerate()
    

2. 多线程代码封装

  • 类实现多线程

    import threading
    import time
    
    
    class MyThread(threading.Thread):
        def run(self):
            time.sleep(1)
            print(self.name)
    
    
    if __name__ == '__main__':
        for i in range(5):
            t = MyThread()
            t.start()
    

    执行结果

    Thread-3
    Thread-1
    Thread-2
    Thread-4
    Thread-5
    
  • 线程执行顺序

    从上面可以看出多线程程序的执行顺序是不确定的

3. 多线程-共享全局变量

在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
缺点 : 线程是对全局变量随意修改可能造成多线程之间对全局变量的混乱(即非线程安全)

  • 线程不安全案例

    import threading
    
    g_num = 0
    
    
    def work1(num):
        global g_num
        for i in range(num):
            g_num += 1
        print(g_num)
    
    
    if __name__ == '__main__':
        t1 = threading.Thread(target=work1, name="1T", args=(10000000,))
        t2 = threading.Thread(target=work1, name="2T", args=(10000000,))
        t1.start()
        t2.start()
    
  • 同步

    线程同步:一个操作完成后再进行下一个操作

    同步用互斥锁完成

    程序中的同步相当于生活中的异步, 异步相当于生活中的同步

    # 创建锁
    lock = threading.Lock()
    # 锁定
    lock.acquire()
    # 释放
    lock.release()
    

    上面线程不案例的案例可以这样解决

    lock = threading.Lock()
    
    lock.acquire()
    global g_num
    lock.release
    
  • 锁总结

    • 锁的好处:
      • 确保了某段关键代码只能由一个线程从头到尾完整地执行
    • 锁的坏处:
      • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
      • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
  • 死锁

    A锁等待B锁先释放, B锁等待A锁先释放,将造成死锁

    • 避免死锁的方法
      • 程序设计时尽量避免
      • 添加超时时间

4. 案例:udp多线程聊天室

  • 示例

    import threading
    from socket import *
    def send_msg(udp_socket):
        while True:
            udp_socket.sendto(input("请输入内容:").encode("utf-8"), ("127.0.0.1", 8888))
    def recv_msg(udp_socket):
        while True:
            recv_msg = udp_socket.recvfrom(1024)
            print("服务器返回消息:"+recv_msg[0].decode("utf-8"))
    def main():
        # 1.创建socket
        udp_socket = socket(AF_INET, SOCK_DGRAM)
        udp_socket.bind(("", 9999))
    	# 发送接收数据
        recv_thread = threading.Thread(target=recv_msg, args=(udp_socket,))
        recv_thread.start()
        send_msg(udp_socket)
    if __name__ == '__main__':
        main()
    

二、多进程

1. 进程介绍

程序 : 例如xxx.py这是程序,是一个静态的
**进程 **: 一个程序运行起来后,代码+用到的资源称之为进程,它是操作系统分配资源的基本单元。

  • 就绪态 : 运行的条件都已经慢去,正在等在cpu执行
  • 执行态 : cpu正在执行其功能
  • 等待态 : 等待某些条件满足,例如一个程序sleep了,此时就处于等待态

2. 创建进程-multiprocessing

  • 函数创建子进程

    import time
    from multiprocessing import Process
    
    
    def run_proc(name, age, **kwargs):
        time.sleep(1)
        print(1)
    
    
    if __name__ == '__main__':
        for i in range(2):
            p = Process(target=run_proc, args=("test", 18), kwargs={"m": 20})
            p.start()
    
    

3. Process语法结构如下:

Process([group[,target[,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(进程号)

4. 进程间不共享全局变量

  • 示例

    import time
    from multiprocessing import Process
    
    
    nums = [11, 22]
    
    
    def run_proc():
        nums.append(1)
        time.sleep(3)
        print(nums)
    
    
    if __name__ == '__main__':
        p = Process(target=run_proc)
        p.start()
        p.join()
    
        p2 = Process(target=run_proc)
        p2.start()
    
    

    结果为

    [11, 22, 1]
    [11, 22, 1]
    
    

三、进程、线程对比

进程 :能够完成多任务,比如在一台电脑上能够同时运行多个QQ
线程 :能够完成多任务,比如一个QQ中的多个聊天窗口

  • 定义的不同

    • 进程 :系统进行资源分配和调度的一个独立单位.

    • 线程 :是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位

      线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和
      栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

  • 区别

    • 一个程序至少有一个进程,一个进程至少有一个线程.
    • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
    • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

线线程不能够独立执行,必须依存在进程中
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人

  • 优缺点
    线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反

四、进程间通信

1. Queue

可以使用multiprocessing模块的Queue实现多进程之间的数据传递

Queue本身是一个消息列队程序

  • Queue的方法

    • q=Queue(5) # 5为最大接收消息数量
    • Queue.qsize() : 返回当前队列包含的消息数量;
    • Queue.empty() : 如果队列为空,返回True,反之False;
    • Queue.full() : 如果队列满了,返回True,反之False;
    • Queue.get([block[,timeout]]) : 获取队列中的一条消息,然后将其从列队中移除
      • block默认值为True,无timeout时, 消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止
      • 如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
      • block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
    • Queue.get_nowait() : 相当Queue.get(False);
    • Queue.put(item,[block[,timeout]]) : 将item消息写入队列,block默认值为True;
    • Queue.put_nowait(item) : 相当Queue.put(item,False);
  • Queue示例

    from multiprocessing import Queue
    
    q = Queue(2)
    
    if not q.full():
        q.put_nowait("info")
        
    if not q.empty():
        q.get_nowait()
    
    

2. Queue在进程间的通信

  • 示例

    from multiprocessing import Queue, Process
    
    
    def write(q):
        while True:
            if not q.full():
                q.put_nowait("info")
    
    
    def read(q):
        while True:
            if not q.empty():
                print(q.get_nowait())
    
    
    def main():
        # 1.父进程创建Queue并传给子进程
        q = Queue(3)  # type:Queue
        pw = Process(target=write, args=(q,))
        pr = Process(target=read, args=(q,))
    
        # 2.写入的子进程写入数据
        pw.start()
        pw.join()
    
        # 3.读取的子进程读取数据
        pr.start()
        pr.join()
    
        # 4.结束
        print("over")
    
    
    if __name__ == '__main__':
        main()
    
    

五、进程池Pool

multiprocessing中的Pool方法一次性动态成生多个进程

  • Pool(5)

    • 初始化5个进程
    • 新请求提交到Pool时,若Pool没满,创建新进程执行该请求
    • 若Pool已达最大值,请求等待,直到池中有进程结束,才会用之前的进程来执行新的任务
  • multiprocessing.Pool常用函数解析

    • apply_async(func[,args[,kwds]]) : 使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
    • close() : 关闭Pool,使其不再接受新的任务;
    • terminate() : 不管任务是否完成,立即终止;
    • join() : 主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
  • 示例

    from multiprocessing import Pool
    
    
    def work(msg):
        pass
    
    
    po = Pool(3)
    for i in range(3):
        po.apply_async(work, (i,))
    
    
    po.close()  # 关闭进程池,关闭后po不再接收新的请求
    po.join()  # 等待po中所有子进程执行完成,必须放在close语句之后
    
    
  • 进程池中的Queue

    使用Pool创建进程,使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue()

    from multiprocessing import Manager
    Manager().Queue(3)
    
    

案例:copy功能

  • 示例

    import os
    from multiprocessing import Process, Queue
    
    
    def copy_file(file_path, new_dir_path, q):
        if os.path.isdir(file_path):
            os.mkdir(new_dir_path)
        else:
            content = None
            try:
                with open(file_path, "rb") as fr:
                    content = fr.read()
            except Exception as e:
                print(e)
            try:
                with open(new_dir_path, "wb") as fw:
                    fw.write(content)
            except Exception as e:
                print(e)
        if not q.full():
            q.put_nowait(file_path)
    
    
    file_path_list = []
    dir_path_list = []
    
    
    def get_file_path(path):
        if os.path.isfile(path):
            file_path_list.append(path)
        else:
            dir_path_list.append(path)
            path_list = os.listdir(path)
            for new_path in path_list:
                get_file_path(path + "/" + new_path)
        return file_path_list, dir_path_list
    
    
    def main():
        # 消息对列用于显示进度
        q = Queue(1000)
    
        # 1.获取copy的文件夹
        dir_path = input("请输入要copy的文件夹绝对路径:")
        dir_path_back = dir_path + "[backup]"
    
        # 2.递归取出每个空文件夹及文件路径
        get_file_path(dir_path)
    
        # 3.分别copy文件及空文件夹  到  新的文件夹
        for empty_file_path in dir_path_list:
            new_dir_path = dir_path_back + empty_file_path[dir_path_back.find("["):]
            copy_file(empty_file_path, new_dir_path, q)
    
        for file_path in file_path_list:
            # /home/test[back]  /home/test/t.txt  --> /home/test[back]/t.txt
            new_dir_path = dir_path_back + file_path[dir_path_back.find("["):]
            # copy_file(file_path, new_dir_path, q)
            p = Process(target=copy_file, args=(file_path, new_dir_path, q))
            p.start()
    
        # 4.显示进度
        count = 0
        sum = len(file_path_list) + len(dir_path_list)
        while True:
            if not q.empty():
                q.get_nowait()
                count += 1
            print("进度为%.2f%%" % (count/sum*100))
            if count/sum == 1:
                break
    
    
    if __name__ == '__main__':
        main()
    
    

六、迭代器

1. 可迭代对象

  • 可迭代对象

    可迭代对象(Iterable) :可以通过for…in…这类语句迭代读取一条数据供我们使用的对象

  • 如何判断一个对象是否可以迭代

    isinstance()判断一个对象是否是Iterable对象

    # 可迭代对象有:[], (), {}, "abc"
    isinstance({}, Iterable)  # 判断一个对象是否是可迭代对象
    
    
    
  • 可迭代对象的本质

    • 迭代器:每迭代一次,for…in…都返回下一条数据,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,我们把这个“人”称为迭代器(Iterator)。
    • 可迭代对象的本质 :可以向我们提供一个这样的中间“人”即迭代器帮我们对其进行迭代遍历
    • 可迭代对象的实现 :通过 __iter__方法向我们提供一个迭代器,那么也就是说,一个具备__iter__ 方法的对象,就是一个可迭代对象。
  • 可迭代对象的实现

    from collections import Iterable
    
    
    class MyList(object):
        def __init__(self):
            self.container = []
    
        def add(self, item):
            self.container.append(item)
    
        def __iter__(self):
            pass
    
    
    my_list = MyList()
    print(isinstance(my_list, Iterable))  # True
    
    
    

2. 迭代器

一个实现了 __iter__方法和__next__ 方法的对象,就是迭代器。

迭代器 : 帮助记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。

实际上 ,在使用next()函数的时候,调用的就是迭代器对象的 __next__ 方法(Python3中是对象的 __next__方法,Python2中是对象的next()方法)

构造一个迭代器 , 就要实现它的__next__方法。python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现 __iter__方法,,而 __iter__ 方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的 __iter__方法返回自身即可。

  • iter()

    iter()函数可获取可迭代对象的迭代器

    from collections import Iterator
    
    print(isinstance([], Iterator))  # False
    print(isinstance(iter([]), Iterator))  # True
    print(isinstance(iter("abc"), Iterator))  # True
    
    
    
  • next()

    对获取到的迭代器不断使用next()获取下一条数据

    list_iter = iter([1, 2])
    next(list_iter)
    # 当next取不到数据时,抛出StopIteration异常
    
    
    

3. for … in … 本质

iter() --获取–> 可迭代对象Iterable的迭代器 --> next()获取值赋给item --> 异常StopIteration结束循环

4. 应用

迭代器通过next()函数的调用来返回下一个数据值 --> 不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间

  • 示例

    from collections import Iterable
    
    
    class FibIterator(object):
        def __init__(self, n):
            self.n = n
            self.current = 0
            self.num1 = 0
            self.num2 = 1
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.current < self.n:
                self.num1, self.num2 = self.num2, self.num1+self.num2
                self.current += 1
                return self.num1
            else:
                raise StopIteration
    
    
    fib = FibIterator(10)
    for num in fib:
        print(num, end=" ")
    
    
    

七、生成器

1. 生成器介绍

生成器是一类特殊的迭代器

利用迭代器可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成.

但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据

为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)

2. 创建生成器

  • [] --> () : 把一个列表生成式的[]改成()

    G = (x*2 for x in range(5))
    
    
    

    对于生成器G, 可以按照迭代器的使用方法来使用,即可以通过next()函数、for循环、list()等方法使用。

    next(G)
    for x in G:
        print(x)
    
    
    
  • 方法二:yield

    只要在def中有yield关键字的就称为生成器

    def fib(n):
        current = 0
        num1, num2 = 0, 1
        while current < n:
            num = num1
            num1, num2 = num2, num1 + num2
            current += 1
            yield num  # yield使fib函数成为一个生成器而不再是一个函数
        return "done"
    
    
    F = fib(5)
    print(next(F))
    for f in F:
        print(f)
    
    
    

    获取生成器中return的值

    返回值包含在StopIteration的value中

    F = fib(5)
    while True:
        try:
            x = next(F)
        except StopIteration as e:
            print(e.value)
            break
    
    
    

3. 使用send方法

  • send唤醒生成器

    ...
    temp = yield num
    ...
    
    next(F)  # next(F) 等价于 F.send(None)
    F.send("python")  # 此时的temp就为"python"
    # 正确的解释是next和send函数在执行完yield后暂停执行(断点),即将生成器函数挂起。并将yield后的值返回
    # 使用send时在断点处传入一个“附加数据”,temp用来接收这个附加数据
    
    
    

八、协程

1. 协程介绍

  • 协程,又称微线程,纤程

  • 它是一个自带CPU上下文的执行单元

  • 例如: 可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行并且切换次数及时机由开发者确定

  • 协程和线程差异
    线程切换从系统层面远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。所以线程的切换非常耗性能。

    但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

2. 协程的实现

  • 协程的简单实现

    def consumer():
        r = ''
        while True:
            n = yield r
            if not n:
                return
            print('[CONSUMER] Consuming %s...' % n)
            r = '200 OK'
    
    def produce(c):
        c.send(None)
        n = 0
        while n < 5:
            n = n + 1
            print('[PRODUCER] Producing %s...' % n)
            r = c.send(n)
            print('[PRODUCER] Consumer return: %s' % r)
        c.close()
    
    c = consumer()
    produce(c)
    
    
    

    用yield实现协程

    传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁

    协程生产消费模型生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高

  • gevent

    其原理是当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
    由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

    pip3 install gevent
    
    
    
    import gevent
    
    
    def f(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
            gevent.sleep(1)
    
    
    g1 = gevent.spawn(f, 2)
    g2 = gevent.spawn(f, 2)
    g1.join()
    g2.join()
    # 运行结果为
    <Greenlet at 0x7fc4eca68448: f(5)> 0
    <Greenlet at 0x7fc4eca68648: f(5)> 0
    <Greenlet at 0x7fc4eca68448: f(5)> 1
    <Greenlet at 0x7fc4eca68648: f(5)> 1
    
    
    

3. 给程序打补丁

  • 示例

    将程序中用到的耗时操作的代码,换为gevent中自己实现的模块

    import random
    import time
    from gevent import monkey
    import gevent
    
    monkey.patch_all()  # 猴子补丁
    
    
    def work(work_name):
        for i in range(2):
            time.sleep(random.random())
            print(work_name)
    
    
    gevent.joinall([
        gevent.spawn(work, "work1"),
        gevent.spawn(work, "work2")
    ])
    
    
    

4. 进程、线程、协程总结

  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源很最大,效率很低
  4. 线程切换需要的资源一般,效率一般
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据cpu核数不一样可能是并行的也可能是并发的。协程的本质就是使用当前进程, 在不同的函数代码中切换执行,可以理解为并行。协程是一个用户层面的概念,不同协程的模型实现可能是单线程也可能是多线程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值