进程、线程、协程

进程

1、什么是进程(process)?

  定义:1)进程是资源分配最小单位

     2)当一个可执行程序被系统执行(分配内存资源)就变成了一个进程

      1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程

      2. 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念

      3. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。 

      4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

      5. 进程之间有自己独立的内存,各进程之间不能相互访问

      6. 创建一个新线程很简单,创建新进程需要对父进程进行复制

      多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行

      单道编程: 计算机内存中只允许一个的程序运行

      进程并发性:

          1)在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态

          2)对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念
               他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个

          3)由于操作系统分时,让每个进程都觉得自己独占CPU等资源

          注:如果是多核CPU(处理器)实际上是可以实现正在意义的同一时间点有多个线程同时运行

      线程并发性:

          1)操作系统将时间划分为很多时间段,尽可能的均匀分配给每一个线程。

          2)获取到时间片的线程被CPU执行,其他则一直在等待,所以微观上是走走停停,宏观上都在运行。

          多核CPU情况:          

          如果你的程序的线程数少于CPU的核心数,且系统此时没有其他进程同时运行,那么这个程序的每个线程会享有一个CPU,

          当同时运行的线程数多于CPU核心数时,CPU会采用一定的调度算法每隔一段时间就将这些线程调入或调出CPU
          以确保每个线程都能分享一部分CPU时间,实现多线程并发。

2、有了进程为什么还要线程?

    1. 进程优点:

                          提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率

    2. 进程的两个重要缺点

                          a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

                          b. 第二点:进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。

                          c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻

         即能监听键盘输入、又能监听其它人给你发的消息

                          d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀

                          e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀

线程

1、什么是线程(thread)(线程是操作系统最小的调度单位)

  定义:1)线程是操作系统调度的最小单位

     2)它被包含在进程之中,是进程中的实际运作单位

     3)进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合

  

    1. 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

    2. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

    3. 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行

    4. 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合

    5. 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同

    6. 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程

    7. 两个进程想通信,必须要通过一个中间代理

    8. 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存

2、进程和线程的区别

    启动一个线程比启动一个进程快,运行速度没有可比性。

    先有一个进程然后才能有线程。

    1、进程包含线程

    2、线程共享内存空间

    3、进程内存是独立的(不可互相访问)

    4、进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)

    5、在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现

    6、创建新线程很简单,创建新进程需要对其父进程进行克隆。

    7、一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。

    8、父进程可以修改不影响子进程,但不能修改。

    9、线程可以帮助应用程序同时做几件事

3、进程和程序的区别

    1. 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体

    2. 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资 

     源或事件而被处于等待状态,因完成任务而被撤消

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

    4.一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系)

    5. 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程

多线程

  Python多线程编程中常用方法:

    1、join()方法:如果一个线程或者在函数执行的过程中调用另一个线程,并且希望待其完成操作后才能执行,

     那么在调用线程的时就可以使用被调线程的join方法join([timeout]) timeout:可选参数,线程运行的最长时间

    2、isAlive()方法:查看线程是否还在运行
    3、getName()方法:获得线程名
    4、setDaemon()方法:主线程退出时,需要子线程随主线程退出,则设置子线程的setDaemon()

  1、线程2种调用方式:直接调用, 继承式调用

    

import threading
import time

def sayhi(num):                                   # 定义每个线程要运行的函数
    print("running on number:%s" % num)
    time.sleep(3)

#1、target=sayhi :sayhi是定义的一个函数的名字
#2、args=(1,)    : 括号内写的是函数的参数
t1 = threading.Thread(target=sayhi, args=(1,))    # 生成一个线程实例
t2 = threading.Thread(target=sayhi, args=(2,))    # 生成另一个线程实例

t1.start()                                        # 启动线程
t2.start()                                        # 启动另一个线程

print(t1.getName())                               # 获取线程名
print(t2.getName())

直接调用
import threading
import time

class MyThread(threading.Thread):
    def __init__(self,num):
        threading.Thread.__init__(self)
        self.num = num

    def run(self):#定义每个线程要运行的函数
        print("running on number:%s" %self.num)
        time.sleep(3)

