• 并发编程(进程、线程、协程)


    1、空间多路复用是什么?

    考虑一个场景:浏览器,网易云音乐以及notepad++ 三个软件只能顺序执行是怎样一种场景呢?

    另外,假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。你是不是已经想到在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停。聪明,这当然没问题,但这里有一个关键词:切换;

    补充:

    主线程 ----------》开启子线程后-------------他们会在同时一起工作,这就是并发,也可说实现了异步;

    应用:例如有一个Django WEB API 的视图函数, 接收到request后,需要操作数据库等等,如果response中无需数据库数据,就可以把这些数据库操作,做成线程,先return response给用户;

    GIL:Global interpreter lock (全局解释器锁):

    是Python创始人 Guido van Rossum(吉多·范罗苏姆)龟叔,在CPython解释器层面设计的一把线程锁(只针对CPython),规定同一个时刻只能有一个 线程去使用 1个CPU;

    缺陷:Python 无法实现并行(多个同时在不同CPU上执行)和多核时代无缘了;

    优点:武断得解决了 多线程之间 争强资源问题

    2、进程:

    2.0 进程的概念

    进程就是一个程序在一个数据集上的一次动态执行过程(正在运行的软件就是进程。进程一般由程序、数据集、进程控制块三部分组成;

    程序:执行的内容(要做什么)

    数据集:执行过程中用到的数据

    进程控制块:操作系统切换该进程时保存的状态  

    操作系统切换进程的规则:进程出现IO操作、和固定时间

    举栗子说明:

    举个栗子:
    想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他
    离开时的那一步继续做下去。

    CPU就是这个科学家

    食谱就是程序

    面粉、鸡蛋、糖、香草汁等就是数据集

    被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态)就是进程控制块;

    2.1 进程的状态

    就绪:

    阻塞:遇到IO操作

    运行:程序正在被CPU执行

     

    ps:

    僵尸进程:父进程----》fork1个子进程, 子进程退出了,但是父进程是个死循环/ 没有调用wait()方法对子进程进行回收,此时子进程沦为僵尸进程;

    解决:僵尸进程的父进程 kill -9掉,使其沦为孤儿进程,被init进程回收。

    孤儿进程:父进程退出,父进程fork的子进程全部变成孤儿进程,最后由init进程回收;

    2.2 创建、回收子进程

    os.fork()和os.wait() 
    
    介绍:
    
    A. os.fork()是调用Linux系统的fork()创建1个进程,该函数被调用1次 return 2次,返回0代表子进程执行/返回其他代表父进程执
    
    B.getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id;os.wait() 用于父进程 等待子进程结束,对其回收!
    
    C.fork之后,操作系统会复制一个与父进程完全相同的子进程,
    虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,
    子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同。
    子进程从 父进程fork子进程的代码位置开始执行!(fork嘛! 分叉!)
    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import os,time
    print ('程序开始 (%s) start...' %os.getpid())
    
    pid = os.fork()
    
    if pid==0:                                                  #0代表是子进程执行
        print ('我是子进程1!我的进程ID是%s'%(os.getpid()))
    else:                                                         #其他代表父进程执行
        print ('我是父进程!我的进程ID是%s' %(os.getpid()))
        os.wait()
        pid1=os.fork()                                           #再次创建子进程
        if pid1==0:
            print('我是子进程2!我的进程ID是%s' % (os.getpid()))
        else:
            print('我是父进程!我的进程ID是%s' % (os.getpid()))
            os.wait()                                            #等待函数:用于父进程 等待子进程结束,对其回收!

    exex协议簇:

    execl(file, arg0,arg1,...) 用参数列表arg0, arg1 等等执行文件

    execv(file, arglist) 除了使用参数向量列表,其他的和execl()相同

    execle(file, arg0,arg1,... env) 和execl 相同,但提供了环境变量字典env

    execve(file,arglist, env) 除了带有参数向量列表,其他的和execle()相同

    execlp(cmd, arg0,arg1,...) 于execl()相同,但是在用户的搜索路径下搜索完全的文件路径名

    execvp(cmd, arglist) 除了带有参数向量列表,与execlp()相同

    execlpe(cmd, arg0, arg1,... env) 和execlp 相同,但提供了环境变量字典env

    execvpe(cmd,arglist, env) 和execvp 相同,但提供了环境变量字典env



    import
    os print 'start' os.execl('./b.py','') ''' 在fork后的子进程中使用exec函数族,可以装入和运行其它程序(子进程替换原有进程,和>父进程做不同的事)。 ''' print('end') #---------------------------------------------------------- b.py文件 print('1111111111111')

    2.3 进程间通信方式(IPC Inter-Process Communication)

    在同一台计算机中的进程相互通信的方式主要有:管道(pipe)、信号(Signal)、信号量(Semaphore)、消息队列(Message)、共享内存(shared memory),其中信号量、消息队列、共享内存被称为IPC机制

    不同机器之间的进程通讯可以使用套接字技术。SOCKET

    方式1:管道

    无名管道无法解决无父子关系的进程间通信

    a.无名管道

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    
    import os,time
    r,w=os.pipe() #创建1个管道,返回2个文件描述符 读、写
    pid=os.fork()
    
    if pid ==0:
        os.close(w) #由于os.pipe() 建立的是一条单向通道,避免同一个管道中读写冲突,造成数据丢失,接收端关闭 写功能
        r=os.fdopen(r)
        msg=r.read()
        print('接收到一条消息%s'%msg)
    else:
        w=os.fdopen(w,'w')
        w.write('hellow')      ##由于os.pipe() 建立的是一条单向通道,避免同一个管道中读写冲突,造成数据丢失,发送端关闭 读功
    无名管道

    由于无名管道只能在具有父子关系的进程间进行通信,使用范围比较局限,而且只是单工通信,所以有名管道出现了。

    b.有名管道

    创建2条 有名管道,1条写,1条读,实现全双工通信;

    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    import os
    
    P_Name='./zhanggen'
    
    if os.access(P_Name,os.F_OK)==False:
        os.mkfifo(P_Name)
    
    fp_w=open(P_Name,'w')
    while True:
        msg=input('>> ').strip()
        fp_w.write(msg)
        fp_w.flush()
        if msg=='q':
            break
    fp_w.close()
    发送端
    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    import os
    
    P_Name='./zhanggen'
    
    if os.access(P_Name,os.F_OK)==False:
        os.mkfifo(P_Name)
    
    fp_r=open(P_Name,'r')
    while True:
        msg=fp_r.read(3)
        print(msg)
        if msg=='q':
            break
    
    fp_r.close()
    接收端

     ---------------------------------------------------------------解决粘包--------------------------------------------------------------------------------------------------------------------------------

    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    import os
    
    P_Name='./zhanggen'
    
    if os.access(P_Name,os.F_OK)==False:
        os.mkfifo(P_Name)
    
    fp_w=os.open(P_Name,os.O_WRONLY)
    while True:
        msg=input('>> ').strip()
        os.write(fp_w, msg.encode('utf-8'))
        if msg=='q':
            break
    
    os.close(fp_w)
    发送端
    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    import os
    
    P_Name='./zhanggen'
    
    if os.access(P_Name,os.F_OK)==False:
        os.mkfifo(P_Name)
    
    fp_r = os.open(P_Name, os.O_RDONLY)
    while True:
        msg=os.read(fp_r,100)
        print(msg.decode('utf-8'))
        if msg=='q':
            break
    
    os.close(fp_r)
    接收端

      

    方式2:消息队列

    python Queue模块可以解决具有父子关系的进程之间通信,和线程资源争用。如果跨进程程 或者主机 可使用rabbitMQ 、celery、redis...详见

    # !/usr/bin/env python3
    # -*- coding:utf-8 -*-
    import os
    import queue
    
    print(dir(queue))
    myqueue = queue.Queue()
    
    pid=os.fork()
    if pid:
        print('我是父进程')
        myqueue.put('儿子给你100块钱!')
        print(myqueue.get())
    
    else:
        print('我是子进程!')
        print(myqueue.get())
        myqueue.put('666')
    queue模块多进程直接通信
    from multiprocessing import Process,freeze_support,Lock
    import os
    import queue
    Process_Lock=Lock()
    
    Qms=queue.Queue(10)
    Qms.put(1)
    
    def child_process(name):
        #Process_Lock.acquire() / Process_Lock.release()  #程序锁 避免资源征用
        print('我是子进程%s,我的Pid为%s'%(name,os.getpid()))
        #Process_Lock.release()
        msg=Qms.get()
        print(msg)
    
    p=Process(target=child_process,args=('1',) ) #target=函数名称,args=(1,)注意args一定元组。
    
    if __name__ == '__main__':
        freeze_support()
        print('我是主进程我的Pid为%s' % (os.getpid()))
        p.start()#开启进程
        p.join()#主进程等待回收子进程!
    
    #
    Process模块

    方式3:共享内存

    共享内存(Shared Memory)是最简单的进程间通信方式,它允许多个进程访问相同的内存,一个进程改变其中的数据后,其他的进程都可以看到数

    共享内存的数据类型

    value:只能存储字符串

    array:列表中可嵌套其他数据

    3、线程:

    上述说到 在60年代之前,操作系统 在CPU执行程序时,可以根据程序出现了IO操作、固定时间,利用进程控制块,从当前进程切换到其他进程;实现了并发现象;

    由于CPU的执行速度很快,切换的过程 从程序执行效果,我们无法看到,这实现了并发现象;(看似同时执行多个程序,其实是CPU快速切换制造出的并发现象)

    问题出现:

    (1)由于多个进程间每次上下文切换存时,都要进程的保存数据集、程序运行状态等大量数据。

    (2)上下文切换(在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。

    (3)进程间的上下文切换代价比较大 因此需要引入轻型进程-------线程(只保存运行状态,数据集是共享的不需要保存);

    这就意味着:要开发1个word就必须需要有3个进程;进程1执行写入 、进程2执行保存写入、进程2执行读取;

    由于3个进程的数据集都是相互独立的, 这3个进程之间通信、切换时,都要保存一套相同的数据集;导致切换开销非常大;

    解决方法:

    (1)把数据集变成公共资源,进程开多个线程(微进程)共享数据集,线程上下文切换只需要加载少量必要的信息(减少了上下文开销,进程之间数据交换)

    4、总结

    进程:资源管理单位(包含共享数据集+各个线程)

    线程:最小执行单位

    (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
    (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源
    (3)CPU分给线程,即真正在CPU上运行的是线程。

    5、串行、并发、并行

    串行:1颗CPU   执行程序1完了------执行2完了-----执行3结束 (顺序执行)

    并发:1颗CPU   执行程序1一下、执行程序2一下、执行程序2一下 (与串行的本质区别,串行执行完了,再执行下一个,并发执行A一部分,再执行B一部分,再回来执行A

    并行:假设有3颗CPU:CPU1执行程序1、CPU2执行程序2、CPU3执行程序3(同时进行)

    6、同步、异步

    同步就像打电话:拨通电话之后,只有对方接听以后,只能等待对方接听之后才会结束;

    异步:就像发短信,发完了短信,不用等待,可以去做别的事情;

    二、threading模块

    Python多线程:由于Python的GIL锁,导致同一个进程,同一时刻只能有一个线程执行,所以无法实现多线程并行;(多进程可以)

    import threading
    import time
    
    #调用线程方式1
    #线程:是为了解决 并发的问题,在同一个进程里,处理多个任务;
    print("老子是主线程")    #1、主线程先执行
    def tingge():   
        print("听歌")
        time.sleep(6)
        print("听歌结束!")
    
    def xieboke():
        print("写博客")
        time.sleep(5)
        print("写博客结束")
    
    
    t1=threading.Thread(target=tingge) #2、主线程创建 子线程1
    t2=threading.Thread(target=xieboke)#3、主线程创建 子线程2
    t1.start()   # 线程1启动和主线程一起执行
    #t1.run() 以函数的形式执行
    t2.start()   #线程2启动和主线程一起执行
    print("结束")

    执行结果:

    老子是主线程
    听歌
    写博客
    结束
    写博客结束
    听歌结束!

    2、t1.join():主线程 等待这个子线程t1结束之后,再执行主线程;(其他子线程不等待,只是主线程等待

    默认情况下主线程结束,即使子线程没有结束,也会被强制退出,join方法是为了防止主线程退出,子线程不能完整执行;

    import threading,time
    def threading1():
        time.sleep(5)
        print('子进程结束!')
    
    
    t1=threading.Thread(target=threading1)
    print('主线程结束')
    主线程退出,子线程未执行完毕;
    print("老子是主线程")    #1、主线程先执行
    def tingge():   #主线程
        print("听歌")
        time.sleep(6)
        print("听歌结束!")
    
    def xieboke():
        print("写博客")
        time.sleep(5)
        print("写博客结束")
    
    
    t1=threading.Thread(target=tingge) #2、主线程创建 子线程1
    t2=threading.Thread(target=xieboke)#3、主线程创建 子线程2
    t1.start()   # 线程1启动和主线程一起执行
    t1.join()    #等线程1执行完了之后,再往下执行,启动线程2
    t2.start()   #线程2启动和主线程一起执行
    t2.join()    #等线程2执行完了之后,再往下执行,print("结束")
    print("结束")

    3、setDaemon(True):将线程声明为守护线程,必须在start() 方法调用之前设置,如果线程被置为守护线程,将会和主线程(主线程执行完了,最后退出)一起结束;

    守护线程:进程中所有非守护线程结束时,守护线程随之结束。

    import threading
    from time import ctime,sleep
    import time
    
    def Music(name):
    
            print ("Begin listening 我是T1线程to {name}. {time}".format(name=name,time=ctime()))
            sleep(3)
            print("end listening我是T1线程后半部分 {time}".format(time=ctime()))
    
    def Blog(title):
    
            print ("Begin recording我是T2线程 the {title}. {time}".format(title=title,time=ctime()))
            sleep(3)
            print('end recording 我是T2线程后半部分{time}'.format(time=ctime()))
    
    
    threads = []
    
    
    t1 = threading.Thread(target=Music,args=('FILL ME',))
    t2 = threading.Thread(target=Blog,args=('',))
    
    threads.append(t1)
    threads.append(t2)
    
    if __name__ == '__main__':
    
        #t2.setDaemon(True)
        t1.setDaemon(True)#如果你设置一个线程为守护线程,就表示你在说这个线程会在 其他非守护线程执行完之后自动退出,。(应用于监听、日志打印服务)
        for t in threads:
    
           t.start()
    
    
    
        # t1.join() #先执行主线程 T1线程执行完了,执行
        # t2.join()    #  考虑这三种join位置下的结果?
    
        print ("all over %s" %ctime())

    执行结果;

    Begin listening 我是T1线程to FILL ME. Mon May  8 20:08:25 2017
    Begin recording我是T2线程 the . Mon May  8 20:08:25 2017
    all over Mon May  8 20:08:25 2017
    end listening我是T1线程后半部分 Mon May  8 20:08:28 2017

    4、threading其他方法

    Thread实例对象的方法
      # isAlive(): 返回线程是否活动的。
      # getName(): 返回线程名。
      # setName(): 设置线程名。
    
    threading模块提供的一些方法:
      # threading.currentThread(): 返回当前的线程变量。
      # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
      # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

    -------------------------------------------------------------------------------------------------------------

     

    5、线程资源争用现象 

     1、开100个线程对全局变量sum进行-1操作(正常执行)

    import time
    import threading
    
    def subNum():
        global num     #1、每个线程中都获取 全局变量 num=100
                 #2、每个遇到IO操作,切换 到下一个线程,循环上次拿到num=100
        num -=1         #3、每个线程睡醒之后。。。 对全局变量num-1(100-1)线程结束
    num = 100
    thread_list = []
    
    for i in range(100): #开启100个线程
        t = threading.Thread(target=subNum)
        t.start()
        thread_list.append(t)
    
    for t in thread_list: #等待所有线程执行完毕
        t.join()
    
    print('Result: ', num)

    执行结果:

      Result:  0

    各个线程争用 进程数据集现象

    import time
    import threading
    
    def addNum():
        global num           #每个线程中都获取这个全局变量 num=100 
        temp=num             #线程创建temp变量
        time.sleep(0.01)     #线程遇到IO操作,切换下一个线程一直到第100个线程, (切换前保存当前状态 temp=100,)
        num =temp-1          #线程睡醒拿到 之前保存的状态 变量temp=100   进行temp-1操作 100-1=99  线程执行结束;
        print(num)
    num = 100  #设定一个共享变量
    thread_list = []
    for i in range(100):
        t = threading.Thread(target=addNum)
        t.start()
        thread_list.append(t)
    
    for t in thread_list: #等待所有线程执行完毕
        t.join()
    
    print('Result: ',num)  

    Result:  99

    线程间公共资源争用效果:

    6、同步锁(互斥锁)

    由于一个进程里出现多个线程,线程切换时会导致资源争用,所以要有一把互斥锁,在某1线程,拿到公共数据集时把数据锁住,其他线程等待锁释放之后,在使用资源;

    def addNum():
        mutexA = threading.Lock()
        global num 
        mutexA.acquire() # 对 取得公共资源加锁
        temp=num
        time.sleep(0.1)
        num =temp-1  # 
        mutexA.release()  #释放锁
    num = 100  
    
    thread_list = []
    
    for i in range(100):
        t = threading.Thread(target=addNum)
        t.start()
        thread_list.append(t)
    
    for t in thread_list: #等待所有线程执行完毕
        t.join()
    
    print('Result: ', num)

    执行结果:

    99(恢复正常)

    7、递归锁

    由于多线程使用 互斥锁,都导致死锁,所以在封装了互斥锁的基础上,出现了递归(内部有count计算器,如果有线程拿到了递归锁,其他线程只能等待)

    死锁现象

    class Mythread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
    
        def run(self):
            self.func1()
            self.func2()
        def func1(self):
            mutexA.acquire()  #thread-1 首先取得同步锁 A
            print("I am %s , get res: %s---%s" % (self.name, "ResA", time.time()))
            mutexB.acquire() #thread 1 又取得同步锁 B
            print("I am %s , get res: %s---%s" % (self.name, "ResB", time.time()))
            mutexB.release()  #hread-1 又释放同步锁B
            mutexA.release()  #thread1 又释放同步锁A
    
        def func2(self):
            mutexB.acquire()  #由于 其他线程都在 GIL锁中,在他们竞争的期间, hread-1 又取得同步锁
            print("I am %s , get res: %s---%s" % (self.name, "ResB", time.time()))
            time.sleep(0.2)    #由于thread1 出现IO操作,所以GIL锁中 其他线程 thread2竞争出来
    
            mutexA.acquire()  #thread2得到同步锁,由于hread-1 还没有释放同步锁,导致死锁
            print("I am %s , get res: %s---%s" % (self.name, "ResA", time.time()))
            mutexA.release()
            mutexB.release()
    
    
    if __name__ == '__main__':
        print("开始")
    
        for i in range(0,10):
            myt_hread=Mythread()
            myt_hread.start()

    递归锁出现

    在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

    mutex = threading.RLock()           #创建一把递归锁
    class Mythread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
    
        def run(self):
            self.func1()
            self.func2()
        def func1(self):
            mutex.acquire()  #hread-1 首先取得同步锁
            print("I am %s , get res: %s---%s" % (self.name, "ResA", time.time()))
            mutex.acquire()
            print("I am %s , get res: %s---%s" % (self.name, "ResB", time.time()))
            mutex.release()
            mutex.release()
    
        def func2(self):
            mutex.acquire()
            print("I am %s , get res: %s---%s" % (self.name, "ResB", time.time()))
            time.sleep(0.2)
    
            mutex.acquire()
            print("I am %s , get res: %s---%s" % (self.name, "ResA", time.time()))
            mutex.release()
            mutex.release()
    
    
    if __name__ == '__main__':
        print("开始")
    
        for i in range(0,10):
            myt_hread=Mythread()
            myt_hread.start()
    

      

    8、线程的Event对象

    线程的一个关键特性是每个线程都是独立运行且状态不可预测,为了解决这些问题,我们需要使用threading库中的Event对象,让线程之间可以相互影响和控制相互通信;

    import threading
    import time
    import logging
    
    logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s',)
    
    def worker(event):
        logging.debug('Waiting for redis ready...')
        event.wait()                     #线程遇到 envent.wait()等待。。。
        logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime())
        time.sleep(1)
    
    def main():
        readis_ready = threading.Event() #创建event对象
        t1 = threading.Thread(target=worker, args=(readis_ready,), name='t1')
        t1.start()
    
        t2 = threading.Thread(target=worker, args=(readis_ready,), name='t2')
        t2.start()
    
        logging.debug('first of all, check redis server, make sure it is OK, and then trigger the redis ready event')
        time.sleep(3) # simulate the check progress  
        readis_ready.set()            #唤醒 envent.wait的线程
    
    if __name__=="__main__":
        main()

    执行结果(类似三个人一起去野外,两人站在原地不动,一个人出前面探路,然后唤醒后面的两人,再走)

    (t1        ) Waiting for redis ready...   #线程1执行,遇到event.wait()等待 唤醒
    (t2        ) Waiting for redis ready...   #线程2执行,遇到event.wair()等待 唤醒
    (MainThread) first of all, check redis server, make sure it is OK, and then trigger the redis ready event #主线程唤醒 线程1 和线程2 开始支持
    (t1        ) redis ready, and connect to redis server and do some work [Tue May  9 19:03:19 2017]
    (t2        ) redis ready, and connect to redis server and do some work [Tue May  9 19:03:19 2017]
    event.isSet():返回event的状态值;
    
    event.wait():如果 event.isSet()==False将阻塞线程;
    
    event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
    
    event.clear():恢复event的状态值为False。

    9、Semaphore(线程连接池)

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

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

    import threading
    import time
    
    semaphore = threading.Semaphore(5)  #创建一个连接池对象,限制这个池子里面只能出现 5个线程
    
    def func():
        if semaphore.acquire():  #
            print (threading.currentThread().getName() + ' get semaphore')
            time.sleep(2)
            semaphore.release()
    
    for i in range(20):
      t1 = threading.Thread(target=func)  #
      t1.start()
    
    执行结果
    hread-1 get semaphore  #每5个线程为一组执行
    Thread-2 get semaphore
    Thread-3 get semaphore
    Thread-4 get semaphore
    Thread-5 get semaphore
    
    Thread-6 get semaphore
    Thread-8 get semaphore
    Thread-9 get semaphore
    Thread-10 get semaphore
    
    Thread-7 get semaphore
    Thread-11 get semaphore
    Thread-13 get semaphore
    Thread-14 get semaphore
    Thread-12 get semaphore
    Thread-15 get semaphore
    
    Thread-17 get semaphore
    Thread-16 get semaphore
    Thread-19 get semaphore
    Thread-20 get semaphore
    Thread-18 get semaphore
    

     

    协程:

    由于Ipython上的那把全局解释器锁(GIL)Python的多线程其实已经被弱化了(既然多线程也无法实现并行,线程和协程都能实现并发,协程遇到IO切换代价小,那我为什么不要协程呢?)

    线程、多进程、协程都是计算机中实现并发的重要手段

    多进程的并发:是通过启动多个进程,N个CPU 同时调用N进程实现并行;

    协程并发:1个主线程开启N个线程,遇到IO操作切换;

    协程:

    由于线程的开启和销毁都需要一些额外的开销,于是程序员在 用户态空间 使用1个线程 ---》 通过程序逻辑控制  ------》当前线程在N个任务之间不断切换------》实现并发-------》协程

    协程 和进程、线程的区别?

    区别在于: 协程间的切换是通过 程序自己 调度切换,而进程和线程是通过操作系统;

    如何实现1个线程在N个任务之间的切换?

    def consumer():
        while True:
            x=yield #切换
            print('处理了数据:',x)
    
    
    def producer():
        c=consumer()
        next(c)
        for i in range(10):
            c.send(i)  #send就是给yield 1个值
    
    producer()
    #可以看出producer函数在自己 和consumer函数 之间一直切换
    单线程实现切换
    def foo(): 
        r=''
        print("开始")
        while True:
            n=yield r   #
            print("我拿到%s啦!"%n)
    
    
    a=foo()
    # next(a)  #触发生成器函数的运行,直到碰到n=yield r停止
    # a.send(1) #send 在上次next的基础上  触发生成器函数的运行,会传值给yield
    next(a)
    a.send(1)
    a.send(2)
    a.send(3)
    a.send(4)
    View Code

    我们上面我们使用yeild和send()实现了1个线程在N个任务之间切换的效果,

    Monkey_patch()

    把模块中阻塞IO,在不改变代码的前提下,改变成异步IO

    class Monkey():
        def say(self):
            print('我是猴王')
    m=Monkey()
    
    def say_(m):
        print('猴您大爷!')
    
    Monkey.say=say_ #狸猫换太子!!
    m.say()
    monkey_patch()替换原理

    Greenlet模块实现切换

    而真正的协程 使用greenlet模块完成得切换,

    greelet的switch()触发切换;

    greenlet每次切换前都会记录下 程序当前运行到得位置,下次在切换回来的时候 从该位置重新开始;

    因此它的切换是循序渐进的;

    from greenlet import greenlet
    
    def eat1(name):
        print('吃点饭......')
        g2.switch('Martin')
        print('喝完水吃点水果.....')
        g2.switch('Martion')
        print('泡完妞,再泡个妞.....')
        g2.switch('Martin')
    def play(name):
        print('喝点水....')
        g1.switch(name='Martion')
        print('吃完水果再泡个妞....')
        g1.switch('Martin')
        print('不泡了,睡觉!!')
    
    g1=greenlet(eat1)#注册任务1
    g2=greenlet(play)#注册任务2
    g1.switch('zhanggen')#switch()触发切换
    
    '''
    从上面的例子中我们可以看出,1个线程可以在 N个任务之间来回切换了
    (注意:每次切换前greenlet都会记录下程序 当前运行到得位置,下次在切换回来的时候 从结束的位置从新开始;
    (上传切换的点,是上次任务的开始点,consquently greenlet的切换是循序渐进的)
    
    '''
    greenlet循序渐进得在N个任务之间进行切换

    greenlet模块使我们的程序 拥有了更强的灵活性,具备了在不同的任务间不断切换的能力;

    但是无意义的切换是会增加程序的开销和时间复杂度的;(虽然我老家距离北京很近交通便利,但即便没有交通费用,我也不会每天回去,因为耗费精力,如果我放假的时候回去交通费免费,这才合理;)

    所以协程的意义是 :当单个线程 执行某1个任务时遇到IO操作时,通过greenlet切换到另1个任务执行,规避 IO等待时间,最大限度利用了CPU,实现并发效果;

    优点:

    1.由于单线程,无需创建和销毁太多的线程、 无需操作系统做上下文切换;

    2.单线程容易管理不需要对公用 数据集加锁;

    总结:

    在高并发的场景下我们会 使用 进程---》开线程---》线程里有IO操作----》切换协程  的方式来最大限度得提高并发效果;

    1台服务器(4个CPU)建议启动 5个进程

    1个进程可以建议20个线程

    1个线程可以建议500个协程

    所以Nginx部署在1台4核服务器上的最大并发数 是 5*20*500=5W个 

    为什么不能启动更多的进程、线程、协程呢?

    启动线程:需要创建、销毁、保存堆栈上下文对象

    启动协程:需要不断得切换,也耗费时间 

    所以不是越多越好;

    Gevent模块开启协程

    greenlet模块可以实现 1个线程在不同任务间得切换,但是在不同任务间切换也需要耗费时间、资源啊,所以我应该在检测到IO操作的时候,自动切换到其他任务;

    哈哈......Gevent就是干这事的!

    # from greenlet import greenlet
    #
    # def eat1(name):
    #     print('吃点饭......')
    #     g2.switch('Martin')
    #     print('喝完水吃点水果.....')
    #     g2.switch('Martion')
    #     print('泡完妞,再泡个妞.....')
    #     g2.switch('Martin')
    # def play(name):
    #     print('喝点水....')
    #     g1.switch(name='Martion')
    #     print('吃完水果再泡个妞....')
    #     g1.switch('Martin')
    #     print('不泡了,睡觉!!')
    #
    # g1=greenlet(eat1)#注册任务1
    # g2=greenlet(play)#注册任务2
    # g1.switch('zhanggen')#switch()触发切换
    #
    # '''
    # 从上面的例子中我们可以看出,1个线程可以在 N个任务之间来回切换了
    # (注意:每次切换前greenlet都会记录下程序 当前运行到得位置,下次在切换回来的时候 从结束的位置从新开始;
    # (上传切换的点,是上次任务的开始点,consquently greenlet的切换是循序渐进的)
    #
    # '''
    
    ''''
    一般情况下
    
    '''
    import gevent
    #直接可以 import gevent,所有gevent模块相对比较简单
    from gevent import spawn,joinall,monkey;monkey.patch_all()
    #注意 gevent模块默 只能检测到gevent.sleep()类型的IO操作,无法检测到time/request/paramiko等模块中的IO操作,在导入这些模块之前需要 monkey.patch_all() 补丁才可以
    import time
    import threading
    def task1(name):
        print('我是Task1。。。。。。')
        print(threading.current_thread())
        print(threading.current_thread().getName())
        time.sleep(6)
        print('Task1结束。。。。。。')
    
    
    def task2(name):
        print(threading.current_thread())
        print(threading.current_thread().getName())
        print('我是Task2。。。。。。')
        time.sleep(6)
        print('Task2结束。。。。。。')
    
    def task3(name):
        print(threading.current_thread())#获取协程ID
        print(threading.current_thread().getName())#获取协程名称
        print('我是Task3。。。。。。')
        time.sleep(6)
        print('Task3结束。。。。。。')
        return name
    
    # g1=gevent.spawn(task1,'martin') #开启协程,参数
    # g2=gevent.spawn(task2,'martin')
    # g3=gevent.spawn(task3,'martin')
    #
    # g1.join()                       #但我们使用spawn()开启协程之后,主线程整体就变成异步的了,如何让主线程等待 所有协程执行完毕?join()
    # g2.join()
    # g3.join()
    # print(g3.value)                 #获取协程的返回值
    
    gevent.joinall([gevent.spawn(task3,'martin'),gevent.spawn(task2,'martin'),gevent.spawn(task1,'martin'),] )
    #一步到位的语法,joinall([task1,task2,....])就是为了设置协程执行的 顺序
    '''
    我是Task1。。。。。。
    我是Task2。。。。。。  切啦 
    我是Task3。。。。。。  切啦
    Task1结束。。。。。。
    Task2结束。。。。。。
    Task3结束。。。。。。
    '''
    gevent模块简单使用

    同步 VS 协程异步

    # 协程异步
    import gevent
    #直接可以 import gevent,所有gevent模块相对比较简单
    from gevent import spawn,joinall,monkey;monkey.patch_all()
    #注意 gevent模块默 只能检测到gevent.sleep()类型的IO操作,无法检测到time/request/paramiko等模块中的IO操作,在导入这些模块之前需要 monkey.patch_all() 补丁才可以
    import time
    import threading
    def task():
        time.sleep(1)
    
    def sync_():
        for i in range(10):
            task()
    
    def async_():
        g_list=[]
        for i in range(10):
            g=gevent.spawn(task)
            g_list.append(g)
        print(g_list)
        gevent.joinall(g_list)
    
    # sync_() #同步
    
    async_()#协程异步
    Gevent之同步与异步

    协程的应用

    爬虫:

    分析:大致分析1下,1个爬虫工作流程中遇到几次IO操作:

    1.Python程序发起系统调用-------->客户端操作系统开通端口走网络协议
    2.客户端操作系统开通端口--------->请求网站的服务器80端口
    3.网站服务器80端口接收请求------->发送给应用处理
    4.应用处理之后------------------>结果返回服务器操作系统
    5.服务器操作系统---------------->响应客户操作系统
    6.客户端操作系统---------------->Python程序

    以上分析出 1次爬虫过程需要6段的IO操作
    如果是1个爬虫没办法,你只能等待,开协程没有意义!往哪里切换?
    如果是多个爬虫,我们可以利用以上6次IO等待时间,去发更多的URL请求 爬其他网站的数据!!
    那么我们使用协程:
    from gevent import monkey;monkey.patch_all()
    import gevent
    import requests
    import time
    
    def get_page(url):
        print('GET: %s' %url)
        response=requests.get(url)
        if response.status_code == 200:
            print('%d bytes received from %s' %(len(response.text),url))
    
    
    start_time=time.time()
    gevent.joinall([
        gevent.spawn(get_page,'https://www.python.org/'), #开启协程
        gevent.spawn(get_page,'https://www.yahoo.com/'),  #开启协程
        gevent.spawn(get_page,'https://github.com/'),     #开启协程
    ])
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    '''
    分析:大致分析1下,1个爬虫工作流程中遇到几次IO操作:
    
    1.Python程序发起系统调用-------->客户端操作系统开通端口走网络协议  
    2.客户端操作系统开通端口--------->请求网站的服务器80端口      
    3.网站服务器80端口接收请求------->发送给应用处理               
    4.应用处理之后------------------>结果返回服务器操作系统
    5.服务器操作系统---------------->响应客户操作系统
    6.客户端操作系统---------------->Python程序
    
    以上分析出 1次爬虫过程需要6段的IO操作
    如果是1个爬虫没办法,你只能等待,往哪里切换?
    如果是多个爬虫,我们可以利用以上6次IO等待时间,去发更多的URL请求 爬其他网站的数据!!
    那么我们使用协程:
    
    '''
    协程的应用:爬虫

    Socket

    from gevent import monkey;monkey.patch_all()
    from socket import *
    import gevent
    
    #如果不想用money.patch_all()打补丁,可以用gevent自带的socket
    # from gevent import socket
    # s=socket.socket()
    
    def server(server_ip,port):
        s=socket(AF_INET,SOCK_STREAM)
        s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
        s.bind((server_ip,port))
        s.listen(5)
        while True:
            conn,addr=s.accept()
            gevent.spawn(talk,conn,addr)
    
    def talk(conn,addr):
        try:
            while True:
                res=conn.recv(1024)
                print('client %s:%s msg: %s' %(addr[0],addr[1],res))
                conn.send(res.upper())
        except Exception as e:
            print(e)
        finally:
            conn.close()
    
    if __name__ == '__main__':
        server('127.0.0.1',8080)
    
    server
    server
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8080))
    
    
    while True:
        msg=input('>>: ').strip()
        if not msg:continue
    
        client.send(msg.encode('utf-8'))
        msg=client.recv(1024)
        print(msg.decode('utf-8'))
    单线程客户端
    from threading import Thread
    from socket import *
    import threading
    
    def client(server_ip,port):
        c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
        c.connect((server_ip,port))
    
        count=0
        while True:
            c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8'))
            msg=c.recv(1024)
            print(msg.decode('utf-8'))
            count+=1
    if __name__ == '__main__':
        for i in range(500):
            t=Thread(target=client,args=('127.0.0.1',8080))
            t.start()
    
    多线程并发多个客户端
    多线程客户端模拟500并发

    协程

  • 相关阅读:
    reduce常规教程
    新的职业计划
    vscode插件 console helper
    webpack的loader和plugin的区别
    for in 和for of的区别
    https://www.codegrepper.com/index.php
    防抖和节流
    千万级别的表分页查询非常慢,怎么办?
    https://gitee.com/knif/AcceleratorKunn?_from=gitee_search
    17个可以实现微前端的方案
  • 原文地址:https://www.cnblogs.com/sss4/p/6825326.html
Copyright © 2020-2023  润新知