我的秋招--“进程&线程&协程&IO多路复用&异步”

2020-12-17突然字节又打电话约面试,两个月没看了,来突击一下,关于这些内容,刚巧看到了一篇博客,我觉得看下面的内容先看看这篇博客,还是挺好的,当个引子

引子

博客
以下内容,为这篇博客的整理。
Python当中为我们提供了完善的threading库,通过它,我们可以非常方便地创建线程来执行多线程。
首先,我们引入threading中的Thread,这是一个线程的类,我们可以通过创建一个线程的实例来执行多线程。

from threading import Thread
t = Thread(target=func, name='therad', args=(x, y))
t.start()

简单解释一下它的用法,我们传入了三个参数,分别是target,name和args,从名字上我们就可以猜测出它们的含义。首先是target,它传入的是一个方法,也就是我们希望多线程执行的方法。name是我们为这个新创建的线程起的名字,这个参数可以省略,如果省略的话,系统会为它起一个系统名。当我们执行Python的时候启动的线程名叫MainThread,通过线程的名字我们可以做区分。args是会传递给target这个函数的参数。

一个经典的例子

import time, threading


def loop(n):
    print('thread %s is running...' % threading.current_thread().name)
    for i in range(n):
        print('thread %s >>> %d' % (threading.current_thread().name, i))
        time.sleep(2)
    print('thread %s is ended...' % threading.current_thread().name)


print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='loopThread', args=(10,))
t.start()
print('thread %s is ended...' % threading.current_thread().name)

运行结果:

thread MainThread is running...
thread loopThread is running...thread MainThread is ended...

thread loopThread >>> 0
thread loopThread >>> 1
thread loopThread >>> 2
thread loopThread >>> 3
thread loopThread >>> 4
thread loopThread >>> 5
thread loopThread >>> 6
thread loopThread >>> 7
thread loopThread >>> 8
thread loopThread >>> 9
thread loopThread is ended...

表面上看这个结果没毛病,但是其实有一个问题,什么问题呢?输出的顺序不太对,为什么没打完数字,主线程就结束了呢?另外一个问题是,既然主线程已经结束了,为什么Python进程没有结束, 还在向外打印结果呢?

因为线程之间是独立的,对于主线程而言,它在执行了t.start()之后,并不会停留,而是会一直往下执行一直到结束。如果我们不希望主线程在这个时候结束,而是阻塞等待子线程运行结束之后再继续运行,我们可以在代码当中加上t.join()这一行来实现这点。

t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

join操作可以让主线程在join处挂起等待,直到子线程执行结束之后,再继续往下执行。我们加上了join之后的运行结果是这样的:

thread MainThread is running...
thread loopThread is running...
thread loopThread >>> 0
thread loopThread >>> 1
thread loopThread >>> 2
thread loopThread >>> 3
thread loopThread >>> 4
thread loopThread >>> 5
thread loopThread >>> 6
thread loopThread >>> 7
thread loopThread >>> 8
thread loopThread >>> 9
thread loopThread is ended...
thread MainThread is ended...

Process finished with exit code 0

我们再来看第二个问题,为什么主线程结束的时候,子线程还在继续运行,Python进程没有退出呢?这是因为默认情况下我们创建的都是用户级线程对于进程而言,会等待所有用户级线程执行结束之后才退出。这里就有了一个问题,那假如我们创建了一个线程尝试从一个接口当中获取数据,由于接口一直没有返回,当前进程岂不是会永远等待下去?

这显然是不合理的,所以为了解决这个问题,我们可以把创建出来的线程设置成守护线程。
守护线程
守护线程即daemon线程,它的英文直译其实是后台驻留程序,所以我们也可以理解成后台线程,这样更方便理解。daemon线程和用户线程级别不同,进程不会主动等待daemon线程的执行,当所有用户级线程执行结束之后即会退出。进程退出时会kill掉所有守护线程。
我们传入daemon=True参数来将创建出来的线程设置成后台线程:

t = threading.Thread(target=loop, name='LoopThread', args=(10, ), daemon=True)

2020-09-26 夜 宿舍 下了一天的雨 起床整理一下博客

同步、异步

描述的是任务的提交方式
同步:任务提交之后,原地等待任务的返回结果,等待的过程中不做任何事

程序层面上表现出来的感觉就是卡住了

异步:任务提交之后,不原地等待任务的返回结果,直接去做其他的事情。

问题:提交的任务结果如何获取?
任务的返回结果会有一个异步回调机制自动处理

阻塞、非阻塞

描述的是程序的运行状态
阻塞:阻塞态
非阻塞:就绪态、运行态
理想状态:让程序永远处于就绪态和运行态之间切换