if __name__ == '__main__':
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()

继承式调用

  2、for循环同时启动多个线程

      说明:下面利用for循环同时启动50个线程并行执行,执行时间是3秒而不是所有线程执行时间的总和

import threading
import time

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start()

for循环启动多个线程

   3、t.join(): 实现所有线程都执行结束后再执行主线程

      说明:在4中虽然可以实现50个线程同时并发执行,但是主线程不会等待子线程结束在这里我们可以使用t.join()指定等待某个线程结束的结果

import threading
import time
start_time = time.time()

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)

t_objs = []    #将进程实例对象存储在这个列表中
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start()          #启动一个线程,程序不会阻塞
    t_objs.append(t)
print(threading.active_count())    #打印当前活跃进程数量
for t in t_objs: #利用for循环等待上面50个进程全部结束
    t.join()     #阻塞某个程序
print(threading.current_thread())    #打印执行这个命令进程

print("----------------all threads has finished.....")
print(threading.active_count())
print('cost time:',time.time() - start_time)

t.join() 主线程等待子线程

  4、setDaemon(): 守护线程,主线程退出时,需要子线程随主线程退出

import threading
import time
start_time = time.time()

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.setDaemon(True)  #把当前线程变成守护线程,必须在t.start()前设置
    t.start()          #启动一个线程,程序不会阻塞
print('cost time:',time.time() - start_time)

守护线程

  

    注:因为刚刚创建的线程是守护线程,所以主线程结束后子线程就结束了,运行时间不是3秒而是0.01秒

  5、GIL锁和用户锁(Global Interpreter Lock 全局解释器锁    

     1.全局解释器锁:保证同一时间仅有一个线程对资源有操作权限

        作用:在一个进程内,同一时刻只能有一个线程通过GIL锁 被CUP调用,切换条件:I/O操作、固定时间(系统决定)

        说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多

        1)为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL

        2)GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程

        3)为了解决这个问题,CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据

        4)python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口

        5)但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷   

      2. 用户锁:线程锁(互斥锁Mutex)  :当前线程还未操作完成前其他所有线程都无法对其操作,即使已经释放了GIL锁

        1. 在有GIL锁时为何还需要用户锁

          1)GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了

        2. 线程锁的原理

          1)当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作

          2)这样就可以防止还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱问题

import time
import threading
lock = threading.Lock()          #1 生成全局锁
def addNum():
    global num                  #2 在每个线程中都获取这个全局变量
    print('--get num:',num )
    time.sleep(1)
    lock.acquire()              #3 修改数据前加锁
    num  -= 1                   #4 对此公共变量进行-1操作
    lock.release()              #5 修改后释放

用户锁使用举例

      3. 在有GIL的情况下执行 count = count + 1 会出错原因解析,用线程锁解决方法

# 1)第一步:count = 0   count初始值为0
        # 2)第二步:线程1要执行对count加1的操作首先申请GIL全局解释器锁
        # 3)第三步:调用操作系统原生线程在操作系统中执行
        # 4)第四步:count加1还未执行完毕,时间到了被要求释放GIL
        # 5)第五步:线程1释放了GIL后线程2此时也要对count进行操作,此时线程1还未执行完,所以count还是0
        # 6)第六步:线程2此时拿到count = 0后也要对count进行加1操作,假如线程2执行很快,一次就完成了
        #    count加1的操作,那么count此时就从0变成了1
        # 7)第七步:线程2执行完加1后就赋值count=1并释放GIL
        # 8)第八步:线程2执行完后cpu又交给了线程1,线程1根据上下文继续执行count加1操作,先拿到GIL
        #    锁,完成加1操作,由于线程1先拿到的数据count=0,执行完加1后结果还是1
        # 9)第九步:线程1将count=1在次赋值给count并释放GIL锁,此时连个线程都对数据加1,但是值最终是1

