7 并发编程

什么是多路复用

多路复用分为:时间上的复用和空间上的复用

  • 时间上复用:
当一个程序在等待I/O时,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%
强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样才能保证下次切换回来时,能基于上次切走的位置继续运行
  • 空间上复用:
多个运行程序同时加载到内存,硬件层面提供保护机制确保各自内存是分割开的。这比一个程序独占整块内存空间一个一个进入内存效率高。

这两种方式合起来便是:多道技术

多道技术针对单核,实现并发


进程理论篇

1 什么是进程

正在进行的一个过程或者说一个任务。而负责执行任务则是cpu。

举例:单核+多道,实现多个进程的并发执行

进程之间内存是相互隔离的,每当开启一个子进程,都会完全拷贝一份新的父类代码执行。


2 进程与程序的区别

程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。


3 并发与并行

  • 并发:
并发是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)
  • 并行:
同时运行,只有具备多个cpu才能实现并行

单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术是针对单核而言的)


4 同步异步/阻塞非阻塞

  • 同步:
同步就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。
按照这个定义,其实绝大多数函数都是同步调用。
但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
  • 异步:
异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
  • 阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
  • 非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
  • 小结:
1.同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行,当函数返回的时候通过状态、通知、事件等方式通知进程任务完成。

2.阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

5 进程的三种状态

运行态-阻塞态-就绪态

当程序a执行过程中遇到IO时,系统会调离执行权限,去就绪态执行程序b,当程序b遇到IO时,会去执行已经IO完成的程序a。

image


6 开启进程的方式

multiprocessing模块

python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看核数),在python中大部分情况需要使用多进程。

强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。

process类

  • Process类的使用
在windows中Process()必须放到# if __name__ == '__main__',下
linux系统中,就无所谓了。
  • 创建进程的两种方式(方式一):
import time
from multiprocessing import Process

def piao(name):
    print('%s piaoing' %name)
    time.sleep(2)
    print('%s piao end' %name)
    
if __name__ == '__main__':   
    p = Process(target=task, args=('子进程1',))#必须加,号
    p1 = Process(target=task, kwargs={'name': '子进程1'})

    p.start()  # 仅仅是给操作系统发送了一个信号
    p1.start()
    print('主线程')
  • 创建进程的两种方式(方式二):
import time
import random
from multiprocessing import Process

class Piao(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
        
    def run(self):  # 固定一定要叫这个名字
        print('%s piaoing' %self.name)
        time.sleep(random.randrange(1,5))
        print('%s piao end' %self.name)

if __name__ == '__main__':
    p1=Piao('egon')
    p2=Piao('alex')
    
    p1.start()  # start会自动调用run
    p2.start() 
    print('主线程')

进程之间内存是相互隔离的,每当开启一个子进程,都会完全拷贝一份新的父类代码执行。

process方法介绍

  • join
p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
  • terminate
p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
p1.start()  # 系统开启的进程
p.terminate()  # 干死该进程,发个指令给操作系统,具体什么时间啊回收,是操作系统的事(回收内存空间是操作系统回收)
print(is_alive())  # 返回True,说明操作系统还没回收,过会就回收了。
  • is_alive
p.is_alive():如果p仍然运行,或者是否存活,是返回True
  • start
p.start():启动进程,并调用该子进程中的p.run()
  • run
p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 
  • CPU个数
count = multiprocessing.cpu_count()

属性介绍

  • daemon
p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
  • name
 p.name:进程的名称
 列:p = Process(target=task, name='sub_proscess')
print('主进程...')
print(p1.name)
  • pid
p.pid:进程的pid
列:
os.getpid() 查看自己的进程编号
os.getppid() 查看自己父类的进程编号
  • exitcode
p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
  • authkey
p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)

windows 系统cmd下 输入tasklist | findstr pycharm 可查看pycharm的pid

linux系统 终端下 输入ps aux |grep pycharm 可查看pycharm的pid

补充:

p.join() # 等待进程p结束后,join函数内部会发送系统调用wait,  去告诉操作系统回收掉进程p的id号

print(p.pid) #???此时能否看到子进程p的id号
答:可以
p.join()是像操作系统发送请求,告知操作系统p的id号不需要再占用了,回收就可以。
此时在父进程内还可以看到p.pid,但此时的p.pid是一个无意义的id号,因为操作系统已经将该编号回收

7 僵尸进程

例:一个主进程下有10个子进程,进程之间相互独立。

答:所有的子进程都会经历僵尸进程状态。
所有的子进程死掉以后,并不是把所有的信息全部清空掉,内容空间会被清理掉,但是会保留下来一些状态信息。相当于一个儿子死掉以后会留下尸体,目的是为了让父进程不管什么时候来看这个子进程的信息,都会存在但已经死掉,父进程会来收尸。因为父进程死掉以后会发起一个系统调用,会把一些僵尸儿子全部清理掉。

坏处:
前提父进程一直不死,每开启一个子进程都会暂用内存,而每一个子进程死掉以后,pid不会立即清理掉。占用内存。

8 孤儿进程

例:一个主进程下有10个子进程,进程之间相互独立。

答:如果子进程没有死,父进程先死了。就出现了孤儿进程。
在linux系统之上会有一个__init__进程,这个进程是所有的进程它爹。当主进程死了,那么主进程下的子进程,会交给init进程管理。

进程实操篇

1 join

join的出现,保证了子进程执行完毕。

from multiprocessing import Process
import time

def task(name, n):
    print('%s is runing' % name)
    time.sleep(n)

if __name__ == '__main__':
    p1 = Process(target=task, args=('进程1', 3))
    p2 = Process(target=task, args=('进程2', 1))

    p_l = [p1, p2]
    for p in p_l:
        p.start()

    p1.join()  # 保证子进程p运行完,在执行主进程
    p2.join()  # 保证子进程p运行完,在执行主进程
    print('主进程...')
    """
    程序等待5秒,执行完,并不会串行.
    以下是串行:
    p1.start()
    p1.join()  
    p2.start()
    p2.join()  
    """

2 守护进程

主进程创建守护进程

其一:守护进程会在主进程代码执行结束后就终止

其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止

import time
from multiprocessing import Process

def foo():
    print(123)
    time.sleep(1)
    print('end123')

def bar():
    print(456)
    time.sleep(3)
    print('end456')

if __name__ == '__main__':
    p1 = Process(target=foo)
    p2 = Process(target=bar)

    p1.daemon = True  # 设置为守护进程;切记一定要在start之前

    p1.start()
    p2.start()
    print("main-----------") # 打印该行则主进程代码结束,则守护进程p1应该被终止,可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止

3 互斥锁

进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理

  • 多个进程共享同一打印终端

    并发运行,效率高,但竞争同一打印终端,带来了打印错乱

from multiprocessing import Process
import os,time

def work():
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())

if __name__ == '__main__':
    for i in range(3):
        p=Process(target=work)
        p.start()
  • 加锁:由并发变成了串行,牺牲了运行效率,但避免了竞争
import time
from multiprocessing import Process, Lock

def task(name, mutex):
    mutex.acquire()  # 拿到锁
    print('%s 1' % name)
    time.sleep(1)
    print('%s 2' % name)
    time.sleep(1)
    print('%s 3' % name)
    mutex.release()  # 释放锁

if __name__ == '__main__':
    mutex = Lock()  # 全局生成锁,传递给子进程
    for i in range(3):
        p = Process(target=task, args=('进程%s' % i, mutex))
        p.start()

总结:

1、加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。 虽然可以用文件共享数据实现进程间通信,但问题是: 1.效率低(共享数据基于文件,而文件是硬盘上的数据) 2.需要自己加锁处理

2、因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。 1 队列和管道都是将数据存放于内存中 2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来, 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。


互斥锁应用

模拟抢票

import json, time
from multiprocessing import Process, Lock