上述概念的组合:最高效的一种组合就是异步非阻塞

进程

开启进程的两种方式

# -*- coding:utf-8 -*-
from multiprocessing import Process
import time


def run_proc():
    """子进程要执行的代码"""
    while True:
        print("----2----")
        time.sleep(1)


if __name__=='__main__':
    p = Process(target=run_proc)
    p.start()
    while True:
        print("----1----")
        time.sleep(1)

# 第二种方式


class MyProcess(Process):
    def run(self):
        while True:
            print('--2--')
            time.sleep(1)


if __name__ == '__main__':
    p = MyProcess()
    p.start()
    while True:
        print('---1---')
        time.sleep(1)

总结
创建进程就是在内存中申请一块内存空间将需要运行的代码丢进去
一个进程对应在内存中就是 一块独立的内存空间
多个进程对应在内存中就是多块独立的内存空间
进程之间的数据默认情况下是无法直接进行交互的

join方法

就是让主进程等待子进程结束再往后执行

from multiprocessing import Process
import time


def run_proc(name):
    """子进程要执行的代码"""
    print("%s is running" % name)
    time.sleep(3)
    print("%s is over" % name)


if __name__=='__main__':
    p = Process(target=run_proc, args=('jason',))
    p.start()
    # time.sleep(10000000000)
    p.join() # 主进程等待子进程结束之后再继续往后执行
    print('主')

进程的执行顺序是不固定的

def run_proc(name):
    """子进程要执行的代码"""
    print("%s is running" % name)
    time.sleep(2)
    print("%s is over" % name)


if __name__=='__main__':
    p1 = Process(target=run_proc, args=('jason',))
    p2 = Process(target=run_proc, args=('egon',))
    p3 = Process(target=run_proc, args=('tank',))
    p1.start()
    p2.start()
    p3.start()
    print('主')
"""
仅仅是告诉操作系统要创建进程 
"""
主
egon is running
jason is running
tank is running
egon is over
jason is over
tank is over

仅仅是告诉操作系统要创建进程

进程间数据隔离

from multiprocessing import Process
import os
import time

nums = [11, 22]

def work1():
    """子进程要执行的代码"""
    print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))
    for i in range(3):
        nums.append(i)
        time.sleep(1)
        print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))

def work2():
    """子进程要执行的代码"""
    print("in process2 pid=%d ,nums=%s" % (os.getpid(), nums))

if __name__ == '__main__':
    p1 = Process(target=work1)
    p1.start()
    p1.join()

    p2 = Process(target=work2)
    p2.start()

in process1 pid=11349 ,nums=[11, 22]
in process1 pid=11349 ,nums=[11, 22, 0]
in process1 pid=11349 ,nums=[11, 22, 0, 1]
in process1 pid=11349 ,nums=[11, 22, 0, 1, 2]
in process2 pid=11350 ,nums=[11, 22]

进程对象其它方法

"""
Linux:ps aux | grep PID
WIN:  tasklist | findstr PID
"""

os.getpid()、os.getppid()

from multiprocessing import  Process,current_process
import time
import os

def task():
    # print('%s is running' % current_process().pid)
    print('%s is running' % os.getpid())
    print('%s is running' % os.getppid()) # 子进程的父进程的pid
    time.sleep(3)

if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    print('主',current_process().pid)
    print('主主',os.getppid())9160
主主 8224
14908 is running
9160 is running

p.terminate()、p.is_alive()

from multiprocessing import  Process,current_process
import time
import os

def task():
    # print('%s is running' % current_process().pid)
    print('%s is running' % os.getpid())
    print('%s is running' % os.getppid()) # 子进程的父进程的pid
    time.sleep(3)


if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    p.terminate() #: 杀死当前进程	告诉操作系统杀死进程,但不一定立即杀死。操作系统需要时间,但代码执行的速度非常快
    time.sleep(0.1)
    print(p.is_alive()) # :判断当前进程是否存活

    print('主',current_process().pid)
    print('主主',os.getppid())
    print(p.is_alive())
False6676
主主 8224
False

僵尸进程和孤儿进程

1. 僵尸进程与孤儿进程的定义

我们知道一个子进程如果要结束,其内核释放进程的所有资源,但是还保存了一部分资源供父进程使用(是一个被称之为僵尸进程的数据结构,包含有进程号、运行时间、退出状态等,它需要父进程去处理),所以其父进程要调用wait或者waitpid获取子进程的状态信息,然而如果父进程没有调用这个信息会发生什么情况呢?

  • 如果父进程没有调用这个信息,那么这段信息将会被一直占用,但是系统中的资源是有限的,如果大量资源被占用,此时将无法产生新的信息。