出错原因分析

      1、使用线程锁解决上面问题的原理

          1) 在GIL锁中再加一个线程锁,线程锁是用户层面的锁

          2) 线程锁就是一个线程在对数据操作前加一把锁,防止其他线程复制或者操作这个数据

          3) 只有这个线程对数据操作完毕后才会释放这个锁,其他线程才能操作这个数据

      2、定义一个线程锁非常简单只用三步:

          第一步:  lock = threading.Lock()                        #定义一把锁

          第二步:  lock.acquire()                                        #对数据操作前加锁防止数据被另一线程操作

          第三步:  lock.release()                                         #对数据操作完成后释放锁

   6、死锁

      1. 死锁定义

        两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

      2. 死锁举例

        1. 启动5个线程,执行run方法,假如thread1首先抢到了A锁,此时thread1没有释放A锁,紧接着执行代码mutexB.acquire(),抢到了B锁,
            在抢B锁时候,没有其他线程与thread1争抢,因为A锁没有释放,其他线程只能等待
        2. thread1执行完func1函数,然后执行func2函数,此时thread1拿到B锁,然后执行time.sleep(2),此时不会释放B锁
        3. 在thread1执行func2的同时thread2开始执行func1获取到了A锁,然后继续要获取B锁
        4. 不幸的是B锁还被thread1占用,thread1占用B锁时还需要同时获取A锁才能向下执行,但是此时发现A锁已经被thread2暂用,这样就死锁了

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

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

# 运行结果:输出下面结果后程序卡死,不再向下进行了
# Thread-1 拿到A锁
# Thread-1 拿到B锁
# Thread-1 拿到B锁
# Thread-2 拿到A锁

产生死锁代码

  7、递归锁:lock = threading.RLock()  解决死锁问题

      1. 递归锁的作用是同一线程中多次请求同一资源,但是不会参数死锁。
      2. 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。
      3. 直到一个线程所有的acquire都被release,其他的线程才能获得资源。

from threading import Thread,Lock,RLock
import time

mutexA=mutexB=RLock()

class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 拿到A锁' %self.name)

        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        mutexB.release()

        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 拿到B锁' % self.name)
        time.sleep(0.1)
        mutexA.acquire()
        print('%s 拿到A锁' % self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(5):
        t=MyThread()
        t.start()
# 下面是运行结果:不会产生死锁
# Thread-1 拿到A锁
# Thread-1 拿到B锁
# Thread-1 拿到B锁
# Thread-1 拿到A锁
# Thread-2 拿到A锁
# Thread-2 拿到B锁
# Thread-2 拿到B锁
# Thread-2 拿到A锁
# Thread-4 拿到A锁
# Thread-4 拿到B锁
# Thread-4 拿到B锁
# Thread-4 拿到A锁
# Thread-3 拿到A锁
# Thread-3 拿到B锁
# Thread-3 拿到B锁
# Thread-3 拿到A锁
# Thread-5 拿到A锁
# Thread-5 拿到B锁
# Thread-5 拿到B锁
# Thread-5 拿到A锁

如果使用RLock代替Lock,则不会发生死锁

  8、Semaphore(信号量)

      1. 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
      2. 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
      3. 作用就是同一时刻允许运行的线程数量

# import threading,time
# def run(n):
#     semaphore.acquire()
#     time.sleep(1)
#     print("run the thread: %s\n" %n)
#     semaphore.release()
# 
# if __name__ == '__main__':
#     semaphore  = threading.BoundedSemaphore(5) #最多允许5个线程同时运行
#     for i in range(22):
#         t = threading.Thread(target=run,args=(i,))
#         t.start()
# 
# while threading.active_count() != 1:
#     pass #print threading.active_count()
# else:
#     print('----all threads done---')


# 代码结果说明:这里可以清晰看到运行时0-4是同时运行的没有顺序,而且是前五个,
# 表示再semaphore这个信号量的定义下程序同时仅能执行5个线程

信号量举例

   9、events总共就只有四个方法

      1. event.set()          : 设置标志位

      2. event.clear()       : 清除标志位

      3. event.wait()        : 等待标志被设定

      4. event.is_set()     : 判断标志位是否被设定

import time,threading

event = threading.Event()
#第一:写一个红绿灯的死循环
def lighter():
    count = 0
    event.set()               #1先设置为绿灯
    while True:
        if count > 5 and count <10:      #2改成红灯
            event.clear()          #3把标志位清了
            print("red light is on.....")
        elif count > 10:
            event.set()            #4再设置标志位,变绿灯
            count = 0
        else:
            print("green light is on.....")
        time.sleep(1)
        count += 1

#第二:写一个车的死循环
def car(name):
    while True:
        if event.is_set():         #设置了标志位代表绿灯
            print("[%s] is running"%name)
            time.sleep(1)
        else:
            print('[%s] sees red light, waiting......'%name)
            event.wait()
            print('[%s] green light is on,start going.....'%name)

light = threading.Thread(target=lighter,)
light.start()
car1 = threading.Thread(target=car,args=("Tesla",))
car1.start()

events(红绿灯例子)

协程

  1、什么是协程(进入上一次调用的状态)

      1. 协程,又称微线程,纤程,协程是一种用户态的轻量级线程。

      2. 线程的切换会保存到CPU的栈里,协程拥有自己的寄存器上下文和栈,

      3. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈

      4. 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态

      5. 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)

   2、协程的好处

      1. 无需线程上下文切换的开销(可以理解为协程切换就是在不同函数间切换,不用像线程那样切换上下文CPU)

      2. 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突

      3. 用法最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

  3、协程缺点

      1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上

      2. 线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

  4、使用yield实现协程相同效果

