进程与线程的概念,协程(生产者与消费者模型)

并发编程里包括了  进程  与 线程  、协程、I/O多路复用,如下图:

image

 

What?

一、何为并发??

一个CPU  在执行叫“并发”   如图:

image

并发 : concurrency   1、single Processor

                                     2、logically  simultaneous processor

举个粟子:

假设一台电脑只有一核(也就是一个CPU) ,这时开了 world QQ  音乐  ,这从我们的感知上是在并行的,事实不是。

我一边写文档,写着写着又去聊天, 还听着音乐,这时CPU的执行是,你写着文档在运行着,然后发现你点到QQ聊天去了,这时将WORLD文档当前执行状态保存着,然后执行你的QQ聊天,聊着聊着又去听音乐,  这时又把QQ程序的执行状态保存着,又去执行听音乐,因为CPU的切换速度在0.0X毫秒间,你没有感知到,这种状态叫 “并发”,如 我们经常所听到的 高并发  ,是一个CPU在执行着多个(进程)。

###执行状态保存的切换: PCB. 进程控制块 Process control  block

####程序:    数据集

####运行:    CPU运行

 

二、何为并行??

多个CPU在执行叫“并行”:如图:

image

parallelism:  1 、multiprocess ,multicore

                      2 physically  simultaneous processing

每个CPU 在执行着每个进程的时候,叫 “并行”,  这种情况很少,毕竟 需要多个物理CPU 执行

1、还有一种情况 :操作系统会切换  ,出现IO操作的时候, 当某一个程序在读写的时候,这个时候是不是占用CPU的,那这时候就是并行

2、固定时间切换: 多个程序(进程)都没有I/O操作时候,就是都运行着。

 image

进程与线程的概念:

1、进程是一个包含多个执行的 容器

2、线程是进程里的最小执行单位

从整体看,并发编程里有进程、线程、协程、IO多路复用,应用程序要运行在操作系统上,就是开了一个进程,而一个进程里有多个线程在运行,

运行就是CPU在运行程序。

image

 

 

进程定义:

由程序、数据集、进程控制块。

程序:读取内在数据二进制

数据集:内存数据二进制

进程控制块:保存当前读取内存二进制,切换到另一个数据集--又读取程序(读取内存二进制)--进程控制块又保存读取数据集二进制

 

线程定义:

最小执行单位

 

 

How?

开线程的方法:

import threading  #导入threading 模块
import time   
def watch_tv():             #子进程
    print("watch tv ",)
    time.sleep(3)
    print('watch tv done!!!')
def listen_music():       #子进程
    print('listen music ')
    time.sleep(5)
    print('listen music done')

watch_obj=threading.Thread(target=watch_tv)  #线程对象
listen_obj=threading.Thread(target=listen_music) #线程对象

print('master process done!')    #  主线程  最外层运行   
watch_obj.start()          
listen_obj.start()

image 

join()    子线程执行完后,再执行主线程

obj.join()

1 join方法的作用是阻塞主进程无法执行join以后的语句,专注执行多线程,必须等待多线程执行完毕之后才能执行主线程的语句。
2 多线程多join的情况下,依次执行各线程的join方法,前一个结束之后,才能执行后一个。
3 无参数,则等待到该线程结束,才开始执行下一个线程的join。
4 设置参数后,则等待该线程N秒之后不管该线程是否结束,就开始执行后面的主进程。

 

setDaemon:  守护线程:主线程结束后,不管子线程

obj.setDaemon(True)

obj跟着线程结束

import threading,time     
def watch_tv():      
    print("watch tv ",)
    time.sleep(3)
    print('watch tv done!!!')
def listen_music():
    print('listen music ')
    time.sleep(5)
    print('listen music done',time.time())
number='cctv5'
watch_obj=threading.Thread(target=watch_tv)
listen_obj=threading.Thread(target=listen_music)

watch_obj.setDaemon(True)       

watch_obj.start()       
watch_obj.join()        #等于accept   ,  等待watch_obj执行完  
listen_obj.start()    
print('master process done!')

 

 

class 自定义:

class Mythread(threading.Thread):
    def __init__(self,number):
        threading.Thread.__init__(self)
        self.number=number
    def run(self):
        print('Mythread %s'%self.number,)
        time.sleep(3)
my_thread=Mythread(12)
my_thread1=Mythread(34)
my_thread.start()
my_thread1.start()
my_thread1.join()

print('master thread')

 

 

 

 

GIL (全局解释器锁)

 

进程锁,每个进程在能出一个线程,在python中   进程  的处理有不好的地方 ,所以要完成多线程的处理只能靠协程解决 。

第二个方法就是开多个进程, 但是进程多了有弊端:开销大、切换复杂

 

在进程里 GIL规定了每个进程里有多个线程,但只允许每次出去一个线程运算,(至于这个线程如何出去的,是线程们自己竞称。)

