老男孩14期自动化运维day10随笔和作业

1.IO(磁盘,网络等)操作不占用CPU
计算占用CPU,例如1+1

多线程使用场景:python多线程不适合CPU密集操作型的任务,适合IO密集型的任务(例如socket server )

2.进程
每一个进程都是由默认父进程启动的(每一个子进程都是由主进程启动的)
比如在pycharm启动程序 ,在windows上是pycharm为父进程:主进程的父进程为pycharm
比如在linux终端启动程序,在linux上是terminal为父进程,主进程的父进程为terminal
两个方法:os.getpid() 获取进程号
os.getppid() 获取父进程号

创建进程与线程类似

from multiprocessing import Process

def func(a):
  pass
p=Process(target=func,args=(a,))
p.start()

3.多进程(multiprocess)
多进程间的通信:不同进程之间是不允许访问对方内存的,多进程要实现通信,只能通过以下方式
----进程Queue
----pipe 管道

以下为后两种方式详细解释:
(1)进程通过进程Queue进行通信
与线程的queue不一样 只能用于进程通信的特殊的queue叫进程Queue

from multiprocessing import Process,Queue

def f(q):
    q.put([42,None,'1'])

if __name__=='__main__':
    q=Queue()
    p=Process(target=f,args=(q,))
    p.start()
    print(q.get())

上述过原理为: 在主进程通过进程QUEUE读到了子进程往q里存的数据
解析过程:

