• 多任务 : 操作系统可以同时运行多个任务
  • 并发 : 任务数 > 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核数不一样可能是并行的也可能是并发的。协程的本质就是使用当前进程, 在不同的函数代码中切换执行,可以理解为并行。协程是一个用户层面的概念,不同协程的模型实现可能是单线程也可能是多线程。