import time
import queue

def consumer(name):
    print("--->starting eating baozi...")
    while True:
        new_baozi = yield  # 只要遇到yield程序就返回,yield还可以接收数据
        print("[%s] is eating baozi %s" % (name, new_baozi))
        time.sleep(1)

def producer():
    r = con.__next__()  # 直接调用消费者的__next__方法
    r = con2.__next__()  # 函数里面有yield第一次加括号调用会变成一个生成器函数不执行,运行next才执行
    n = 0
    while n < 5:
        n += 1
        con.send(n)  # send恢复生成器同时并传递一个值给yield
        con2.send(n)
        print("\033[32;1m[producer]\033[0m is making baozi %s" % n)

if __name__ == '__main__':
    con = consumer("c1")
    con2 = consumer("c2")
    p = producer()

yield模拟实现协程效果

  5、协程为何能处理大并发1:Greenlet遇到I/O手动切换

      1. 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)

      2. 这里先演示用greenlet实现手动的对各个协程之间切换

      3. 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换

from greenlet import greenlet

def test1():
    print(12)       #4 gr1会调用test1()先打印12
    gr2.switch()    #5 然后gr2.switch()就会切换到gr2这个协程
    print(34)       #8 由于在test2()切换到了gr1,所以gr1又从上次停止的位置开始执行
    gr2.switch()    #9 在这里又切换到gr2,会再次切换到test2()中执行

def test2():
    print(56)       #6 启动gr2后会调用test2()打印56
    gr1.switch()    #7 然后又切换到gr1
    print(78)       #10 切换到gr2后会接着上次执行,打印78

gr1 = greenlet(test1)    #1 启动一个协程gr1
gr2 = greenlet(test2)    #2 启动第二个协程gr2
gr1.switch()             #3 首先gr1.switch() 就会去执行gr1这个协程

Greenlet遇到I/O手动切换

  6、协程为何能处理大并发2:Gevent遇到I/O自动切换

      1. Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程

      2. 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程

      3. Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

      4. Gevent原理是只要遇到I/O操作就会自动切换到下一个协程

  7、Gevent实现简单的自动切换小例子

    注:在Gevent模仿I/O切换的时候,只要遇到I/O就会切换,哪怕gevent.sleep(0)也要切换一次

import gevent

def func1():
    print('\033[31;1m第一次打印\033[0m')
    gevent.sleep(2)          # 为什么用gevent.sleep()而不是time.sleep()因为是为了模仿I/O
    print('\033[31;1m第六次打印\033[0m')

def func2():
    print('\033[32;1m第二次打印\033[0m')
    gevent.sleep(1)
    print('\033[32;1m第四次打印\033[0m')

def func3():
    print('\033[32;1m第三次打印\033[0m')
    gevent.sleep(1)
    print('\033[32;1m第五次打印\033[0m')