其实是两个queue,主进程开启queue,把queue作为参数传入子进程中,是复制了另一个queue给子进程(pickle序列化
然后 子进程往queue传数据,再pickle反序列化给主进程的queue
只是实现了这个进程的数据传给另一个进程

(2)PIPE管道

from multiprocessing import Process, Pipe


def f(conn):
    conn.send([42, None, 'hello from child'])
    conn.send([42, None, 'hello from child3'])
    print("",conn.recv())
    conn.close()


if __name__ == '__main__':
    parent_conn, child_conn = Pipe() # 生成管道有两头,返回两个值
    p = Process(target=f, args=(child_conn,))
    p.start()
    print("parent",parent_conn.recv())  # prints "[42, None, 'hello']"
    print("parent",parent_conn.recv())  # prints "[42, None, 'hello']"
    parent_conn.send(" from hshs")  # prints "[42, None, 'hello']"
    p.join()

就是生成一个Pipe管道对象,管道有两头,返回两个值,在这里两头没有准确定义必须是传数据的还是收数据的

(2)进程间的数据共享:
本来进程之间内存不能互相访问,但是可以通过manager实现进程间的数据共享
----通过manager

from multiprocessing import Process, Manager
import os
# 进程间的共享数据
# 其实也是copy了十个数据 最后汇总,而且内部加了锁 ,不用人为加锁


def f(d, l):
    d[os.getpid()]=os.getpid()
    l.append(1)
    print(l,d)


if __name__ == '__main__':
    with Manager() as manager: # 和 manger=Manager() 一样
        d = manager.dict() # 生成一个可在多个进程之间共享、传递的字典

        l = manager.list(range(5)) # 生成一个可在多个进程之间共享、传递的list

        p_list = []
        for i in range(10):
            p = Process(target=f, args=(d, l))
            p.start()
            p_list.append(p)
        for res in p_list: # 等待结果
            res.join()
        l.append("from parent")
        print(d)
        print(l)

原理:创建manager对象,通过manager.dict()生成一个可以再多进程间共享、传递的字典对象,通过manager.list() 生成一个可以在多进程间共享、传递的列表对象

其实就是copy了十个数据,最后汇总,而且内部加了锁,所以这里不用加锁

4.进程锁
本身进程之间内存不共享,为什么还需要进程锁?

原因:主要是保证在屏幕上打印的时候不乱

from multiprocessing import Process, Lock

# 进程锁
# 本身进程之间内存不共享,为什么还需要进程锁?
# 原因:主要是保证在屏幕上打印的时候不乱

def f(l, i):
    #l.acquire()
    print('hello world', i)
    #l.release()


if __name__ == '__main__':
    lock = Lock()

    for num in range(100):
        Process(target=f, args=(lock, num)).start()

5.进程池

进程启动开销比线程启动大很多,所以python有进程池 概念防止电脑崩溃。线程池可以通过信号量自己定义

在windows上启动多进程跟linux不一样 ,要import freeze_support或者加if name==‘main

from multiprocessing import Process,Pool,freeze_support

# 在windows上启动多进程跟linux不一样 ,要import freeze_support或者加if __name__=='__main__'

import time
import os
def Foo(i):
    time.sleep(2)
    print("in process ",os.getpid())
    return  i+100

def Bar(arg):
    print("--->exec done:",arg)

if __name__=='__main__':
    #freeze_support()
    pool=Pool(5) # 和 pool=Pool(process=5)一样  意思为 允许进程池同时放入5个进程,同时运行的进程只有五个

    for i in range(10):# 启动了 但是被放进进程池的进程才会运行
        pool.apply_async(func=Foo,args=(1,),callback=Bar)  # callback=回调  执行完Foo 再执行Bar ,注意:回调是主进程调用的而不是子进程,例如备份数据库时候只用父进程创建连接,子进程去回调父进程的连接 而不用多次创建连接
        # pool.apply(func=Foo,args=(i,)) # 串行
        # pool.apply_async(func=Foo,args=(1,)) # 并行
    print('end')
    pool.close()
    pool.join() # 等所有进程结束 不写join 主进程不会等子进程结束

# 注意python的要求(官方文档都没写),先close再join(死记硬背),如果把join注释了,不等子进程执行完毕程序就关闭了

过程:通过Pool()创建一个pool对象。

pool=Pool(5) 与 pool=Pool(process=5) 表示允许同时放入5个进程,同时运行的进程只有五个,跟多线程的信号量一样。

在for循环里启动进程,但这是还没有启动,只有放入进程池的进程才能运行。

pool.apply(func=Foo,args=(1,)) 这是串行运行进程
pool.apply_async(func=Foo,args=(1,)) 这是并行运行进程(异步)
pool.app;y_async(func=Foo,args=(1,),callback=Bar)
这里callback是回调函数,执行完Foo,再执行Bar,注意:回调是主进程的调用而不是子进程的调用,例如备份数据库时候只用父进程创建连接,子进程去回调父进程的连接
而不用多次创建连接

最后很重要的一点:python的官方要求文档都没写,必须先close再join(死记硬背),如果把join注释了,不等子进程执行完毕程序就关闭了。(而不像多线程时,先join再close)

pool.close()
pool.join()

另外:这里再将一下_ _name _ _的的意思:

如果再程序里加入 if name== 'main’则是手动执行的时候会执行
如果把该程序当做模块被另外程序import后,另外的程序不执行 if name== 'main’里的内容
____ main__代表主进程的__name_ 子进程为__mp_main__的__name__
所以 if name=='main’是判断是否__name__为主进程
(就是当前该程序手动执行自己,被其他模块导入则不执行里面的内容)

6.协程
(微线程)是一种用户态的轻量级线程
协程为什么很快:协程是遇到IO操作就切换,所以剩下都是CPU操作就很快
线程的切换是保存在cpu的寄存器里,而协程拥有自己的寄存器上下文和栈。
可以在单线程下实现并发效果(实际上还是串行 因为切换时间很快,所以在用户视角下是并行)

优点:
1.无需线程上下文切换的开销
2.无需原子操作的锁定及同步的开销(改变量可以叫原子操作)
3.方便切换控制流,简化编程模型
4.高并发、高扩展、低成本:一个cpu支持上万的协程都不是问题

缺点:
1.因为是单线程,无法利用多核资源,它不能同时将单个CPU的多个核用上(不能多核),协程
需要和进程配合才能运行在多CPU上,除非是CPU密集型应用
2.进行阻塞(Blocking)操作(如IO)会阻塞掉整个程序

注意:当一个函数含有yield关键字时 ,第一次调用它是变成一个生成器,必须加__next__()才执行

两种切换方式
(1)手动切换 greenlet


from greenlet import greenlet
# 手动切换  gevent 封装了greenlet
def test1():
    print(12)
    gr2.switch() # 切换gr2
    print(34)
    gr2.switch()
def test2():
    print(56)
    gr1.switch() # 切换gr1
    print(78)

gr1 = greenlet(test1) #启动一个协程
gr2 = greenlet(test2)
gr1.switch()

(2)自动切换 gevent

gevent自动IO切换 封装了greenlet (手动IO切换),可以通过greenlet实现并发同步或异步编程

import gevent
# 自动IO切换


def foo():
    print('Running in foo')
    gevent.sleep(2)
    print('Explicit context switch to foo again')
def bar():
    print('Explicit精确的 context内容 to bar')
    gevent.sleep(1)
    print('Implicit context switch back to bar')
def func3():
    print("running func3 ")
    gevent.sleep(0)
    print("running func3  again ")


gevent.joinall([
    gevent.spawn(foo), # 启动协程
    gevent.spawn(bar),
    gevent.spawn(func3),
])

代码中 sleep只是gevent模拟io消耗的时间代指类似于io的消耗的时间

gevent.spawn(协程) 启动协程,遇到IO操作自动切换

运行过程为:
foo 先打印第一句 然后遇到io 切换给bar打印第一句 然后遇到io 切换给func3 打印第一句 然后遇到io
又给foo 发现foo还在等io,就给bar bar 也在等io ,又给func3 io结束后 打印第二句,返回给foo 还在等io给bar,bar 等io结束打印第二句 发给foo,foo等待io结束 打印foo第二句
这个单线程的异步执行,只要2s ,但是不使用协程要3s,要2s是指单线程中io等待时间最多的点

通过gevent自动切换协程能实现什么?

(1)很牛逼的实现:gevent实现大并发单线程socket server(通过协程)

from gevent import socket, monkey

monkey.patch_all()


# 通过gevent自动切换协程实现单线程下的socket并发(很牛逼!!!)
# 大并发socket server(单线程)


def server(port):
    s = socket.socket()
    s.bind(('0.0.0.0', port))
    s.listen(500)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)