GIL 是PYTHON创始人加的,我们在用的时候没法修改

image

 

但是可以锁住用户程序的进程。让这个进程执行完以后再执行其它的进程,加个用户线程锁:  线程安全

同步锁:

threading.acquire

image

同步锁:

import time
import threading
#同步锁
def subNum():
    global num  #在每个线程中都获取这个全局变量
    print('ok')
    lock.acquire()
    time.sleep(1)
    temp=num
    num=temp-1  #对此公共变量进行-1操作
    lock.release()

num=100  #设定一个共享变量
thread_list=[]
lock=threading.Lock()
for i in range(100):
    t=threading.Thread(target=subNum)
    t.start()
    thread_list.append(t)

for t in thread_list:   #等待所有线程执行完毕
    t.join()
print('Result:',num)

结果:

image

 

死锁   与   Rlock()  递归锁

Lock_d1=threading.Lock()
Lock_d2=threading.Lock()
class Mythread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        self.fun1()   
        self.fun2()
    def fun1(self):
        Lock_d1.acquire()
        print('I am Lock_d1')
        Lock_d2.acquire()
        print('I am Lock_d2')
        Lock_d2.release()
        Lock_d1.release()
    def fun2(self):
        Lock_d2.acquire()
        print('I am Lock_d22 ')
         time.sleep(3)
        Lock_d1.acquire()
        print('I am Lock_d11')
        Lock_d1.release()
        Lock_d2.release()
if __name__ == '__main__':
    print('------------game start %s'%time.time())
    for i in range(0,2):
        my_thread_d=Mythread()
        my_thread_d.start()

image

因为两个线程 抢占锁, 都没有抢占到。现在所有的操作系统都拥有 “抢占式调度”  官方称(preemptive multitasking ) 抢先式多任务能力

 

递归锁:

Lock_d1=threading.RLock()
# Lock_d2=threading.RLock()
class Mythread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        self.fun1()
        self.fun2()
    def fun1(self):
        Lock_d1.acquire()
        print('I am Lock_d1')
        # time.sleep(0.5)
        Lock_d1.acquire()
        print('I am Lock_d2')
        Lock_d1.release()
        Lock_d1.release()
    def fun2(self):
        Lock_d1.acquire()
        print('I am Lock_d22 ')
        # time.sleep(0.2)
        Lock_d1.acquire()
        print('I am Lock_d11')
        Lock_d1.release()
        Lock_d1.release()
if __name__ == '__main__':
    print('------------game start %s'%time.time())
    for i in range(0,2):
        my_thread_d=Mythread()
        my_thread_d.start()

 

Event对象:

event.wait()  :默认为False  
event.set()   : 设置 为True
event.clear  :恢复event的状态值为False

image

 

Semaphore(信号量)

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

 

Why?

什么情况用:

总结:

对于计算密集型任务:python的多线程并没有用

对于IO密集型任务: python的多线程是有意义的

Python一定要使用多核;  必须得开进程   进程弊端:开销大、切换复杂

所以在开发程序的时候要着重 加强学习 协程  + 多进程

还有一个就是:IO 多路复用

 

协程实现并发:

image

一个是 之前学习过的yield 实现,yield是当前状态保存,挂起来,在PYTHON中有相应的模块实现,最常用的地方是在IO操作的时候切换,

1、由于是单线程,不能再切换

2、不再有任何锁的概念

 

想用协程必须借助其它 的模块实现,greenle 是协和的基础库,它下面有gevent  eventlet (对于eventlet 需要时做了解)
switch()就是在有IO的情况下切换线程,

wps3089.tmp

还有一个就是joinall,它里面是一个列表

Sleep 模拟IO,在IOCPU就会切换处理

Joinall()  它里面是一个列表,

第一个处理的时候遇到 IO 切换到另一个函数,

wps3D57.tmp

 

IO模型:

前面的知识, 我们无法监测到IO操作。 但是IO模型可以实现。IO多路复用是IO模型的一种,

有五种:

1、阻塞 IO

2、非阻塞 IO

3、IO多路复用 #有自己的监听多个连接    #重点

4、异步IO

5、驱动信号

 

操作系统内核空间,用户空间

#在发数据的时候,应用程序在compute1上,先从自己的操作系统中的用户空间转换成内核空间-->网卡   经过网络  发送到另一台电脑的  网卡--> 到对方的操作系统的内核空间,内核空间转换成用户空间(也就是应用程序(用户态)),你就能看到这条信息了

wps73E2.tmp

 

1、阻塞IO   非阻塞  IO多路复用 就是同步

有阻塞就是 同步

Sock.accept()  发送系统调用  用户态切换内核态。这时内核空间一直在等消息过来,进程一直在等等待完成,直到完成再做其它的事情

wps32AF.tmp