def search(name):
    time.sleep(1)
    dic = json.load(open('db.txt', 'r', encoding='utf-8'))
    print('<%s> 查看到剩余票数[%s]' % (name, dic['count']))

def get(name):
    time.sleep(1)
    dic = json.load(open('db.txt', 'r', encoding='utf-8'))
    if dic['count'] > 0:
        dic['count'] -= 1
        time.sleep(2)
        json.dump(dic, open('db.txt', 'w', encoding='utf-8'))
        print('<%s>购票成功' % name)
    else:
        print('<%s>购票失败' % name)

def task(name, mutex):
    search(name)
    mutex.acquire()
    get(name)
    mutex.release()

if __name__ == '__main__':
    mutex = Lock()
    for i in range(10):
        p = Process(target=task, args=('路人%s' % i, mutex))
        p.start()

互斥锁和join的区别

互斥锁:可以对整段代码中,只对局部的共享数据修改代码,加上互斥锁,变为串行。

join:是将整体代码变为串行。一个一个执行,虽保证了数据的安全性,但损失了性能。

不加锁:并发执行,速度快,数据不安全

import json, time
from multiprocessing import Process, Lock

def search(name):
    time.sleep(1)
    dic = json.load(open('db.txt', 'r', encoding='utf-8'))
    print('<%s> 查看到剩余票数[%s]' % (name, dic['count']))

def get(name):
    time.sleep(1)
    dic = json.load(open('db.txt', 'r', encoding='utf-8'))
    if dic['count'] > 0:
        dic['count'] -= 1
        time.sleep(2)
        json.dump(dic, open('db.txt', 'w', encoding='utf-8'))
        print('<%s>购票成功' % name)

def task(name):
    search(name)
    get(name)

if __name__ == '__main__':
    for i in range(10):
        p = Process(target=task, args=('路人%s' % i, ))
        p.start()
        p.join()

4 队列(推荐)

进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的

创建队列的类(底层就是以管道和锁定的方式实现)

Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
maxsize是队列中允许最大项数,省略则无大小限制,但受内存大小影响

方法介绍:

  • put
q.put方法用以插入数据到队列中。
put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
  • get
q.get方法可以从队列读取并且删除一个元素。(数据先进先出)
同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
  • get_nowait() & put_nowait()
q.get_nowait():同q.get(False)
q.put_nowait():同q.put(False)
  • empty()
q.empty():调用此方法时q为空则返回True。
该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
  • full()
q.full():调用此方法时q已满则返回True。
该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
  • qsize()
q.qsize():返回队列中目前项目的正确数量。
结果也不可靠,理由同q.empty()和q.full()一样

其他方法(了解):

  • cancel_join_thread()
q.cancel_join_thread():不会在进程退出时自动连接后台线程。可以防止join_thread()方法阻塞
  • close()
q.close():关闭队列,防止队列中加入更多数据。
调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将调用此方法。关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。
  • join_thread()
q.join_thread():连接队列的后台线程。
此方法用于在调用q.close()方法之后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread方法可以禁止这种行为

应用:

multiprocessing模块支持进程间通信的两种主要形式:管道和队列 都是基于消息传递实现的,但是队列接口

队列是先进先出型

from multiprocessing import Process,Queue
import time
q=Queue(3)

q.put(3)
q.put(4)
q.put(5)
print(q.full()) # 是不是满了,满了返回True

print(q.get())
print(q.get())
print(q.get())
print(q.empty()) # 是不是空了,空则返回True

生产者消费者模型

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

为什么要使用生产者和消费者模式:

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式

什么是生产者消费者模式:

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

基于队列实现生产者消费者模型

# from multiprocessing import Process, Lock
# import os,time
#
# def work(mutex):
#     mutex.acquire()
#     print('%s is running' %os.getpid())
#     time.sleep(2)
#     print('%s is done' %os.getpid())
#     mutex.release()
#
# if __name__ == '__main__':
#     mutex = Lock()
#     for i in range(3):
#         p=Process(target=work, args=(mutex, ))
#         p.start()
# import json
#
# dic = open('db.txt', 'r', encoding='utf-8')
# print(json.load(dic))



# from multiprocessing import Queue
#
# q = Queue(3)
# q.put('hello')
# q.put({'a': 1})
# q.put([3,3,1])
#
# print(q.full())


from multiprocessing import Process, JoinableQueue
import time, random, os


def consumer(q):
    while True:
        res = q.get()
        if res is None: break  # 收到结束信号则结束
        time.sleep(random.randint(1, 2))
        print('\033[45m%s 吃 %s\033[0m' % (os.getpid(), res))
        q.task_done()  # 通知生产者取走了一个数据


def producer(q):
    for i in range(3):
        time.sleep(random.randint(1, 2))
        res = '包子%s' % i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' % (os.getpid(), res))
    q.join()  # 等待队列中的数据被取完。就不等了。


if __name__ == '__main__':
    q = JoinableQueue()
    # 生产者们:即厨师们
    p1 = Process(target=producer, args=(q,))
    p2 = Process(target=producer, args=(q,))

    # 消费者们:即吃货们
    c1 = Process(target=consumer, args=(q,))
    c1.daemon = True  # 当生产者结束掉,消费者也没必要存在了。

    p1.start()
    p2.start()
    c1.start()

    p1.join()  # 保证生产者q已被取空
    p2.join()
    print('主')

总结:

1、程序中有两类角色

一类负责生产数据(生产者)
一类负责处理数据(消费者)

2、引入生产者消费者模型为了解决的问题是

平衡生产者与消费者之间的速度差
程序解开耦合

3、如何实现生产者消费者模型

生产者<--->队列<--->消费者

5 JoinableQueue队列

以上代码中如果有多个生产者和多个消费者时,我们则需要用一个很low的方式去解决;有几个消费者就需要发送几次结束信号:相当low

其实我们的思路无非是发送结束信号而已,有另外一种队列提供了这种机制

JoinableQueue:

JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
maxsize是队列中允许最大项数,省略则无大小限制。

方法介绍:

JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:

  • task_done()
q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
  • join()
q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
from multiprocessing import Process, JoinableQueue
import time, random, os

def consumer(q):
    while True:
        res = q.get()
        if res is None: break  # 收到结束信号则结束
        time.sleep(random.randint(1, 2))
        print('\033[45m%s 吃 %s\033[0m' % (os.getpid(), res))
        q.task_done()  # 通知生产者取走了一个数据

def producer(q):
    for i in range(3):
        time.sleep(random.randint(1, 2))
        res = '包子%s' % i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' % (os.getpid(), res))
    q.join()  # 等待队列中的数据被取完。就不等了。

if __name__ == '__main__':
    q = JoinableQueue()
    # 生产者们:即厨师们
    p1 = Process(target=producer, args=(q,))
    p2 = Process(target=producer, args=(q,))

    # 消费者们:即吃货们
    c1 = Process(target=consumer, args=(q,))
    c1.daemon = True  # 当生产者结束掉,消费者也没必要存在了。

    p1.start()
    p2.start()
    c1.start()

    p1.join()  # 保证生产者q已被取空
    p2.join()
    print('主')

6 管道Pipes

# Pipes
import time, multiprocessing


def task(conn):
    time.sleep(1)
    conn.send([11, 22, 33, 44])
    data = conn.recv()  # 阻塞
    print('子进程接收:', data)
    time.sleep(2)

    
if __name__ == '__main__':
    parent_conn, child_conn = multiprocessing.Pipe()
    p = multiprocessing.Process(target=task, args=(child_conn,))
    p.start()

    info = parent_conn.recv()  # 阻塞
    print('主进程接收:', info)
    parent_conn.send(666)

7 共享数据

展望未来,基于消息传递的并发编程是大势所趋;即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合;通过消息队列交换数据。这样极大地减少了对使用锁定和其他同步手段的需求;还可以扩展到分布式系统中

进程间通信应该尽量避免使用本节所讲的共享数据的方式