def handle_request(conn):
    try:
        while True:
            data = conn.recv(1024)
            print("recv:", data)
            conn.send(data)
            if not data:
                conn.shutdown(socket.SHUT_WR)

    except Exception as  ex:
        print(ex)
    finally:
        conn.close()


if __name__ == '__main__':
    server(9999)

(2)gevent实现简单大并发单线程爬虫

import gevent,time
from urllib import request
from gevent import monkey

#简单协程大并发爬网页


monkey.patch_all() # 把当前程序的所有io操作给我单独做上标记

def f(url):
    print('GET:%s'%url)
    resp=request.urlopen(url)
    data=resp.read()
    print('%d bytes received from %s .'%(len(data),url))


urls=['https://www.python.org/',
      'https://www.yahoo.com/',
      'https://github.com/'
]
time_start=time.time()
for url in urls:
    f(url)
print("同步cost:",time.time()-time_start)


async_time_start=time.time()

gevent.joinall([
    gevent.spawn(f,'https://www.python.org/'),
    gevent.spawn(f,'https://www.yahoo.com/'),
    gevent.spawn(f,'https://github.com/'),

])
print("异步cost:",time.time()-async_time_start)

(没加monkey.path.all()前提下)时间一样是因为gevent 跟urllib没关系 ,gevent不知道urllib在做io ,所以就没有切换,可以通过加入 monkey补丁

monkey.patch_all() # 把当前程序的所有io操作给我单独做上标记,标记他是IO操作,遇到他就切换

7.论事件驱动与异步IO

通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新进程,来处理该请求
(2)每收到一个请求,创建一个新线程,来处理该请求
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO方式来处理请求(协程)

io是操作系统执行的(就是利用事件驱动模型把io操作扔到操作系统中一个队列,io执行完后调用回调函数告知你执行完的标记)
上面的几种方式,各有千秋:
第(1)种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单
第(2)种方法,由于要涉及到线程的同步,有可能面临死锁等问题
第(3)种方法,在写应用程序代码时,逻辑比前面两种都复杂
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式

事件驱动模型:

1.有一个事件(消息)队列
2.例如鼠标按下时 ,往这个队列中增加一个点击事件(消息)
3.有个循环,不断从队列中取出事件,根据不同的时间,调用不同的函数,如onClick()、onKeyDown()等
4.事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数

在这里插入图片描述

事件驱动编程是一种编程范式,这里程序的执行流由外部事件决定

8.IO多路复用

进程的阻塞
只有处于运行态的进程,才有可能转为阻塞状态。当进程进入阻塞状态时,不会占用CPU资源

文件描述符 fd:
就是一组非负整数,是操作系统内部文件记录表(有序的,存放的是句柄对象)的索引值,操作系统拿到文件描述 符,从文件记录表中找到文件句柄对象,从对象中操作数据。在unix,linux上才有文件描述符的概念

缓存IO:
又被称作标准I/O,大多数文件系统默认IO都是缓存IO。在linux缓存IO机制中,数据会先拷贝到操作系统内核的缓存 区中,然后从操作系统内核再拷贝到应用程序的地址空间(比如socket中,两次send会发送在一起(黏包),是因为系统为了减少操作系统内核拷贝
到应用程序的开销。)(内核态—》用户态的数据转换) 缓存IO缺点:这些数据拷贝对cpu以及内存的开销是非常大的