代码查看: accept就已经在阻塞着

wps5FD7.tmp

 

 

2、非阻塞IO  

有没有数据都回来,从内核空间读取数据,不管有没有都返回一个信息给用户空间,然后你看到这条消息。

wpsD102.tmp

优点: 不用等数据过来,wait for data 时无阻塞

缺点:多次发系统调用(由accept recv),消耗资源  ,数据不是即时接收的

两个阶段中:wait for data   非阻塞

          Copy data    是阻塞的状态

wps41AF.tmp

 

3、IO多路复用

Select 只是多路复用中一种。

Select 阻塞  将accept拆成两步 第一步在wait for data,  前面都是accept 在wait for data, sock.accept()   sock是一个套接字对象

里面可以监听多个socket 对象(也就是多个文件描述符)

 

多路复用图:

wps225F.tmp

如下:

wpsE2EE.tmp

Sock  永远只是一个sock(套接字对象永远是服务器的),因为客户端

sock.connect((ip,port)) ,服务端每次拿到的还是自己的socket对象, 每次进来的都是 客户端的conn,所以变化的就是conn

特点:1、全程(wait for data,cpoy) 阻塞

       2、能监听多个文件描述符

1、套接字对象 是一个非零整数,不会变

2、收发数据的时候,对于接收端而言,数据先到内核空间,然后COPY到用户空间,同时内核空间的数据清掉

3、对于服务端而言由于TCP三次握手四次挥手,客户端不回,服务端不清除内核数据。

image

 

4、异步IO

全程无阻塞

 

总结:

 

image

 

 

五个IO模型比较:

image

 

 

前面学到select

现在讲selector 模块,它是select 模块实现的IO多路复用,推荐使用selector

Windows下有select 模块:

Linux: 下有select    poll    epoll    #select 是效率最低的一种

 

select 缺点

wpsA867.tmp

1、每次调用select ,select都要将所有fd(文件描述符),拷贝到内核空间,导致效率下降。

<socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000)>

2、遍历所有的fd是否有数据访问:(最重要的问题  遍历所有)

3、并且select 还有最大连接数(1024)

Poll : 最大连接数没有限制

epoll: select 和poll 都只有一个函数实现 

1、epoll第一个函数:创建epoll句柄,也是将所有fd(文件描述符),拷贝到内核空间,但是只需拷贝一次。

2、第二个函数:回调函数::: 某一个事件(函数、动作)成功后,会触发的函数,为所有的fd绑定一个回调函数,一旦有数据访问触发该  回调函数,回调函数将fd 放到链表

3、第三个函数::判断链表是否为空

示例:

#!/usr/bin/env python
#!-*- coding:utf-8 -*-

import selectors
#基于select 模块实现的IO多路复用,推荐使用这个
import selectors
import socket
sock=socket.socket()
sock.bind(('127.0.0.1',9000))
sock.listen(5)
sock.setblocking(False)
sel=selectors.DefaultSelector() #根据具体平台选择最佳IO多路,比如在linux上,会为我们epoll

def read(conn,mask):
    try:
        data=conn.recv(1024)
        print(data.decode('utf-8'))
    except Exception:
        sel.unregister(conn)
    send_data=input('>>>: ')
    conn.send(send_data.encode('utf-8'))
def accept(sock,mask):
    conn,addr=sock.accept()
    sel.register(conn,selectors.EVENT_READ,read)

sel.register(sock,selectors.EVENT_READ,accept) #注册谁,就是监听谁(真正监听的是select)
#sock 有变化,accept函数会运行。就不用像写select 那样麻烦了。accept 是触发函数
while True:
    print('wating..')
    events=sel.select()  #第一个是返回的sock对象,第二个是mask #[(key,mask),(key,mask)]
    for key,mask in events:
        print(key.data)     #2  conn   #1  accept  :<function accept at 0x0000000000917F28>
        print(key.fileobj)  #sock:    <socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000)>
        func=key.data   #
        obj=key.fileobj  #
        func(obj,mask)   #ACCEPT(sock,mask)  #2 read(conn,mask)

 

队列:queue   #是一种数据类型,默认是先进先出的数据类型

image

使用方法:

put()

get()

join 和 task_done配合使用

wps154E.tmp

 

示例:

import queue
# q=queue.Queue(3)#q就是队列对象 ,先进先出  3管道最大值
# q.put(111)
# q.put('hello')
# q.put(222)
# q.put(123)#到第4值已经put 不进去了,就会报错
#
#
# # ret=q.get()
# print(q.get())
# print(q.get())
# print(q.get())
# print(q.get(False))


#join 和task_done
q=queue.Queue(5)
q.put(111)
q.put(222)
while not q.empty(): #判断不为空的时候值取完
    print(q.get())