进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的

虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此

进程之间操作共享数据

from multiprocessing import Manager,Process,Lock
import os

def work(d,lock):
    # with lock: #不加锁而操作共享的数据,肯定会出现数据错乱
        d['count']-=1

if __name__ == '__main__':
    lock=Lock()
    with Manager() as m:
        dic=m.dict({'count':100})
        p_l=[]
        for i in range(100):
            p=Process(target=work,args=(dic,lock))
            p_l.append(p)
            p.start()
        for p in p_l:
            p.join()
        print(dic)
        #{'count': 94}

8 信号量

信号量Semahpore(同线程一样)(统一时间能进多少线程/进程)

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 , 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去, 如果指定信号量为3,那么来一个人获得一把锁,计数加1,当计数等于3时,后面的人均需要 等待。一旦释放,就有人可以获得一把锁

信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念

from multiprocessing import Process,Semaphore
import time,random

def go_wc(sem,user):
    sem.acquire()
    print('%s 占到一个茅坑' %user)
    time.sleep(random.randint(0,3)) #模拟每个人拉屎速度不一样,0代表有的人蹲下就起来了                        
    sem.release()
    
    """
    可以用上下文管理with
    with sem:
        print('%s 占到一个茅坑' %user)
        time.sleep(random.randint(1, 3))
	"""
    
if __name__ == '__main__':
    sem=Semaphore(3)
    p_l=[]
    for i in range(10):
        p=Process(target=go_wc,args=(sem,'user%s' %i,))
        p.start()
        p_l.append(p)

    for i in p_l:
        i.join()
    print('============》')

9 Event

Event(同线程一样)(说白了就是一个线程通知另一个线程)

python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。

事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 
event.wait([int]) 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。加参数可设置等待多少时间在执行,不用接收event.set()

clear:将“Flag”设置为False
set:将“Flag”设置为True

方法介绍:

from threading import Event

event.is_set():返回event的状态值

event.wait():如果event.is_set()==False将阻塞线程

event.set():设置event的状态值为True

event.clear():恢复event的状态值为False

用法:

import time
from threading import Thread, Event

event = Event()
# event.wait()  # 等待 
# event.set()  # 结束

def student(name):
    print('学生%s 正在听课' % name)
    event.wait()  # 等待。只要没有下发结束的信号,就一直等着
    print('学生%s 课间活动' % name)

def teacher(name):
    print('老师%s 正在上课' % name)
    time.sleep(7)
    event.set()  # 下发结束信号。

if __name__ == '__main__':
    stu1 = Thread(target=student, args=('alex',))
    stu2 = Thread(target=student, args=('wxx',))
    stu3 = Thread(target=student, args=('yxx',))
    t1 = Thread(target=teacher, args=('egon',))

    stu1.start()
    stu2.start()
    stu3.start()
    t1.start()

应用场景:

from threading import Thread, Event, currentThread
import time

event = Event()

def conn():
    n = 0
    while not event.is_set():  # 检查是否被设置
        if n == 3:
            print('%s try too many times' % currentThread().getName())
            return
        print('%s try %s ' % (currentThread().getName(), n))
        event.wait(2.5)  # 等待时长
        n+=1

    print('%s is connected' % currentThread().getName())

def check():
    print('%s is checking' % currentThread().getName())
    time.sleep(5)
    event.set()

if __name__ == '__main__':
    for i in range(3):
        t = Thread(target=conn)
        t.start()
    t = Thread(target=check)
    t.start()

10 进程池线程池

在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。多进程是实现并发的手段之一,需要注意的问题是:

1、很明显需要并发执行的任务通常要远大于核数

2、一个操作系统不可能无限开启进程,通常有几个核就开几个进程

3、进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行)

例如当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个。。。手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。

我们就可以通过维护一个进程池来控制进程数目,比如httpd的进程模式,规定最小进程数和最大进程数... ps:对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。

创建进程池的类:如果指定numprocess为3,则进程池会从无到有创建三个进程,然后自始至终使用这三个进程去执行所有任务,不会开启其他进程

Pool([numprocess  [,initializer [, initargs]]]):创建进程池

1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组

方法介绍:

  • apply
 p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()
  • apply_async
p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。
  • close()
p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
  • jion()
P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用

其他方法:

  • obj.get()
obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
  • obj.ready()
obj.ready():如果调用完成,返回True
  • obj.successful()
obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
  • obj.wait()
obj.wait([timeout]):等待结果变为可用
  • obj.terminate()
obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数

应用:

计算密集型用进程池;IO密集型用线程池

# 导入进程池和线程池
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
from threading import currentThread  # 查看线程名
import os,time,random

def task():
    print('name:%s pid:%s run' % (currentThread().getName(),os.getpid()))
    time.sleep(random.randint(1,3))

if __name__ == '__main__':
    # pool = ProcessPoolExecutor(4)  # 进程,参数不指定默认为cpu核数
    pool = ThreadPoolExecutor(5)  # 线程,参数指定开几个线程

    for i in range(10):
        pool.submit(task)  # 异步调用:调节完任务之后,不用再等着任务。
        # 执行拿到结果。不管任务有没有起来,也不拿结果.

    pool.shutdown(wait=True)  # 等着进程池完全执行完,默认就等于True

    print('主')

11 进程池&回调函数

  • 回调函数都是主进程来执行的
import time, multiprocessing
from concurrent.futures import ProcessPoolExecutor


def task(num):
    print('子进程', multiprocessing.current_process().pid)
    print('执行', num)
    time.sleep(2)
    return num


def done(res):
    print(multiprocessing.current_process().pid)
    time.sleep(1)
    print(res.result())
    time.sleep(1)


if __name__ == '__main__':
    pool = ProcessPoolExecutor(2)
    for i in range(6):
        fur = pool.submit(task, i)
        fur.add_done_callback(done)  # 回调函数都是主进程来执行的

    print(multiprocessing.current_process().pid)
    pool.shutdown(True)


12 进程池中使用锁

如果在进程池中要使用进程锁,则需要基于Manager中的Lock和RLock来实现

import time, multiprocessing
from concurrent.futures import ProcessPoolExecutor


def task(lock):
    print('开始')
    # lock.acquire()
    # lock.relase()
    with lock:
        # 假设文件中保存的内容就是一个值:10
        with open('mv.txt', mode='r', encoding='utf-8') as f:
            current_num = int(f.read())
        
        print('排队抢票了')
        time.sleep(1)
        current_num -= 1
        

if __name__ == '__main__':
    pool = ProcessPoolExecutor()
    # lock_obj = multiprocessing.RLock()
    manager = multiprocessing.Manager()
    lock_obj = manager.RLock()
    for i in range(10):
        pool.submit(task, lock_obj)


13 multiprocessiong模块操作进程

  • fork

    注意:“拷贝”几乎所有资源;支持文件对象/线程锁等传参;系统:只能运行在Unix;任意位置开始

import multiprocessing, time

def task():
    print(name)  # 此输出为[],拷贝主进程资源。当执行name.append(123)则会加入123
    name.append(123)

if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    name = []
    p1 = multiprocessing.Process(target=task)  # 不用传args参数,该模式下全拷贝父进程所有资源

    p1.start()
    time.sleep(2)
    print(name)  # 此输出空[],因为主进程中的name=[]
import multiprocessing, time
import threading

def task():
    print(file_object, lock)

if __name__ == '__main__':
    multiprocessing.set_start_method("fork")

    name = []
    file_object = open('mv.txt', mode='a+', encoding='utf-8')
    lock = threading.RLock()

    p1 = multiprocessing.Process(target=task)  # 也可以传值

    p1.start()
  • spawn

    注意:run参数传必备资源;不支持文件对象/线程锁等传参;系统:Unix、win;main代码块开始

import multiprocessing, time