2.僵尸进程

一个进程如果创建出子进程,如果此时子进程退出,而父进程没有进行善后工作(wait与waitpid获取子进程状态信息),那么此时子进程的进程描述符仍然保存在系统中。

3.孤儿进程

如果一个父进程退出,它的子进程(有一个或者多个)还在,那么子进程将成为孤儿进程,这个时候这些孤儿进程会被1号进程,也就是init进程所收养,并且由init进程完成善后工作(状态收集)。

4.解决方法

由于孤儿进程在最后都会被一个一号进程也就是init进程所收养,而init进程会对这个孤儿进程进行善后工作,所以一般情况下我们不需要处理孤儿进程。

但是僵尸进程如果不处理会占用大量资源,所以我们必须要对僵尸进程进行处理,一般的处理方式我们有如下两种:

(1)用信号进行处理

信号处理是一般的做法,一般子进程退出的时候向父进程发送SIGCHILD信号,所以我们只需要捕捉这个信号,在信号处理函数中进行wait(),对其子进程进行善后工作,将不会产生僵尸进程。

(2)fork()两次进行处理

思路:
如果父进程处理时间长,子进程处理时间短,如果父进程不wait()处理的话,子进程就会产生僵尸进程,但是如果父进程进行了wait()处理,那么父进程又会产生阻塞,所以解决方法就是让自己尽快退出,任务让子进程的子进程来处理。
方法:
fork()两次的做法是在unix环境高级编程一书中所提到的,其方法是在fork第一次进程的里面再用一次fork,然后让第一次fork出来的子进程变为孤儿进程,交给init进程进行处理,自己则用waitpid善后即可。

守护进程

from multiprocessing import Process
import time

def task(name):
    print('%s 总管 正在活着' % name)
    time.sleep(3)
    print('%s 总管 死了' % name)

if __name__ == '__main__':
    p = Process(target=task,args=('egon',))
    p.start()
    print('君王jason死了')
#君王jason死了
#egon 总管 正在活着
#egon 总管 死了

创建守护进程的效果

from multiprocessing import Process
import time

def task(name):
    print('%s 总管 正在活着' % name)
    time.sleep(3)
    print('%s 总管 死了' % name)

if __name__ == '__main__':
    p = Process(target=task,args=('egon',))
    p.daemon = True # 这句话必须在start上方
    p.start()
    print('君王jason死了')
# 君王jason死了

互斥锁

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定

from multiprocessing import Process,Lock
import json
import time
import random

# 1.查票
def search(i):
    # 读取文件
    with open('data','r',encoding='utf8') as f:
         dic = json.load(f)
    print('用户%s查询余票:%s' % (i, dic.get('ticket_num')))
    # 字典取值不要用[]的形式,推荐使用get

# 买票 1.先查2.再买

def buy(i):
    with open('data','r',encoding='utf8') as f:
        dic = json.load(f)
    # 模拟网络延迟
    time.sleep(random.randint(1,3))
    #判断当前是否有票
    if dic.get('ticket_num') > 0:
        dic['ticket_num'] -= 1
        with open('data', 'w', encoding='utf8') as f:
            json.dump(dic,f)
        print('用户%s买票成功' % i)
    else:
        print('买票失败')

def run(i, mutex):
    search(i)
    # 给买票环节加锁
    mutex.acquire()
    buy(i)
    mutex.release()

if __name__ == '__main__':
    mutex = Lock()
    for i in range(1,11):
        p = Process(target=run, args=(i, mutex))
        p.start()

进程间通信

from multiprocessing import Queue
# 创建一个队列
q = Queue() # 括号内可以传数字,表示生成的队列最大可以同时存放的数据量

q.put(111)
q.put(222)
v1 = q.get()
v2 = q.get()
v3 = q.get()
print(v1)
print(v2)
print(v3)
from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    while True:
        if not q.empty():
            value = q.get(True)
            print('Get %s from queue.' % value)
            time.sleep(random.random())
        else:
            break

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 等待pw结束:
    pw.join()
    # 启动子进程pr,读取:
    pr.start()
    pr.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    print('')
    print('所有数据都写入并且读完')

生产者消费者模型

"""
生产者:生产制造东西的
消费者:消费处理东西的
媒介:生产者和消费者之间不是直接交互的,而是借助媒介
"""
from multiprocessing import Process,Queue
import random
import time

def producer(name,food,q):
    for i in range(5):
        data = '%s 生产了%s%s' % (name, food, i)

        time.sleep(random.randint(1,3))
        print(data)
        q.put(data)