# print(q.get())
# q.task_done()
# print(q.get())  #这个时候已经取完值,仍会卡住 因为没有用task_done
# q.task_done()

# q.join()
print('ending')

 

Queue模式:先进后出:

q=queue.LifoQueue() #
q.put(123)
q.put(432)
print(q.get())

 

优先级:

wps5BA2.tmp

生产者消费者模型:

生产者  生产太多的消息   消费者 一处理一条,消息过剩

生产者消费者模型 解决的是耦合问题

这个会涉及到消息中间件 ribtmq  与对比着 现在学习的queue 学习

线程进程是操作系统的内容。

 

 

回顾:

进程:最小的资源管理单位(盛放线程的容器)

线程:最小的执行单位

串行,并行,并发

串行:执行完一个,才执行第二个

并行:要有多个CPU,每个CPU执行一个进程,称为并行,只能通过进程实现,但是cpython有GIL锁,锁住了每个进程,每个进程只能出一个线程被执行。

(同一时刻同一进程只能有一个线程执行)

并发:一个CPU ,执行多个线程,线程之间的切换,与串行的区别就是:有切换,

 

 

 

Python 线程库:threading

wps415F.tmp

 

现在有三个线程并发的向下执行。

wps6600.tmp

自定义:

wpsA5DE.tmp

线程对象的方法:

wpsCA9E.tmp

Join  主线程与子线程的关系。  主线程等子线程。

setDaemon  守护线程,守护A,等B结束

程序直到不存在非守护线程时退出

其它方法:

wps43A5.tmp

 

同步锁:

由于多线程处理公共资源

wps9C6F.tmp

 

互斥锁:只允许一次一个线程执行完才能执行下一个

Smaphore   允许多个线程并发执行。连接量,一同只允许20个连接。

协程:

协程:又称微线程、纤程。 释义:相互配合工作的一个过程;

∆子程序,或者称为函数 ,例如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后A执行完毕。

∆协程是在执行过程中-----子程序内部中断,转而执行另一个函数(子程序),另一个子程序执行完返回,接着执行前一个中断的程序

∆通常用到的是生产者和消费者模型,

ø协程的优势:优势就是协程的执行效率,因为子程序切换不是线程切换,而是由程序自身控制 ,因此,没有线程切换的开销,和多线程比,线程数量越多,

协程的性能优势就越明显。

ø协程的优势二:没有多线程锁机制,因为只有一个线程,在协程中控制 共享资源不加锁只需要判断状态就好,所以执行效率比多线程高很多

∆因为协程是一个线程执行,那怎么利用多核CPU?  方法:多进程+协程,即充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

python中对协程的支持是通过generator实现的。

在generator中,不但可以通过for循环来迭代,还可以不断调用next() 函数获取由yield语句返回的下一值。

python 的yield 不但可以返回一个值,它还可以接收调用者发出的参数 。

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

如果改用协和,生产者生产消息后(send),直接通过yield 跳转到消费者开始 执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

import time
#生产者就是吃饭拉屎,消费者就是吃生产者的屎后回应
def consumer2():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s----------' % n)
        r = '吃完了'

def consumer1():
    r = ''
    while True:
        time.sleep(1)
        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()

def produce1(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % (''))  
        r = c.send('')
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()
c1 = consumer1()
c2=consumer2()
produce1(c2)
produce(c1)

结果:

 如图:

 

 

这次消费者不吃屎:

import time
#生产者就是吃饭拉屎,消费者就是吃生产者的屎后回应
def consumer2():
    r = ''
    while True:
        n = yield r   #吃到的饭
        if not n:
            return
        print('[CONSUMER] Consuming %s----------' % n) #打印吃到的饭
        r = '200ok'   #回应

def produce1(c):
    c.send(None)  #初始化 。。说:准备好”开始吃饭了
    n = 0
    while n < 5:  #5个菜可以吃
        n = n + 1
        print('[PRODUCER] Producing %s...' % n) #吃饭
        r = c.send(n) #给消费者吃同样的饭
        print('[PRODUCER] Consumer return: %s' % r) #消费者吃完饭的回应
    c.close() #吃完收工

c2=consumer2() 
produce1(c2) #给哪个消费生产消息 

 

总结:∆1、生产者 send(None) 启动 生成器。

   ∆2、send(n) 切换到consumer执行

     ∆3、consumer 通过yield 接收到生产者的消息--> 处理后,又通过yield 把结果传回
   ∆4、produce 拿到consumer处理的结果,继续生产下一条消息;

   ∆5、produce决定不生产了,通过c.close()关闭生产,不生产也就是关闭了消费,整个程序结束。

再总: 整个程序没有锁,在一个线程内由produce和consumer协作完成任务,所以称为‘协程’,而不是线程的抢占式多任务。

 

posted @ 2017-05-08 17:03  tonycloud  阅读(853)  评论(0)    收藏  举报