def task(data):
    print(data)  # 此输出为[],拷贝主进程资源。当执行name.append(123)则会加入123
    data.append(999)

if __name__ == '__main__':
    multiprocessing.set_start_method("spawn")
    name = []
    p1 = multiprocessing.Process(target=task, args=(name,))  # 该模式下不会全拷贝父进程所有资源,需要传args参数

    p1.start()
    time.sleep(2)
    print(name)   # 此输出空[],因为主进程中的name=[]
import multiprocessing
import threading

def task():
    file_object = open('mv.txt', mode='a+', encoding='utf-8')
    lock = threading.RLock()
    print(file_object, lock)

if __name__ == '__main__':
    multiprocessing.set_start_method("spawn")

    name = []
    p1 = multiprocessing.Process(target=task)  # spawn模式,只能在子进程中自己重新写一份

    p1.start()
   
# 注意:线程锁需要子进程自己创建,而进程锁可以通过主进程args传参
  • forkserver

    注意:run参数传必备资源;不支持文件对象/线程锁等传参;系统:部分Unix;main代码块开始

import multiprocessing, time

def task(data):
    print(data)  # 此输出为[],拷贝主进程资源。当执行name.append(123)则会加入123
    data.append(999)

if __name__ == '__main__':
    multiprocessing.set_start_method("foerkserver")
    name = []
    p1 = multiprocessing.Process(target=task, args=(name,))  # 该模式下不会全拷贝父进程所有资源,需要传args参数

    p1.start()
    time.sleep(2)
    print(name)   # 此输出空[],因为主进程中的name=[]import multiprocessing
multiprocessing.set_start_method("forkserver")

线程理论篇

1 什么是线程

在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程

多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

创建线程的开销比创建进程的开销小


2 线程和进程的区别

线程是真正执行的单位,而进程是提供资源单位

1、线程共享创建它的进程的地址空间;进程有自己的地址空间。
2、线程可以直接访问其进程的数据段;进程有自己的父进程数据段的副本。
3、线程可以直接与其进程的其他线程通信;进程必须使用进程间通信与同级进程通信。
4、新线程易于创建;新进程需要父进程的重复。
5、线程可以对同一进程的线程进行相当大的控制;进程只能对子进程进行控制。
6、主线程的更改(取消、优先级更改等)可能会影响进程的其他线程的行为;对父进程的更改不会影响子进程。


3 为何要用多线程

多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

1、每启动一个进程,至少该进程内有一个线程。
2、进程本身是一个资源单位,并不能真正的执行,而进程内的线程才是真正执行的单位。
3、一个进程内,可以有多个线程,线程之间共享进程的资源;跨进程之间资源不共享。
4、创建一个进程需要申请内存空间,而开一个线程是基于已经申请到的空间,起一个线程,所以启进程的开销远远大于启线程的开销。


线程实操篇

1 threading模块

multiprocessing模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍


2 开启线程两种方式

方式一:

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    print('主线程')

"""
此时有两个线程
"""

方式二:

from threading import Thread
import time
class Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        time.sleep(2)
        print('%s say hello' % self.name)

if __name__ == '__main__':
    t = Sayhi('egon')
    t.start()
    print('主线程')

3 进程下开启多个线程和子进程的区别

1、同一进程下开启多个线程的速度最快,开销最小。

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello')

if __name__ == '__main__':
    #在主进程下开启线程
    t=Thread(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    hello
    主线程/主进程
    '''

    #在主进程下开启子进程
    t=Process(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    主线程/主进程
    hello
    '''

2、同一进程下,多个线程的pid都是一样的,因为同属于一个进程。而子进程之间相互隔离,pid也不一样。

from threading import Thread
from multiprocessing import Process,current_process
import os

def work():
    print('hello',os.getpid())  # os.getpid()可换成current_process().pid
                                    查看当前pid号

if __name__ == '__main__':
    #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid',os.getpid())# os.getpid()可换成current_process().pid


    #part2:开多个进程,每个进程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid',os.getpid())# os.getpid()可换成current_process().pid

3、同一进程下的线程共享该进程的资源,而子进程之间相互独立。

from  threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n=0

if __name__ == '__main__':
    # n=100
    # p=Process(target=work)
    # p.start()
    # p.join()
    # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自
                        己的,查看父进程的n仍然为100


    n=1
    t=Thread(target=work)
    t.start()
    t.join()
    print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据

4 threading属性和方法

Thread实例化对象方法

isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。

threading模块提供的一些方法

threading.currentThread(): 返回当前的线程变量。

threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。

threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

实操

from threading import Thread, currentThread, active_count,enumerate
from multiprocessing import Process
import os

def work():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())
    
# def task():
#   print('%s is running' % currentThread().getName())  
#   time.sleep(1)
#   print('%s is done' % currentThread().getName())


if __name__ == '__main__':
    #在主进程下开启线程
    t=Thread(target=work)
    t.start()
    
# if __name__ == '__main__':
#    t = Thread(target=task, name='子线程1')
#    t.start()
    
    
    # t.setName('儿子线程1')  # 修改子线程名字
    # print(t.getName())  # 查看子线程名字
    # currentThread().setName('主线程')  # 修改主线程名字
    # print(t.isAlive())  # 查看t线程是否存活
    # print(active_count())  # 查看总的线程存活数.
    # print(enumerate())  # 查看当前活跃的线程对象
    # print(len(threding.enumerate))  # 查看线程个数
    
    
    print(threading.current_thread().getName())  # 获取当前线程的名字
    print(threading.current_thread()) #主线程
    print(threading.enumerate()) #连同主线程在内有两个运行的线程
    print(threading.active_count())
    print('主线程/主进程')

    '''
    打印结果:
    MainThread
    <_MainThread(MainThread, started 140735268892672)>
    [<_MainThread(MainThread, started 140735268892672)>, 
        <Thread(Thread-1, started 123145307557888)>]
    主线程/主进程
    Thread-1
    '''

主线程等待子线程结束

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    t.join()  # 等待当前线程的任务执行完毕后在向下继续执行
    print('主线程')  
    print(t.is_alive())  # 检查该线程是否处于活动状态,即它是否仍在运行。
    '''
    egon say hello
    主线程
    False
    '''

5 守护线程

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

需要强调的是:运行完毕并非终止运行

假设:当一个进程中,总线程已经运行完毕,进程也不会关闭,因为该进程需要等着其他子线程运行完毕之后在结束该进程

1.对主进程来说,运行完毕指的是主进程代码运行完毕

2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会 一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束

主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主 线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都 运行完毕后才能结束。

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.setDaemon(True) # 必须在t.start()之前设置
    # t.daemon=True  # 跟t.setDaemon一个意思
    t.start()

    print('主线程')
    print(t.is_alive())  # 检查该线程是否处于活动状态,即它是否仍在运行。
    '''
    主线程
    True
    '''

迷惑人的例子

from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':
    t1=Thread(target=foo)
    t2=Thread(target=bar)

    t1.daemon=True
    t1.start()
    t2.start()
    print("main-------")

    """
    123
    456
    main------------
    end123
    end456
    """

6 互斥锁

原理:都是把并行变为串行,降低了效率,保证了数据安全。

互斥锁(mutex):精髓在与局部串行,只针对共享数据部分的修改,串行执行。

from threading import Thread
import os,time
n=100
def work():
    global n
    temp=n
    time.sleep(0.1)
    n=temp-1
if __name__ == '__main__':
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()
    print(n) #结果可能为99
# 当执行p.start时,线程1执行work函数,到time.sleep(0.1)等待0.1秒,有足够的时间另外99个线程也执行到此代码。所以在睡的时间中,都拿到了temp=n的值,所以接下来就是谁执行谁减1.最后度结果为99,导致数据不安全。

# 解决方法:
from threading import Thread, Lock
import time
n=100
def task():
    global n
    mutex.acquire()  # 加锁
    temp = n
    time.sleep(0.1) 
    n = temp - 1
    mutex.release()  # 解锁