gevent.joinall([            # 将要启动的多个协程放到event.joinall的列表中,即可实现自动切换
    gevent.spawn(func1),    # gevent.spawn(func1)启动这个协程
    gevent.spawn(func2),
    gevent.spawn(func3),
])

# 运行结果:
# 第一次打印
# 第二次打印
# 第三次打印
# 第四次打印
# 第五次打印
# 第六次打印

Gevent实现简单的自动切换小例子

  8、使用Gevent实现并发下载网页与串行下载网页时间比较

from urllib import request
import gevent,time
from gevent import monkey
monkey.patch_all()      #把当前程序所有的I/O操作给我单独做上标记

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

#1 并发执行部分
time_binxing = 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("并行时间:",time.time()-time_binxing)

#2 串行部分
time_chuanxing = time.time()
urls = [
        'https://www.python.org/',
        'https://www.yahoo.com/',
        'https://github.com/',
                                        ]
for url in urls:
    f(url)
print("串行时间:",time.time()-time_chuanxing)

# 注:为什么要在文件开通使用monkey.patch_all()
# 1. 因为有很多模块在使用I / O操作时Gevent是无法捕获的,所以为了使Gevent能够识别出程序中的I / O操作。
# 2. 就必须使用Gevent模块的monkey模块,把当前程序所有的I / O操作给我单独做上标记
# 3.使用monkey做标记仅用两步即可:
      第一步(导入monkey模块):  from gevent import monkey
      第二步(声明做标记)    :   monkey.patch_all()

并行串行时间比较

    说明:monkey.patch_all()猴子补丁作用

      1)用过gevent就会知道,会在最开头的地方gevent.monkey.patch_all();
      2)作用是把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候可以跟平常一样使用,无需修改任何代码,但是它变成非阻塞的了.

  9、通过gevent自己实现单线程下的多socket并发

import gevent
from gevent import socket,monkey     #下面使用的socket是Gevent的socket,实际测试monkey没用
# monkey.patch_all()

def server(port):
    s = socket.socket()
    s.bind(('0.0.0.0',port))
    s.listen(5)
    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 e:
        print(e)
    finally:
        conn.close()

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

server端
import socket
HOST = 'localhost'    # The remote host
PORT = 8001           # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
    msg = bytes(input(">>:"),encoding="utf8").strip()
    if len(msg) == 0:continue
    s.sendall(msg)
    data = s.recv(1024)
    print('Received', repr(data))
s.close()

client端

  10、协程本质原理

      1. 协程1通过os去读一个file,这个时候就是一个io操作,在调用os的接口前,就会有一个列表

      2. 协程1的这个操作就会被注册到这个列表中,然后就切换到其他协程去处理;

      3. 等待os拿到要读file后,也会把这个文件句柄放在这个列表中

      4. 然后等待在切换到协程1的时候,协程1就可以直接从列表中拿到数据,这样就可以实现不阻塞了

      5. epoll返回给协程的任务列表在内核态,协程在用户态,用户态协程是不能直接访问内核态的任务列表的,
          所以需要拷贝整个内核态的任务列表到用户态,供协程去访问和查询  

  11、epoll处理 I/O 请求原理

      1. epoll() 中内核则维护一个链表,epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。

      2. 在内核实现中 epoll 是根据每个 sockfd 上面的与设备驱动程序建立起来的回调函数实现的。

      3. 某个 sockfd 上的事件发生时,与它对应的回调函数就会被调用,来把这个 sockfd 加入链表,其他处于“空闲的”状态的则不会。

      4. epoll上面链表中获取文件描述,这里使用内存映射(mmap)技术, 避免了复制大量文件描述符带来的开销

      内存映射(mmap):内存映射文件,是由一个文件到一块内存的映射,将不必再对文件执行I/O操作

  12、select处理协程

      1. 拷贝所有的文件描述符给协程,不论这些任务的是否就绪,都会被返回

      2. 那么协程就只能for循环去查找自己的文件描述符,也就是任务列表,select的兼容性非常好,支持linux和windows

 

posted @ 2020-03-09 16:00  一介䝂鷘  阅读(110)  评论(0编辑  收藏  举报