def consumer(name,q):
    while True:
        food = q.get()
        if food is None:break
        time.sleep(random.randint(1,3))
        print('%s吃了%s' % (name,food))


if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('zhl','包子',q))
    p2 = Process(target=producer, args=('小张','油条',q))
    c1 = Process(target=consumer,args=('z',q))
    p1.start()
    p2.start()
    c1.start()

    p1.join()
    p2.join()
    # 等待生产者生产完毕之后,往队列添加结束表示
    q.put(None)  # 放几个None  取决于消费者有几个

线程

为什么要有线程

开设进程:
1.申请内存空间, 耗资源
2.“拷贝代码” 耗资源
开线程
一个进程内可以开设多个线程,在用一个进程内开设多个线程无序再次申请内存空间及拷贝代码 的操作。
总结:开设线程的开销要远远小于进程的开销。 同一个进程下的线程资源共享。

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

一个线程可以创建和撤销另一个线程; 同一个进程中的多个线程之间可以并发执行.

进程在执行过程中拥有独立的内存单元,而该进程的多个线程共享内存,从而极大地提高了程序的运行效率。

每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

进程简说:

进程就是程序的一次执行。

进程是为了在CPU上实现多道编程而发明的一个概念。

事实上我们说线程是进程里面的一个执行上下文,或者执行序列,显然一个进程可以同时拥有多个执行序列,更加详细的描述是,舞台上有多个演员同时出场,而这些演员和舞台就构成了一出戏,类比进程和线程,每个演员是一个线程,舞台是地址空间,这个同一个地址空间里面的所有线程就构成了进程。

比如当我们打开一个word程序,其实已经同时开启了多个线程,这些线程一个负责显示,一个接受输入,一个定时进行存盘,这些线程一起运转让我们感到我们的输入和屏幕显示同时发生,而不用键入一些字符等好长时间才能显示到屏幕上。

线程管理:

将线程共有的信息存放在进程控制块中,将线程独有的信息存放在线程控制块中。

那么如何区分哪些信息是共享的?哪些信息是独享的呢?

一般的评价标准是:如果某些资源不独享会导致线程运行错误,则该资源就由每个线程独享,而其他资源都由进程里面的所有线程共享。

创建线程的方法

from threading import Thread
import time


def run_proc(name):
    """子进程要执行的代码"""
    print("%s is running" % name)
    time.sleep(1)
    print("%s is over" % name)


if __name__=='__main__':
    t = Thread(target=run_proc,args=('jason',))
    # p1 = Process(target=run_proc, args=('jason',))
    t.start()
    # p1.start()
    print('主')

    # 创建进程的话要拷贝代码 开销很大。但是线程不需要。但是代码执行是很快的。没有io的话,会很快往下走
#
# """第二种方法"""
class MyThead(Thread):
    def __init__(self,name):
        # 重写了别人的方法又不知道别人的方法里有啥  你就调用父类的方法
        super().__init__()
        self.name = name

    def run(self):
        print('%s is running' % self.name)
        time.sleep(1)
        print('egon DSB')

mythread = MyThead('egon')
mythread.start()

TCP服务端实现并发效果

服务端

from threading import Thread
import time
import socket

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

def talk(coon):
    while True:
        try:
            data = coon.recv(1024)
            if len(data) == 0:break
            print(data.decode('utf-8'))
            coon.send(data.upper())
        except ConnectionResetError as e:
            print(e)
    coon.close()

while True:
    coon, addr = server.accept()# 接客
    t = Thread(target=talk, args=(coon,))
    t.start()

客户端

import socket

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

client.connect(('127.0.0.1',8080))

while True:
    client.send(b'hello world')
    data = client.recv(1024)
    print(data.decode('utf-8'))

守护线程

主线程运行结束后,不会立刻结束,会等待所有其他非守护线程结束后才会结束

from threading import Thread
import time

def task(name):
    print('%s is running' % name)
    time.sleep(1)
    print('%s is over' % name)

if __name__ == '__main__':
    t = Thread(target=task, args=('egon',))
    t.daemon = True
    t.start()
    # t.join()
    print('主')
def task1():
    time.sleep(1)
    for i in range(5):
        print('this is task1--->%s' % i)
        time.sleep(0.5)

def task2():
    time.sleep(1)
    for i in range(5):
        print('*****%s*****' % i)
        time.sleep(0.5)
if __name__ == '__main__':
    t1 = Thread(target=task1)
    t2 = Thread(target=task2)

    t1.daemon = True
    t2.daemon = True

    t1.start()
    t2.start()

    print('主')