if __name__ == '__main__':
    mutex = Lock()  # 生成一个锁
    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
    print('主', n)
# 当执行t.start时,线程1执行work函数,拿到锁执行到sleep,在等待0.1秒的时间中。其他99个线程在等线程1释放锁。所以大概执行时间为10秒。效率低但是数据安全了。

7 GIL全局解释器锁

GIL锁本质就是一把互斥锁,是CPython解释器特有的一个玩意,让一个进程中同一个时刻只能有一个线程可以被CPU调用。

GIL锁和互斥锁的区别:

GIL锁是解释器级别的锁
互斥锁是自己代码内的锁,

三个需要注意的点

1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来

2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高

3.一定要看本小节最后的GIL与互斥锁的经典分析

锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据,保护不同的数据就应该加不同的锁。

最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限

线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

既然是串行,那我们执行
t1.start()
t1.join
t2.start()
t2.join()
这也是串行执行啊,为何还要加Lock呢?需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。

因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的 线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你 自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量, py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好 又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了 解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动, 这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。

GIL的工作流程:

image

线程1,2,3要执行代码,需先抢到GIL的执行权限。当线程1抢到GIL执行权限,会把代码丢给解释器代码参数,在执行线程里代码,拿到mutex锁,当遇到阻塞。会被系统剥夺执行权限,释放GIL锁。然而线程内,并没有释放mutex锁。当进程2抢到GIL锁,会将线程代码丢给解释器代码参数,当线程2代码执行到mutex acquire ,发现线程1拿着mutex锁还没有释放,所以阻塞。又会被系统剥夺执行权限。线程3跟线程2一样,都不会执行。直到线程1需要再次执行,拿到GIL,从暂停位置,在执行以下代码,释放mutex 锁,最终在释放GIL锁。之线程2或者线程3拿到GIL锁,在次执行。

GIL总结:

1、一个进程内的多个线程,同一时间只有一个线程执行。(保证python的垃圾回收是线程安全的)
2、针对不同的数据应该加上不同的锁,GIL锁是解释器级别的锁(保护的是解释器级别的数据),不能保证其他线程代码的数据安全,针对代码本身的锁,需要自己加上互斥锁。

假设我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:

方案一:开启四个进程
方案二:一个进程下,开启四个线程

单核情况下,分析结果:

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

多核情况下,分析结果:

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

结论:

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

多线程性能测试:

如果并发的多个任务是计算密集型:多进程效率高

from multiprocessing import Process
from threading import Thread
import os, time

def work():
    res = 0
    for i in range(100000000):
        res *= i

if __name__ == '__main__':
    l = []
    print(os.cpu_count())  # 查询cpu核数
    start = time.time()

    for i in range(4):
        p = Process(target=work)  # 耗时17s多
        # p = Thread(target=work)  # 耗时28s多
        l.append(p)
        p.start()

    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop - start))

如果并发的多个任务是I/O密集型:多线程效率高

from multiprocessing import Process
from threading import Thread
import threading
import os, time

def work():
    time.sleep(2)
    print('===>')

if __name__ == ' __main__':
    l = []
    print(os.cpu_count())  # 本机为4核
    start = time.time()
    for i in range(10):
        p = Process(target=work)  # 耗时12s多,大部分时间耗费在创建进程上
        # p=Thread(target=work)  #耗时2s多
        l.append(p)
        p.start()

    for p in l:
        p.join()

    stop = time.time()
    print('run time is %s' % (stop - start))

8 死锁和递归锁

进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额

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

# 互斥锁:只能acquire一次
from threading import Thread, Lock
import time

mutexA = Lock()
mutexB = Lock()