9.IO五种网络模式(有一种驱动IO很少用)

情景:用户有个read操作

1-3都是同步IO(synchronous IO 必须等内核态到用户态的转变)
(1)阻塞IO(blocking iO)

在linux ,默认情况下所有的socket都是阻塞IO (blocking IO) 用户发送read操作到内核
内核中没有数据,在等待数据被发送过来,此时用户进程在等待,当内核中有数据后,再返回给用户。 用户在等待的时候就是阻塞I/O

(2)非阻塞IO(nonblocjing io)

linux下可以通过设置socket为nonblocking
用户发送read操作到内核,内核中没有数据,用户不用等内核是或否有数据,内核没有数据会发送一个error到用户,用户收到
内核的信息做判断,当为error的时候可以去做其他事,收到数据之后再处理数据 所以 nonblocking
IO的特点是用户进程徐不断的主动询问kernel数据好了没有,可以实现用户视角下的单线程多并发
但是在内核态到用户态 如果数据过大 还是会阻塞

(3)I/O多路复用(IO multiplexing或者 event driven IO 事件驱动IO)

常用的select poll epoll 是建立在非租塞IO的情况下,因为非阻塞IO情况下,在等待
接受数据的时候没有阻塞,但是在拷贝数据的时候,如果从内核拷贝到用户的数据太大,则会阻塞,这是IO多路复用要解决的问题。

三种方式 select poll epoll:
select (windows,linux) 例如多个连接 循环这些连接(例如有一百个链接,就循环这个一百个,有一个返回数据就返回给用户),任意一个返回就返回信号(缺点,文件描述符上限1024,当然可以自行修改,如果要循环连接(数组轮询)过多,容易浪费资源)
poll 没有最大文件描述符限制(基于select优化 但还是有select的缺点)

epoll (最流行的,windows不支持,linux2.6内核.Django就是用的这个,例如nginx)

(1)epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

(2)epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件

比如

epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入

epoll_ctl(epollfd, EPOLL_CTL_DEL, socket,
EPOLLOUT);//注册缓冲区非满事件,即流可以被写入

(3)epoll_wait(epollfd,…)等待直到注册的事件发生

(注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。

4.异步IO(asynchronous IO,不用等内核态到用户态)—用得少(其实很多叫异步IO都用的是IO多路复用 epoll)

发起一个read操作,立刻返回,所以不会对用户进程产生任何block。然后kernel会等待数据准备完成,然后拷贝到用户内存,
当这一切完成之后,kenel会给用户进程发送一个signal,告诉他read操作已完成。

注意 :这里拷贝完成才会给用户进程发送一个signal,所以用户进程是不会阻塞的,用户进程只是把任务丢给内核,可以去做其他事,当他收到内核发送的signal时就知道数据已经从内核态到用户态了。

以上这四种网络模式图解:
在这里插入图片描述

一个单线程下通过select方式实现IO多路复用的socket server 例子:

import select
import socket
import queue

# 单线程下的io 多路复用的selcet实现socket_server


server=socket.socket()
server.bind(('localhost',9000))
server.listen(1000)

# 设置为非阻塞模式

server.setblocking(False) # 不阻塞



inputs=[server]
outputs=[]



while True:
  readable,writeable,exceptional=select.select(inputs,outputs,inputs)

  print(readable,writeable,exceptional)
  for r in readable:
    if r is server: # 如果是server 代表来了一个新连接
      conn,addr=server.accept()
      print("来了个新连接",addr)
      inputs.append(conn)   # 是应为这个新建立的连接还没发数据过来,现在就接受的话程序就要报错
         # 所以要想实现这个客户端发数据来时,server端能知道,就需要让select再监测这个conn
    else:  # 如果是之前的conn 表示发数据了
      data=r.recv(1024)
      print("收到数据",data)
      r.send(data)
      print("send done..")

server.setblocking(False) 设为非阻塞模式

两个列表 inputs 和 outputs
select 去循环去inputs列表里的对象

在inputs列表里的对象首先必须要是server本身,其次是conn连接实例。

如果是server 代表来了一个新连接
然后 inputs.append(conn) 是应为这个新建立的连接还没发数据过来,现在就接受的话程序就要报错 ,所以要想实现这个客户端发数据来时,server端能知道,就需要让select再监测这个conn
如果是conn 表示这个连接开始发数据了

猜你喜欢

转载自blog.csdn.net/qq_33060225/article/details/84560714