# 主

线程互斥锁

GIL

  • GIL不是python的特点,而是Cpyton解释器的特点

  • 在Cpython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行,同一个进程的多个线程无法利用多核优势。
    因为:cython中的内存管理(垃圾回收机制)不是线程安全的。

  • GIL是保证解释器级别的数据安全

代码不能被cpu直接执行,需要借助python解释器,每一个进程都有一个解释器。如果同一个进程的线程能够同时执行,那么这四个线程都要去执行,线程1刚要申请a,结果被垃圾回收线程删掉了。
线程锁和GIL的区别
我的秋招--“进程&线程&协程&IO多路复用&异步”_第1张图片
先看这个代码:
此时100个线程结束后,输出为99,因为每一个都遇到了IO,释放GIL,最终都是100-1。

from threading import Thread,Lock
import time

mutex = Lock()
money = 100

def task():
    global money
    # mutex.acquire()
    tmp = money
    time.sleep(0.1) #  模拟网络延迟  是肯定存在的
    money = tmp - 1
    # mutex.release()


if __name__ == '__main__':

    t_list = list()
    for i in range(100):
        t = Thread(target=task,)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(money)

# 99
# 如果没有那个time的话,输出为0

== 因此为了保证money从100变成0,就要自己加一把锁==。
GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图
我的秋招--“进程&线程&协程&IO多路复用&异步”_第2张图片

验证多进程多线程

一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。

如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,

反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高

结论:

对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用

当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

def work():
    time.sleep(2)
if __name__ == '__main__':
    l = []
    print(os.cpu_count())
    start_time = time.time()
    print(start_time)
    for i in range(40):
        p = Process(target=work)  # 29.739346742630005
        # t = Thread(target=work)   # 2.0272865295410156
        p.start()
        # t.start()
        l.append(p)
        # l.append(t)
    for p in l:
        p.join()
    # for t in l:
    #     t.join()

    print(time.time()-start_time)

#分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程

#单核情况下,分析结果: 
  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

#多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

 
#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

死锁

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁。

抢锁必须要释放锁,否则会产生死锁现象

from threading import Thread,Lock,RLock
import time

mutexA = Lock()

mutexB = Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        mutexB.release()
        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(10):  # 起10个线程 然后自动执行run方法
        t = MyThread()
        t.start()
"""
线程1先抢到A锁,然后再去抢B锁,没有竞争者所以线程1抢到B锁,
这个时候其他线程还在抢A锁,然后线程1释放B锁,再释放A锁。
然后在func2里面,线程1抢到B锁,其他线程这个时候在抢A锁。
线程1抢到B锁,再睡两秒,线程2抢完A锁,要抢B锁。
这个时候线程1和2就卡住了。
"""
# 解决方法 递归锁

递归锁

"""
递归锁的特点:
	可以被连续的acquire和release
	但是只能被第一个抢到这把锁的执行上述操作
	它的内部有一个计数器  每acquire一次计数加1  每release一次计数减1
	只要计数不为0 那么其他人就无法抢到该锁
"""
mutexA = mutexB = RLock() # 同一把锁
class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        mutexB.release()
        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t = MyThread()
        t.start()

信号量

信号量在不同的阶段可能对应不同的技术点

在并发编程中信号量指的是锁!!!

"""
如果我们将互斥锁比喻成一个厕所的话
那么信号量就相当于多个厕所
"""
from threading import Thread,Semaphore
import time
import random

sm = Semaphore(5) # 括号内写几   就开设几个坑位

def task(name):
    sm.acquire()
    print('%s 正在蹲坑'% name)
    time.sleep(random.randint(1,5))
    sm.release()

if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=task,args=(i,))
        t.start()

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

Event事件

from threading import Thread,Event

import time
import random

event = Event() # 造了一个红绿灯

def light():
    print('红灯亮')
    time.sleep(3)
    print('绿灯亮')
    event.set()

def car(name):
    print('%s车正在等红灯'% name)
    event.wait() # 等待别人给你发消息,只要有人出发event.set() 这句话就会往下走

    print('%s 车加油门走了' % name)


if __name__ == '__main__':
    t = Thread(target=light)
    t.start()

    for i in range(20):
        t = Thread(target=car, args=('%s'%i,) )
        t.start()

进程池和线程池

先看看之前是如何实现tcp服务端并发效果

import socket
from threading import Thread


def communication(new_socket):
    while True:
        try:
            data = new_socket.recv(1024)
            if len(data) == 0: break
            new_socket.send(data.upper())

        except ConnectionResetError as e:
            print(e)
            break
    new_socket.close()