class MyThead(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(2)

        mutexA.acquire()
        print('%s 拿到了A锁' % self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t = MyThead()
        t.start()
"""
Thread-1 拿到了A锁
Thread-1 拿到了B锁
Thread-1 拿到了B锁
Thread-2 拿到了A锁
然后就卡死了。。。
"""

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

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

mutexA=mutexB=threading.RLock() 
一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
# 递归锁:可以连续acquire多次,每acquire一次计时器+1,只要计数为零时,才能被抢到acquire
from threading import Thread, RLock
import time

mutexB = mutexA = RLock()

class MyThead(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(2)

        mutexA.acquire()
        print('%s 拿到了A锁' % self.name)
        mutexA.release()

        mutexB.release()

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

9 信号量

信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小

from threading import Thread, Semaphore, currentThread
import time, random

def task():
    # sm.acquire()
    # print('%s in ' % currentThread().getName())
    # sm.release()

    with sm:  # 上下文管理
        print('%s in ' % currentThread().getName())
        time.sleep(random.randint(1,3))

if __name__ == '__main__':
    sm = Semaphore(3)  # 信号量
    for i in range(10):
        t = Thread(target=task)
        t.start()

同进程的一样

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

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

from threading import Thread,Semaphore
import threading
import time
# def func():
#     if sm.acquire():
#         print (threading.currentThread().getName() + ' get semaphore')
#         time.sleep(2)
#         sm.release()
def func():
    sm.acquire()
    print('%s get sm' %threading.current_thread().getName())
    time.sleep(3)
    sm.release()
if __name__ == '__main__':
    sm=Semaphore(5)
    for i in range(23):
        t=Thread(target=func)
        t.start()

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


10 Event

同进程的一样

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

方法介绍:

  • event.isSet()
event.isSet():返回event的状态值;默认返回False
  • event.is_set()
event.is_set():判断是否被设置,默认返回False
  • event.wait()
event.wait():如果 event.isSet()==False将阻塞线程
  • event.set()
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
  • event.clear()
event.clear():恢复event的状态值为False。默认返回None

列如,高中学生在听课,老师在授课。当老师授完课,发送下课通知,学生在下课。

from threading import Thread, Event
import time

event = Event()
# event.wait()  等待set
# event.set()  等待结束


def student(name):
    print('学生%s 正在听课'% name)
    event.wait()
    print('学生%s 课间活动' % name)


def teacher(name):
    print('老师%s 正在授课'% name)
    time.sleep(6)
    event.set()


if __name__ == '__main__':
    stu1 = Thread(target=student, args=('alex',))
    stu2 = Thread(target=student, args=('wxx',))
    stu3 = Thread(target=student, args=('yxx',))

    t1 = Thread(target=teacher, args=('egon',))

    stu1.start()
    stu2.start()
    stu3.start()
    t1.start()

列如,大学学生在听课,老师在授课。当老师授课中。学生有事外出,可设置超时时间,不需要等待老师的set代码

from threading import Thread, Event
import time

event = Event()
# event.wait()  等待set
# event.set()  等待结束


def student(name):
    print('学生%s 正在听课'% name)
    event.wait(2)  # 设置超时时间,等2秒之后在执行代码。不需要收到老师set
    print('学生%s 课间活动' % name)


def teacher(name):
    print('老师%s 正在授课'% name)
    time.sleep(6)
    event.set()


if __name__ == '__main__':
    stu1 = Thread(target=student, args=('alex',))
    stu2 = Thread(target=student, args=('wxx',))
    stu3 = Thread(target=student, args=('yxx',))

    t1 = Thread(target=teacher, args=('egon',))

    stu1.start()
    stu2.start()
    stu3.start()
    t1.start()

应用:

from threading import Thread, Event, currentThread
import time

event = Event()

def conn():  # 连服务端
    n = 0
    while not event.is_set():  
        if n == 3:
            print('%s try too many times' % currentThread().getName())
            return
        print('%s try %s ' % (currentThread().getName(), n))
        event.wait(0.5)
        n+=1

    print('%s is connected' % currentThread().getName())

def check():  # 监测服务端是否正常运行
    print('%s is checking' % currentThread().getName())
    time.sleep(5)
    event.set()

if __name__ == '__main__':
    for i in range(3):
        t = Thread(target=conn)
        t.start()
    t = Thread(target=check)
    t.start()

11 定时器

定时器,指定n秒后执行某操作

from threading import Timer

def hello(name):
    print("hello, %s" % name)

if __name__ == '__main__':
    t = Timer(1, hello, args=('egon',))
    t.start()  # after 1 seconds, "hello, egon" will be printed

验证码定时器

from threading import Timer
import random,time

class Code:
    def __init__(self):
        self.make_cache()

    def make_cache(self,interval=5):
        self.cache=self.make_code()
        print(self.cache)
        self.t=Timer(interval,self.make_cache)
        self.t.start()

    def make_code(self,n=4):
        res=''
        for i in range(n):
            s1=str(random.randint(0,9))
            s2=chr(random.randint(65,90))
            res+=random.choice([s1,s2])
        return res

    def check(self):
        while True:
            inp=input('>>: ').strip()
            if inp.upper() ==  self.cache:
                print('验证成功',end='\n')
                self.t.cancel()
                break


if __name__ == '__main__':
    obj=Code()
    obj.check()

12 线程Queue

queue队列 :使用import queue,用法与进程Queue一样

  • class queue.Queue(maxsize=0) #先进先出
import queue

q = queue.Queue(3)  # 先进先出--> 队列
q.put('first')
q.put(2)
q.put('third')

# q.put(4, block=False)  # block默认True(阻塞),改为False(非阻塞)当队列满员时报错。
# q.put_nowait(4)  # 等同于q.put(block=False)
# q.put(4,block=True,timeout=3)  # 等待3秒,如果队列中值没有被取出一个,则3秒后报错

print(q.get())
print(q.get())
print(q.get())

# print(q.get(block=False))  # block默认True(阻塞),改为False(非阻塞)当队列值取超时报错。
# print(q.get_nowait())  # 等同于q.get(block=False)
# print(q.get(block=True, timeout=3))  # 等待3秒,如果队列中没有被传值,则3秒后报错

    输出:
first
2
third
  • class queue.LifoQueue(maxsize=0) #last in fisrt out
import queue

q=queue.LifoQueue()  # 后进先出-->堆栈
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())

'''
结果(后进先出):
third
second
first
'''
  • class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列
import queue

q=queue.PriorityQueue()  # 优先级队列(数字越小优先级越高)
#put进入一个元组或者列表,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),

    
q.put((10, 'one'))  # 位置1:优先级  位置2:需要传的值
q.put((40, 'two'))  # 位置1:优先级  位置2:需要传的值
q.put((30, 'three'))  # 位置1:优先级  位置2:需要传的值

print(q.get())
print(q.get())
print(q.get())

'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'one')
(30, 'three')
(40, 'two')
'''

13 进程池线程池

concurrent.futures模块提供了高度封装的异步调用接口

ThreadPoolExecutor:线程池,提供异步调用

ProcessPoolExecutor: 进程池,提供异步调用

# 导入模块
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

基本方法:

  • submit()
submit(func, *args, **kwargs) # 异步提交任务
  • map()
map(func, *iterables, timeout=None, chunksize=1) #取代for循环submit的操作
  • shutdown()
shutdown(wait=True) 
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前
  • result()
result(timeout=None) # 取得结果
  • add_done_callback()
add_done_callback(fn) # 回调函数

进程池

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

def task(name):
    print('name:%s pid:%s run' % (name,os.getpid()))
    time.sleep(random.randint(1,3))

if __name__ == '__main__':
    pool = ProcessPoolExecutor(4)  # 4不指定默认为cpu核数
    
    for i in range(10):
        pool.submit(task, 'egon%s' % i)  
        # 异步调用:调用完任务之后,不用再等着任务执行拿到结果。
        # 不管任务有没有起来,也不拿结果,只是提交

    pool.shutdown(wait=True)  # 等着进程池完全执行完

    print('主')

线程池

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
from threading import currentThread
import os,time,random

def task():
    print('name:%s pid:%s run' % (currentThread().getName(),os.getpid()))
    time.sleep(random.randint(1,3))

if __name__ == '__main__':
    pool = ThreadPoolExecutor(5)  # 线程

    for i in range(10):
        pool.submit(task)  
        # 异步调用:调用完任务之后,不用再等着任务执行拿到结果。
        # 不管任务有没有起来,也不拿结果,只是提交

    pool.shutdown(wait=True)  # 等着线程池完全执行完

    print('主')

map的用法

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

import os,time,random
def task(n):
    print('%s is runing' %os.getpid())
    time.sleep(random.randint(1,3))
    return n**2

if __name__ == '__main__':

    executor=ThreadPoolExecutor(max_workers=3)

    # for i in range(11):
    #     future=executor.submit(task,i)

    executor.map(task,range(1,12)) # map取代了for+submit

14 同步/异步调用与回调机制

提交任务的两种方式

1、同步调用:提交完任务后,就在原地等待任务执行完毕,拿到结果,在执行下一行代码导致程序是串行执行。

import time, random
from concurrent.futures import ThreadPoolExecutor

def la(name):
    print('%s is laing' % name)
    time.sleep(random.randint(1,3))
    res = random.randint(7,13)*"#"
    return {'name': name, 'res': res}

def weigh(shit):
    name = shit['name']
    size = len(shit['res'])
    print('%s 拉了 《%s》kg' % (name,size))

if __name__ == '__main__':
    pool = ThreadPoolExecutor(13)
    shit1 = pool.submit(la, 'alex').result()  # .result()在原地等待拿到结果
    weigh(shit1)
    shit2 = pool.submit(la, 'wxx').result()  # .result()在原地等待拿到结果
    weigh(shit2)
    shit3 = pool.submit(la, 'yuanhao').result()  # .result()在原地等待拿到结果
    weigh(shit3)

2、异步调用:提交完任务后,不在等待任务执行完毕

import time, random
from concurrent.futures import ThreadPoolExecutor

def la(name):
    print('%s is laing' % name)
    time.sleep(random.randint(1,3))
    res = random.randint(7,13)*"#"
    return {'name': name, 'res': res}
    # weigh({'name': name, 'res': res})

def weigh(shit):
    shit = shit.result()
    name = shit['name']
    size = len(shit['res'])
    print('%s 拉了 《%s》kg' % (name,size))

if __name__ == '__main__':
    pool = ThreadPoolExecutor(13)
    pool.submit(la, 'alex').add_done_callback(weigh) 
    # 绑定一个回调函数。当pool.submit(la, 'alex')
    # 执行完返回值了。在执行.add_done_callback(weigh)

    pool.submit(la, 'wxx').add_done_callback(weigh)

    pool.submit(la, 'yuanhao').add_done_callback(weigh)

同步调用和阻塞区别:

同步任务是一个提交任务的方式,提交完任务在原地等着任务执行完毕,不管该任务是不是计算密集型还是IO密集型,都会在原地等待执行结果。

阻塞当遇到IO时,系统会调回cpu执行权限。


并发之协程

本节的主题是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发,为此我们需要先回顾下并发的本质:切换+保存状态

cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它

image

ps:在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态

一:其中第二种情况并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。为此我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法,我们来简单复习一下:

1 yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
2 send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换

单纯地切换反而会降低运行效率

#串行执行
import time

def func1():
    for i in range(10000000):
        i+1

def func2():
    for i in range(10000000):
        i+1

start = time.time()
func1()
func2()
stop = time.time()
print(stop - start)

# 基于yield并发执行
import time
def func1():
    while True:
        yield

def func2():
    g=func1()
    for i in range(10000000):
        i+1
        next(g)

start=time.time()
func2()
stop=time.time()
print(stop-start)


二:第一种情况的切换。在任务一遇到io情况下,切到任务二去执行,这样就可以利用任务一阻塞的时间完成任务二的计算,效率的提升就在于此。

yield不能检测IO,实现遇到IO不会自动切换

import time
def func1():
    while True:
        print('func1')
        yield

def func2():
    g=func1()
    for i in range(10000000):
        i+1
        next(g)
        time.sleep(3)
        print('func2')
start=time.time()
func2()
stop=time.time()

对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。

2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换

1 协程介绍

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

需要强调的是:

1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)

2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

对比操作系统控制线程的切换,用户在单线程内控制协程的切换

优点如下:

1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu

缺点如下:

1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结协程特点:

1. 必须在只有一个单线程里实现并发
2. 修改共享数据不需加锁
3. 用户程序里自己保存多个控制流的上下文栈
4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实	现,就用到了gevent模块(select机制))

2 greenlet模块

如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦),而使用greenlet模块可以非常简单地实现这20个任务直接的切换

安装greenlet模块

pip3 install greenlet
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时传入参数,以后都不需要

greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

pip3 install greenlet
from greenlet import greenlet
import time

def eat(name):
    print('%s eat 1' %name)
    time.sleep(10)  # 遇到IO就会卡在原地,不会切换。
    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时传入参数,以后都不需要
"""
注意:
此方法遇到IO不能自动切换执行
"""

单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。。如此,才能提高效率,这就用到了Gevent模块。


3 gevent模块

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

方法介绍:

  • gevent.spwan()
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
  • join()
g1.join() # 等待g1结束
  • joinall
g1.join() #等待g1结束
g2.join() #等待g2结束

或者上述两步合作一步:gevent.joinall([g1,g2])
  • value
g1.value#拿到func1的返回值

安装

pip3 install gevent

遇到IO阻塞时会自动切换任务

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('主')

上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

import gevent, time
from gevent import monkey; monkey.patch_all()   # 监测所有IO操作

def eat(name):
    print('%s eat 1' % name)
    time.sleep(3)
    print('%s eat 2' % name)

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

g1 = gevent.spawn(eat, 'egon')
g2 = gevent.spawn(play, 'alex')
# 因为是异步,所以执行完以上代码,主线程就会结束掉。
# 解决方法:
# 方法一:
# time.sleep(5)  # 此方法不推荐,实际应用中,压根不知道多少秒

# 方法二:
# g1.join()
# g2.join()

# 方法三:
gevent.joinall([g1, g2])  # 相当于方法二

我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程


4 协程、线程、进程的区别

线程,是计算机中可以被cpu调度的最小单元。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

	- 由于cPython中GIL的存在
	- 线程,适用于io密集型操作。–进程,适用于计算密集型操作。

协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到io自动切换,就可以通过一个线程实现并发操作。

所以,在处理io操作时,协程比线程更加节省开销(协程的开发难度大一些)。

IO模型

1 IO模型介绍

为了更好地了解IO模型,我们需要事先回顾下:同步、异步、阻塞、非阻塞

Stevens在文章中一共比较了五种IO Model:  
* blocking IO  
* nonblocking IO  
* IO multiplexing  
* signal driven IO  
* asynchronous IO  
由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。

2 阻塞IO

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

image

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。

而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,
然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

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

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

image

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

遇到阻塞操作系统就会调离执行权限,原地等待,什么时候有链接来,在执行。

from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8090))
server.listen(5)

while 1:
    print('starting...')
    conn, addr = server.accept()
    print(addr)

    while 1:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()
server.close()

# 客户端
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8090))

while 1:
    msg = input(">>:").strip()
    if not msg: continue

    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))

一个简单的解决方案:

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

该方案的问题是:

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

改进方案:

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

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

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

3 非阻塞IO

Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

image

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。

也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,
此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,
循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,
进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有,会极大的消耗cpu性能,一直在不停的循环做事。

#服务端
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8099))
server.listen(5)
server.setblocking(False)  # 默认True阻塞,False非阻塞


rlist=[]
wlist=[]
while True:
    try:
        conn, addr = server.accept()  # 不阻塞了
        rlist.append(conn)
        print(rlist)

    except BlockingIOError:
        # 干其他的活
        
        # 收消息
        del_rlist=[]
        for sock in rlist:
            try:
                data=sock.recv(1024)
                if not data:
                    del_rlist.append(sock)
                wlist.append((sock,data.upper()))
            except BlockingIOError:
                continue
            except Exception:
                sock.close()
                del_rlist.append(sock)
		
        # 发消息
        del_wlist=[]
        for item in wlist:
            try:
                sock = item[0]
                data = item[1]
                sock.send(data)
                del_wlist.append(item)
            except BlockingIOError:
                pass

        for item in del_wlist:
            wlist.remove(item)


        for sock in del_rlist:
            rlist.remove(sock)

server.close()


#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8080))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))

但是非阻塞IO模型绝不被推荐。

我们不能否则其优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。

但是也难掩其缺点:

1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。
这会导致整体数据吞吐量的降低。

此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。


4 IO多路复用

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO

(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

image

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,
当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。  
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用\(select和recvfrom\),
而blocking IO只调用了一个系统调用\(recvfrom\)。但是,用select的优势在于它可以同时处理多个connection。

强调:

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

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

结论: select的优势在于可以处理多个连接,不适用于单个连接

# 服务端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8093))
server.listen(5)
server.setblocking(False)
print('starting...')

rlist=[server,]  # 收套接字的列表(初始只有一个server)
wlist=[]
wdata={}

while True:
    rl,wl,xl=select.select(rlist,wlist,[],0.5)  # 接收,发送,异常,超时时间
    print(wl)
    for sock in rl:
        if sock == server:
            conn,addr=sock.accept()
            rlist.append(conn)
        else:
            try:
                data=sock.recv(1024)
                if not data:
                    sock.close()
                    rlist.remove(sock)
                    continue
                wlist.append(sock)
                wdata[sock]=data.upper()
            except Exception:
                sock.close()
                rlist.remove(sock)

    for sock in wl:
        sock.send(wdata[sock])
        wlist.remove(sock)
        wdata.pop(sock)

# 客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8081))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))

select监听fd变化的过程分析:

用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,
就会发送信号给用户进程数据已到;
用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,
这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。

该模型的优点:

相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。
如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

该模型的缺点:

首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。
很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,
所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。
其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

5 异步IO

Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:

image

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。


serlectors模块

这三种IO多路复用模型在不同的平台有着不同的支持,而epoll在windows下就不支持,好在我们有selectors模块,帮我们默认选择当前平台下最合适的

1 单用户聊天

单线程的聊天,并不支持多用户聊天

# 服务器
from socket import *

tcp_socket_server = socket()
tcp_socket_server.bind(('127.0.0.1', 8090))
tcp_socket_server.listen(5)

while 1:
    conn, addr = tcp_socket_server.accept()
    print('客户端:', addr)
    while 1:
        client_data = conn.recv(1024)
        print(client_data.decode('utf-8'))
        if client_data.decode('utf-8') == "exit":
            print('客户端端口连接,等待新的用户...')
            break
        print('接受数据>>:', str(client_data, 'utf-8'))
        response = input('响应数据>>>')
        conn.sendall(bytes(response, 'utf-8'))

    conn.close()

# 客户端
import socket
sock = socket.socket()
sock.connect(('127.0.0.1', 8090))
print('客户端启动:')

while True:
    inp = input('发送的数据>>>')
    sock.sendall(bytes(inp, 'utf8'))
    if inp == 'exit':
        break
    server_response = sock.recv(1024)
    print('服务器响应数据>>>', str(server_response, 'utf8'))

sock.close()

2 多用户聊天

socketserver使用模式:

第一步:功能类
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):  
        ...
        
第二步:
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8090), MyServer) 
帮我们实现了,self.socket; self.socket.bind(); self.socket.listen(5)

第三步:
server.serve_forever()

ThreadingTCPServer:开启多线程的tcp套接字
ThreadingUDPServer:开启多线程的udp套接字
ForkingTCPServer:开启多进程的tcp套接字
ForkingUDPServer:开启多进程的udp套接字
# 服务端
import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self) -> None:  # 固定函数
        """
        conn:self.request
        """
        while 1:
            client_data = self.request.recv(1024)
            print(client_data.decode('utf-8'))
            if client_data.decode('utf-8') == "exit":
                print('客户端端口连接,等待新的用户...')
                break
            print('接受数据>>:', str(client_data, 'utf-8'))
            response = input('响应数据>>>')
            self.request.sendall(bytes(response, 'utf-8'))

        self.request.close()

server = socketserver.ThreadingTCPServer(('127.0.0.1', 8091), MyServer)  # 多线程实现并发
server.serve_forever()


# 客户端
import socket
sock = socket.socket()
sock.connect(('127.0.0.1', 8090))
print('客户端启动:')

while True:
    inp = input('发送的数据>>>')
    sock.sendall(bytes(inp, 'utf8'))
    if inp == 'exit':
        break
    server_response = sock.recv(1024)
    print('服务器响应数据>>>', str(server_response, 'utf8'))

sock.close()

套接字通迅

基于多进程实现并发套接字通讯

server:

from socket import *
from multiprocessing import Process

def talk(conn):
    while 1:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

def server(ip, port):
    server = socket(AF_INET, SOCK_STREAM)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind((ip, port))
    server.listen(5)

    while 1:
        conn, addr = server.accept()
        p = Process(target=talk, args=(conn, ))
        p.start()

    server.close()

if __name__ == '__main__':
    server('127.0.0.1', 8090)

client:

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8090))

while 1:
    msg = input(">>:").strip()
    if not msg: continue

    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))

基于多线程实现并发套接字通讯

server:

from socket import *
from threading import Thread

def talk(conn):
    while 1:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

def server(ip, port):
    server = socket(AF_INET, SOCK_STREAM)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind((ip, port))
    server.listen(5)

    while 1:
        conn, addr = server.accept()
        t = Thread(target=talk, args=(conn, ))
        t.start()

    server.close()

if __name__ == '__main__':
    server('127.0.0.1', 8090)

client:

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8090))

while 1:
    msg = input(">>:").strip()
    if not msg: continue

    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))

进程池线程池的套接字通讯

server:

from socket import *
from concurrent.futures import ThreadPoolExecutor

def talk(conn):
    while 1:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

def server(ip, port):
    server = socket(AF_INET, SOCK_STREAM)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind((ip, port))
    server.listen(5)

    while 1:
        conn, addr = server.accept()
        pool.submit(talk, conn)

    server.close()

if __name__ == '__main__':
    pool = ThreadPoolExecutor(2)  # 最多只允许2个
    server('127.0.0.1', 8090)

client:

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8090))

while 1:
    msg = input(">>:").strip()
    if not msg: continue

    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))

gevent实现并发套接字通讯

server:

from socket import *
from gevent import monkey, spawn; monkey.patch_all()

def talk(conn):
    while 1:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

def server(ip, port):
    server = socket(AF_INET, SOCK_STREAM)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind((ip, port))
    server.listen(5)

    while 1:
        conn, addr = server.accept()
        spawn(talk, conn)

    server.close()

if __name__ == '__main__':
    g = spawn(server('127.0.0.1', 8090))
    g.join()

client:

from socket import *
from threading import Thread, currentThread

def client():
    client = socket(AF_INET, SOCK_STREAM)
    client.connect(('127.0.0.1', 8090))
    while 1:
        client.send(('%s hello' % currentThread().getName()).encode('utf-8'))
        data = client.recv(1024)
        print(data.decode('utf-8'))

    client.close()

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

单例模式

面向对象 + 多线程相关的一个面试题(以后项目和源代码中会用到)

之前写一个类,每次执行() 都会实例化一个对象。

class Foo:
    pass
obj1 = Foo()
obj2 = Foo()
print(obj1, obj2)
# 每次实例化的对象内存地址都是不一样的

以后开发会遇到单例模式,每次实例化类的对象时,都是最开始创建的那个对象,不在重复创建对象。

  • 简单的实现单例模式
import threading

class Singleton(object):
    instance = None

    def __init__(self, name):
        self.name = name

    def __new__(cls, *args, **kwargs):
        # 返回空对象
        if cls.instance:
            return cls.instance

        cls.instance = object.__new__(cls)  # 创建一个空对象
        return cls.instance


obj1 = Singleton('alex')
print(obj1)

obj2 = Singleton('alex')
print(obj2)
# obj1,obj2, 内存地址都是一样的



完美的线程单例模式(背会)

# 上述单例模式中在多线程的模式下就会出问题,解决方案如下(背会):
import threading
import time

class Singleton(object):
    instance = None
    lock = threading.RLock()

    def __init__(self, name):
        self.name = name

    def __new__(cls, *args, **kwargs):
        # 返回空对象
        if cls.instance:
            return cls.instance
        with cls.lock:
            if cls.instance:
                return cls.instance
            # time.sleep(0.1)  # 在真实开发中,不会写这个(自己测试用)
            cls.instance = object.__new__(cls)  # 创建一个空对象
            return cls.instance

def task():
    obj = Singleton('x')
    print(obj)

for i in range(10):
    t = threading.Thread(target=task)
    t.start()

阶段总结

1 并发编程 & 网络编程

  • 案例一:多线程socket服务器
# 服务端
import socket, threading


def task(conn):
    while True:
        client_data = conn.recv(1024)
        data = client_data.decode('utf-8')
        print('收到客户端发来的消息:', data)
        if data.upper() == 'Q':
            break
        conn.sendall('收到'.encode('utf-8'))
    conn.close()


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8090))
    sock.listen(5)

    while True:
        # 等待客户端连接
        conn, addr = sock.accept()
        t = threading.Thread(target=task, args=(conn,))
        t.start()

    sock.close()


if __name__ == '__main__':
    run()
# 客户端
import socket

# 1.向指定IP发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8090))

while True:
    txt = input(">>>:")
    client.sendall(txt.encode('utf-8'))
    if txt.upper() == "Q":
        break
    reply = client.recv(1024)
    print(reply.decode('utf-8'))

# 关闭连接,关闭连接时会向服务端发送空数据
client.close()

  • 多进程socket服务端
# 服务端
import socket, multiprocessing


def task(conn):
    while True:
        client_data = conn.recv(1024)
        data = client_data.decode('utf-8')
        print('收到客户端发来的消息:', data)
        if data.upper() == 'Q':
            break
        conn.sendall('收到'.encode('utf-8'))
    conn.close()


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8090))
    sock.listen(5)

    while True:
        # 等待客户端连接
        conn, addr = sock.accept()
        t = multiprocessing.Process(target=task, args=(conn,))
        t.start()

    sock.close()


if __name__ == '__main__':
    run()
import socket

# 1.向指定IP发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8090))

while True:
    txt = input(">>>:")
    client.sendall(txt.encode('utf-8'))
    if txt.upper() == "Q":
        break
    reply = client.recv(1024)
    print(reply.decode('utf-8'))

# 关闭连接,关闭连接时会向服务端发送空数据
client.close()


2 串行、并发和并行

  • 串行,多个任务排队按照先后顺序逐—去执行。

  • 并发,假设有多个任务,只有一个CPU,那么在同一时刻只能处理一个任务,为了避免串行,可以让将任务切换运行(每个任务运行一点,然后再切换),达到并发效果(看似都在同时运行)。

    并发在Python代码中体现:协程、多线程(由cPython的GIL锁限制,多个线程无法被CPu调度)。
    
  • 并行,假设有多个任务,有多个CPU,那么同一时刻每个CPU都是执行一个任务,任务就可以真正的同时运行。

    并行在Python代码中的体现:多进程。
    
posted @ 2022-09-22 16:53  角角边  Views(59)  Comments(0)    收藏  举报