def server(ip,port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip, port))

    server.listen(1024)
    while True:
        new_socket,new_addr = server.accept()

        # 开设多进程或者多线程处理客户端通信
        t = Thread(target=communication, args=(new_socket,))
        t.start()

if __name__ == '__main__':
    s = Thread(target=server, args=('',7890))
    s.start()

每来一个人就开设一个进程或者线程去处理。

"""
无论时开设进程还是开设线程也好  都要消耗资源
只不过开设线程的消耗比开设进程的稍微小一点而已

我们是无论如何不可能做到无限制的开设进程和线程的,因为计算机硬件的资源跟不上

我们的宗旨时在保证计算机硬件能够正常工作的情况下最大程度利用它
""""""
池是用来保证计算机硬件安全的情况下最大限度的利用计算机
他降低了程序的运行效率,但是保证了计算机硬件的安全
"""

基本使用

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,os

pool = ProcessPoolExecutor()# ThreadPoolExecutor(5) # 池子里面固定只有5个线程
"""
池子造出来之后, 里面会固定存在几个线程\进程

这几个线程\进程不会出现重复创建和销毁的过程
"""

def task(n):
    print(n,os.getpid())
    time.sleep(2)
    return ('aaa')


"""
任务的提交方式:
    同步:
    提交任务之后原地等待任务的返回结果  期间不做任何事情
    异步:
    提交任务之后不等待任务的返回结果 执行往后进行
"""
if __name__ == '__main__':

    t_list= list()
    for i in range(20):
        res = pool.submit(task, i) # 朝池子中提交任务   异步
        # print(res.result())  # 变成了同步提交,所以做下面的措施
        t_list.append(res)
    """
    程序由并发编程了串行
    res.result()拿到的是异步提交的任务的返回结果
    """
    pool.shutdown()  # 关闭线程池  等待线程池中所有任务运行完毕

    for t in t_list:
        print('>>>',t.result())
0 60856
1 59880
2 61256
3 58340
4 59880
5 60856
6 61256
7 58340
8 60856
9 59880
10 61256
11 58340
12 59880
13 60856
14 61256
15 58340
16 60856
17 59880
18 61256
19 58340
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa
>>> aaa

实现join的效果

 pool.shutdown()  # 关闭线程池  等待线程池中所有任务运行完毕

回调机制

相当于给每个异步任务绑定了一个定时炸弹,一旦该任务有结果,立刻触发爆炸
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,os

pool = ProcessPoolExecutor()# ThreadPoolExecutor(5) # 池子里面固定只有5个线程
"""
池子造出来之后, 里面会固定存在几个线程\进程

这几个线程\进程不会出现重复创建和销毁的过程
"""

def task(n):
    print(n,os.getpid())
    time.sleep(2)
    return ('n', n*2)

def call_back(n):
    print(n.result())

"""
任务的提交方式:
    同步:
    提交任务之后原地等待任务的返回结果  期间不做任何事情
    异步:
    提交任务之后不等待任务的返回结果 执行往后进行
    返回结果如何获取???
    异步提交的返回结果  应该通过回调机制来获取
         
"""
if __name__ == '__main__':

    t_list= list()
    for i in range(20):
        res = pool.submit(task, i).add_done_callback(call_back) # 朝池子中提交任务   异步
        # print(res.result())  # 变成了同步提交,所以做下面的措施
    #     t_list.append(res)
    # """
    # 程序由并发编程了串行
    # res.result()拿到的是异步提交的任务的返回结果
    # """
    # pool.shutdown()  # 关闭线程池  等待线程池中所有任务运行完毕
    #
    # for t in t_list:
    #     print('>>>',t.result())
0 60608
1 61616
2 32000
3 47428
4 61616
('n', 0)
5 60608
('n', 2)
6 32000('n', 4)

7 47428
('n', 6)
8 60608
('n', 10)
9 61616
('n', 8)
10 32000
('n', 12)
11 47428
('n', 14)
12 60608
13 61616
('n', 18)
('n', 16)
14 32000
('n', 20)
15 47428
('n', 22)
16 60608
17 61616
('n', 26)
('n', 24)
18 32000
('n', 28)
19 47428
('n', 30)
('n', 32)
('n', 34)
('n', 36)
('n', 38)

Process finished with exit code 0

协程

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

  • 无需线程上下文切换的开销。协程就是单线程里的,都是串行操作,不需要锁。
  • 无需原子操作锁定及同步的开销
    • “原子操作(atomic operation)是不需要synchronized”,所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
"""
进程:资源单位
线程:执行单位
协程:这个概念是程序员自己搞的--微线程--用户态的轻量级线程
	单线程下实现并发:程序员在代码层面上检测io操作,一旦遇到io了,我们在代码级别完成切换,这样给cpu的感觉是这个程序一直在····运行,没有io,从而提升程序的运行效率。 

多道技术:切换+保存状态
	切换:
		1.程序遇到io
		2.程序长时间占用

tcp服务器
	accept
	recv
代码如何做到:	切换+保存状态
	切换:切换不一定提升效率 
		io切:提升效率
		计算密集型:降低效率
	保存状态:
		保存上一次执行的状态  下一次来接着上一次的操作集需执行 。yield
"""

yield

import time

def work1():
    while True:
        print("work1---")
        yield 3
        # time.sleep(0.5)
        print("eee")

def work2():
    while True:
        print("work2---")
        yield
        # time.sleep(0.5)

if __name__ == '__main__':
    wk1 = work1()  # 此时按照调用函数的方式使用生成器就不再是执行函数体了,而是会返回一个生成器对象,然后就可以按照使用迭代器的方式来使用生成器了。
    wk2 = work2()
    # a = wk1.__next__()
    # print(a)
    # wk1.__next__()
    # wk1.__next__()
    # print(wk1)
    while True:
        next(wk1)
        next(wk2)

此时按照调用函数的方式使用生成器就不再是执行函数体了,而是会返回一个生成器对象,然后就可以按照使用迭代器的方式来使用生成器了。

greenlet

greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。

from greenlet import greenlet

def eat(name):
    print('%s eat 1' %name)
    g2.switch('egon')
    print('%s eat 2' %name)
    g2.switch()
def play(name):
    print('%s play 1' %name)
    g1.switch()
    print('%s play 2' %name)

g1=greenlet(eat)
g2=greenlet(play)

g1.switch('egon')#可以在第一次switch时传入参数,以后都不需要
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()  # 这就叫手动切换 括号里面传参数

感觉确实用着比generator还简单了呢,但好像还没有解决一个问题,就是遇到IO操作,自动切换,对不对?
问题来了:什么时候切回来

gevent

greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent

其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

1. 用法

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g2=gevent.spawn(func2)
g1.join() #等待g1结束
g2.join() #等待g2结束
#或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value#拿到func1的返回值
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()

运行结果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行

2. gevent切换执行

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
#运行结果
<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
import gevent
def eat(name):
    print('%s eat 1' %name)
    gevent.sleep(2)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)
    gevent.sleep(1)
    print('%s play 2' %name)


g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')

3.给程序打补丁

from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

运行结果

work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9

打补丁

# 有耗时操作时需要
monkey.patch_all()  # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块

运行结果

work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9

同步和异步的区别

import gevent


def task(pid):
    """
    Some non-deterministic task
    """
    gevent.sleep(1)
    print('Task %s done' % pid)


def synchronous():
    for i in range(1, 10):
        task(i)


def asynchronous():
    threads = [gevent.spawn(task, i) for i in range(10)]
    gevent.joinall(threads)


print('Synchronous:')
synchronous()

print('Asynchronous:')
asynchronous()

我的秋招--“进程&线程&协程&IO多路复用&异步”_第3张图片

用协程实现tcp的并发

服务端

from gevent import monkey;monkey.patch_all
import socket
from gevent import spawn

def communication(new_socket):
    while True:
        try:
            data = new_socket.recv(1024)
            print(data)
            if len(data) == 0: break
            new_socket.send(data.upper())

        except ConnectionResetError as e:
            print(e)
            break
    new_socket.close()
    
def server(ip,port):
	server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	server.bind(ip,port)
	server.listen(1024)
	while True:
		new_socket, new_addr = server.accept()
		spawn(communication, new_socket)

if __name__ == '__main__':
    g1 = spawn(server,'127.0.0.1',7890)
    g1.join()

客户端:

from threading import Thread,current_thread
import time
import socket

client = socket.socket()
client.connect(('127.0.0.1',7890))

def x_client():
    n = 0
    while True:
        msg ='%s say hello %s' % (current_thread().name,n)
        client.send(msg.encode('utf-8'))
        n += 1
        data = client.recv(1024)
        print(data.decode('utf-8'))

if __name__ == '__main__':
    for i in range(100):
        t = Thread(target=x_client)
        t.start()

论事件驱动与异步IO

通常,我们写服务器处理模型的程序时,有一下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;

(2)每收到一个请求,创建一个新的线程,来处理该请求;

(3)==每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

上面的几种方式,各有千秋,

第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。

第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。

第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。

综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式。

看图说话讲事件驱动模型

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点
\1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
\2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
\3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的

方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
\1. 有一个事件(消息)队列;
\2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
\3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
\4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
我的秋招--“进程&线程&协程&IO多路复用&异步”_第4张图片
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。

让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。

我的秋招--“进程&线程&协程&IO多路复用&异步”_第5张图片
在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

此处要提出一个问题,就是,上面的事件驱动模型中,只要一遇到IO就注册一个事件,然后主程序就可以继续干其它的事情了,直到io处理完毕后,继续恢复之前中断的任务,这本质上是怎么实现的呢?哈哈,下面我们就来一起揭开这神秘的面纱。。。。

IO模型简介

"""
这里的IO模型是针对网络IO的

    * blocking IO
    * nonblocking IO
    * IO multiplexing
    * signal driven IO
    * asynchronous IO
    由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。

"""
同步异步
阻塞非阻塞
常见的网络阻塞状态:
	accept
    recv
    recvfrom
    
    send虽然有io行为,但是不在考虑范围之内。 

应用程序无法直接操作软硬件,需要将数据交给操作系统。
recv:跟操作系统要数据
被动触发
我的秋招--“进程&线程&协程&IO多路复用&异步”_第6张图片
send
主动触发;不考虑在IO之内。

阻塞IO

我的秋招--“进程&线程&协程&IO多路复用&异步”_第7张图片
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。如下图

​ ps:所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。

一个简单的解决方案:

在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

该方案的问题是:

开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。

改进方案:

很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。

改进后方案其实也存在着问题:

“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

非阻塞IO

我的秋招--“进程&线程&协程&IO多路复用&异步”_第8张图片
虽然非阻塞io能够实现,但是该模型会 长时间占用着cpu不干活,让cpu空转。

实际应用中也不会考虑非阻塞io模型

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

IO多路复用

select:
IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
我的秋招--“进程&线程&协程&IO多路复用&异步”_第9张图片
select:操作系统提供的监管机制,能够帮你监管socket对象和conn对象,并且可以监管多个,只要有人触发了,立刻给你返回可执行的对象。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

import socket
import select

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

read_list = [server]
server.setblocking(False) # 设为非阻塞模式

# res = select.select(read_list, [], []) # 这里会有阻塞,就是select监管的对象没有人连的时候,会阻塞。
# print(res)
# print("aaa")
# ([], [], [])
# 人没来时,一直阻塞,一旦有人来,立刻返回的是被监管的对象

# 应该做一个循环  不停监测
while True:
    r_list, w_list, x_list = select.select(read_list, [], []) # 这里会有阻塞,就是select监管的对象没有人连的时候,会阻塞。

    print(r_list)
    # r_list里面既有server对象,又有conn对象
    for i in r_list:
        """针对不同的对象不同的处理"""
        if i is server:
            conn, addr = i.accept() # 这个时候 这里不会再阻塞,因为前面已经判断过了,走到这里的,都是已经获得了请求的
            # conn也应该添加到监管机制中,因为对方有可能不会立即发消息
            read_list.append(conn)
        else:
            res = i.recv(1024)
            if len(res) == 0:
                i.close()
                # 将无效的监管对象 移除
                read_list.remove(i)

            print(res)
            i.send(b'heihie')
import socket


client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

client.connect(('127.0.0.1',8080))
while True:
    client.send(b'hello world')
    data = client.recv(1024)
    print(data)
"""总结"""

“sekect机制” :Windows和linux都有

“poll机制”  :只在linux有;poll监管的数量更多

上述两个机制其实都不是很完美,当监管的对象特别多的时候,可能出现及其大的延迟相应
----------------------------------------------
“epoll机制” : 只在linux有
他给每一个监管对象都绑定一个回调机制
一旦有响应  回调机制立刻发起提醒
----------------------------------------------
“selectors模块”:根据不同的平台自动选择对应的监管机制

以上的都是同步IO

因为在最终读数据的时候,都要经历从内核空间copy数据到用户进程的过程。这个过程要等着结束才继续进行。

异步IO

我的秋招--“进程&线程&协程&IO多路复用&异步”_第10张图片

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。
然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
"""
异步IO效率最高的,也是使用最广泛的
由于需要和操作系统交互,所以纯python无法实现
但是有封装好的模块
    模块: asyncio模块
    框架: sanic  tronado  twisted
     速度非常快
"""

总结

我的秋招--“进程&线程&协程&IO多路复用&异步”_第11张图片

blocking和non-blocking的区别

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

synchronous IO和asynchronous IO的区别

在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。


2020.10.17
终于把这篇给弄完了

你可能感兴趣的:(我的秋招)