第二十章:并发编程

并发编程

为什么需要引入并发编程呢

场景1:一个网络爬虫,按顺序爬取花了一小时,采用并发下载减少到20 min

场景2:一个app 应用,优化前每次打开页面需要3s,采用异步并发提升到每次200ms;引入并发,就是为了提升程序运行的速度

相关理论

多道技术

在并发编程的过程中,不做刻意提醒的情况下,默认一台计算机就一个CPU(只有一个干活的人)

单道技术
	所有的程序排队执行,过程中不能重合
多道技术
	利用空闲时间提前准备其他数据,最大化提升CPU利用率
  
多道技术详细
	1.切换
	计算机的CPU在两种情况下会切换(不让你用 给别人用)
		1.程序有IO操作
			输入\输出操作
			input、time.sleep、read、write
		2.程序长时间占用CPU	
			我们得雨露均沾 让多个程序都能被CPU运行一下 

	2.保存状态
		CPU每次切换走之前都需要保存当前操作的状态 下次切换回来基于上次的进度继续执行

进程理论

进程与程序的区别
	程序:一堆死代码(还没有被运行起来)
	进程:正在运行的程序(被运行起来了)
 
进程的调度算法(重要)
	1.FCFS(先来,先服务)
  		对短作业不友好
	2.短作业优先调度
  		对长作业不友好
	3.时间片轮转法 + 多级反馈队列(目前还在用)
  		将时间均分,然后根据进程时间长短再分多个等级
    	等级越靠下表示耗时越长,每次分到的时间越多,但是优先级越低

并行与并发

1.并行(parallel)
同一时刻有多个事情在同时进行(真同时并非时间切片)。多个进程同时执行,必须要有多个CPU参与 单个CPU无法实现并行。
 
2.并发(concurrency)
并发着重于发,即发生。在某个时刻或者某段时间,同时发生了很多需要处理的请求
多个进程看上去像同时执行,单个CPU可以实现,多个CPU肯定也可以。

进程的三状态

就绪态
	所有的进程在被CPU执行之前都必须先进入就绪态等待
运行态
	CPU正在执行
阻塞态
	进程运行过程中出现了IO操作,阻塞态无法直接进入运行态,需要先进入就绪态

同步与异步

同步:执行 IO 操作时,必须等待执行完成才得到返回结果。

异步:执行 IO 操作时,不必等待执行就能得到返回结果。期间去做其他事,有结果自动通知。

阻塞与非阻塞

函数或方法调用的时候,是否立即返回。

立即返回就是非阻塞调用。
不立即返回就是阻塞调用。

阻塞和非阻塞关注得是程序在等待调用结果(消息,返回值)时的状态

1、阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
2、非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

僵尸进程与孤儿进程

linux 系统中:

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。
孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
僵尸进程因为资源不会完全释放,因此有可能会造成资源泄漏,但是孤儿进程不会。

危害

  1. 僵尸进程会占用系统资源,过多的僵尸进程可能会影响服务器性能
  2. 孤儿进程没啥危害,孤儿进程还活着,被送进了孤儿院(linux 系统中 init 进程管理,系统会定期进行处理)

如何避免

​ 父进程调用 wait 或 waitpid 等待子进程结束

认识进程

主要参数

from multiprocessing import Process

p = Process(target=func, )      # 实例化
p.pid                           # 查看 pid
p.start()                       # 启动进程
p.join()                        # 让主进程检测子进程是否运行完毕
p.name()                        # 查看进程名
p.terminate()                   # 终止进程 
p.is_alive()                    # 查看进程是否存活,返回值:True、False。
os.getpid()                     # 参看pid
os.getppid()                    # 查看ppid,父级

创建的两种方式

import os
from multiprocessing import Process


# 方法一
def myprocess():
    print(f'子线程:{os.getpid()}   父线程:{os.getppid()}')    # 子线程:21536   父线程:22000


if __name__ == '__main__':
    p = Process(target=myprocess,)
    p.start()
    print(f'父线程:{os.getpid()}')     # 父线程:22000


# 方法二
class Myprocess(Process):
    def __init__(self):
        super().__init__()

    def run(self):
        print(f'子线程:{os.getpid()}   父线程:{os.getppid()}')    # 子线程:13620   父线程:8976

if __name__ == '__main__':
    m = Myprocess()
    m.start()
    print(f'父线程:{os.getpid()}')     # 父线程:8976

进程 join 方法

使用 join 可以将并发变成串行,主进程等待子进程终止,主进程处于等待的状态。

import os
import time
from multiprocessing import Process

def myprocess():
    print('开启子进程...')
    time.sleep(3)
    print(f'结束子进程:{os.getpid()}')



if __name__ == '__main__':
    p = Process(target=myprocess,)
    p.start()
    p.join()
    print(f'父线程:{os.getpid()}')     # 父线程:22000

# 添加 join() 的执行结果
# 开启子进程...
# 结束子进程:23076
# 父线程:22948

# 不添加 join() 的执行结果
# 父线程:10628
# 开启子进程...
# 结束子进程:22748

进程间数据隔离

from multiprocessing import Process
import time

money = 1000

def task():
    # global money
    money = 666
    print('子进程', money) # 子进程 666


if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start()  # 创建子进程
    # time.sleep(3)  # 主进程代码等待3秒
    print(f'主进程:{money}')  # 主进程:1000

守护进程

关于守护进程需要强调两点

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

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

def foo():
    time.sleep(1)
    print('foo')
    pass


def run():
    print('this is 子进程')
    time.sleep(2)
    # p1 = Process(target=foo,)   # AssertionError: daemonic processes are not allowed to have children
    # p1.start()
    print('this is 子进程 end')


if __name__ == '__main__':
    p = Process(target=run,)
    p.daemon = True             # 守护进程要主进程执行完毕前开启
    p.start()
    p.join()
    print('主进程')

队列

进程间通信之IPC机制

IPC:进程间通信
消息队列:存储数据的地方,所有人都可以存,也都可以取

主要参数

注意:当队列已满(为空)时,再向队列中放入(取出)值,系统会卡在,等待队列取出(放入)值。

q.put():用以插入数据到队列中。
q.get():可以从队列读取并且删除一个元素。
q.full():判断队列是否已满,返回值:True、False。
q.empty():判断队列是否为空,返回值:True、False。

队列的介绍

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

Queue([maxsize]):创建共享的进程队列,Queue 是多进程安全的队列,可以使用 Queue 实现多进程之间的数据传递。

maxsize 是队列中允许最大项数,省略则无大小限制。
但需要明确:
1、队列内存放的是消息而非大数据
2、队列占用的是内存空间,因而 maxsize 即便是无大小限制也受限于内存大小

from multiprocessing import Process, Queue


def get(q):
    # 取数据
    ret = q.get()
    print(f'取数据:{ret}')     # 取数据:床前明月光

def put(q):
    # 存数据
    q.put('床前明月光')


if __name__ == '__main__':
    q = Queue(3)
    p1 = Process(target=put, args = (q,))
    p2 = Process(target=get, args = (q,))
    p1.start()
    p2.start()

生产者消费者模型

1、程序中有两类角色
	一类负责生产数据(生产者)
	一类负责处理数据(消费者)

2、引入生产者消费者模型为了解决的问题是
	平衡生产者与消费者之间的速度差
	程序解开耦合

3、如何实现生产者消费者模型
	生产者 <---> 队列 <---> 消费者
import time
import random
from multiprocessing import Process, Queue


def producer(q, food):
    '''
    生产者
    :return:
    '''
    for i in range(10):
        time.sleep(random.random())
        q.put(f'生产 {food} {i}')


def consumer(q, pen):
    '''
    消费者
    :return:
    '''
    while 1:
        ret = q.get()
        time.sleep(random.random())
        if not ret: break
        print(f'消费者:{pen} {ret}')


if __name__ == '__main__':
    q = Queue()
    food = '包子'
    food2 = '油条'
    food3 = '饼'

    pen = 'ysg'
    pen2 = 'ysg2'

    # 生产者
    scz = Process(target=producer, args=(q, food))
    scz2 = Process(target=producer, args=(q, food2))
    scz3 = Process(target=producer, args=(q, food3))

    # 消费者
    xfz = Process(target=consumer, args=(q, pen))
    xfz2 = Process(target=consumer, args=(q, pen2))

    s_lit = [scz, scz2, scz3]
    c_lit = [xfz, xfz2]
    for i in s_lit:
        i.start()

    for i in c_lit:
        i.start()

    for i in s_lit:
        i.join()

    q.put(None)
    q.put(None)
    print('-----主进程-----')

互斥锁

使用 join 可以将并发变成串行,互斥锁的原理也是将并发变成穿行,那我们直接使用join就可以了啊,为何还要互斥锁?
说到这里我赶紧试了一下。

join 是将一个任务整体串行,而互斥锁的好处则是可以将一个任务中的某一段代码串行,比如只让run 函数中的 buy_ticket 任务串行。

import time
import random
from multiprocessing import Process, Lock


dit = {'ticket': 1}


def show(name):
    time.sleep(random.random())
    print(f"{name} 查看余票:{dit['ticket']}")


def buy_ticket(name):
    if dit['ticket'] > 0:
        time.sleep(random.random())
        print(f'{name} 买票成功! \n')
        dit['ticket'] -= 1


def run(name, lock):
    show(name)
    lock.acquire()
    buy_ticket(name)
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    for i in range(10):
        name = f'ysg {i}  '
        p = Process(target=run, args=(name, lock))
        p.start()

认识线程

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

线程顾名思义,就是一条流水线工作的过程(流水线的工作需要电源,电源就相当于cpu),而一条流水线必须属于一个车间,一个车间的工作过程是一个进程,车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一条流水线。

所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

多线程(即多个控制线程)的概念是,在一个进程中存在多个线程,多个线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。

总结上述区别,无非两个关键点,这也是我们在特定的场景下需要使用多线程的原因:

1、同一个进程内的多个线程共享该进程内的地址资源
2、创建线程的开销要远小于创建进程的开销(创建一个进程,就是创建一个车间,涉及到申请空间,而且在该空间内建至少一条流水线,但创建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小)

创建线程的两种方式

方法一

from threading import Thread
import time
import random


def thread(name):
    print('this is 线程 %s' % name)
    time.sleep(random.randrange(1, 3))
    print('end')

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

方法二

from threading import Thread
import time
import random


class Threads(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print('this is thread')
        time.sleep(random.randrange(1, 3))
        print('end')

if __name__ == '__main__':
    t = Threads('ysg')
    t.start()
    print('主线程')

Thread 对象的其他属性或方法

Thread实例对象的方法

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

    
threading模块提供的一些方法

threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread, currentThread, enumerate,active_count
import time

def func():
    print('Name:%s' % currentThread().getName())
    time.sleep(3)
    print('end')


if __name__ == '__main__':
    t = Thread(target=func, name='线程 1')    # 开启线程时,设置线程名;存在默认值
    t.start()
    print(t.getName())          # 获取线程名;结果:线程 1
    t.setName('线程 A')         # 修改线程名
    print(t.getName())          # 结果:线程 A
    print(enumerate())          # 以列表的方式获取线程信息;结果:[<_MainThread(MainThread, started 2436)>, <Thread(线程 A, started 8292)>]
    print(t.is_alive())         # 判断线程是否存活,返回值 True、False;结果:True
    print(active_count())       # 获取存活进程数;结果:2

守护线程

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

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

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

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

详细解释:

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

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

单线程

from threading import Thread
import time

def func():
    time.sleep(1)
    print('123')


if __name__ == '__main__':
    t = Thread(target=func,)
    t.daemon = True     # 必须在t.start()之前设置,t.setDaemon(True) 两者效果一致
    t.start()
    print('主线程')
    print(t.isAlive())

# 执行结果
# 123
# True

多线程

def func():
    print('123')
    time.sleep(1)
    print('end 123')

def func2():
    print('123')
    time.sleep(1)
    print('end 123')


if __name__ == '__main__':
    t = Thread(target=func,)
    t2 = Thread(target=func2,)

    t.daemon = True

    t.start()
    t2.start()
    print('主线程')


# 执行结果
    # 123
    # 123
    # 主线程
    # end 123
    # end 123

GIL全局解释器锁

定义

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

首先需要明确的一点是GIL并不是 Python 的特性,它是在实现 Python 解析器 (CPython) 时所引入的一个概念。

1、就好比 C++ 是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。
2、有名的编译器例如GCC,INTEL C++,Visual C++等。
3、Python也一样,同样一段代码可以通过 CPython,PyPy,Psyco,JPython等不同的 Python 执行环境来执行。像其中的 JPython 就没有GIL。
4、然而 CPython 是大部分环境下默认的 Python 执行环境。所以在很多人的概念里 CPython 就是 Python,也就想当然的把GIL归结为 Python 语言的缺陷。
5、所以这里要先明确一点:GIL并不是 Python 的特性,Python 完全可以不依赖于GIL。

GIL介绍

1、GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

2、可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

3、要想了解GIL,首先确定一点:每次执行 python 程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py 会产生3个不同的 python 进程。

4、GIL的存在是因为 CPython 解释器中内存管理不是线程安全的(垃圾回收机制)
垃圾回收机制:引用计数、标记清除、分代回收

验证GIL的存在

以下代码,如果 GIL 存在结果为 0,不存在则为 0-100 之间的一个数

from threading import Thread

num = 100


def func():
    global num
    num -= 1


if __name__ == '__main__':
    lit = []
    for i in range(100):
        t = Thread(target=func)
        t.start()
        lit.append(t)

    for i in lit:
        i.join()
    print(num)

GIL与普通互斥锁

机智的同学可能会问到这个问题:Python 已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要 lock?

首先,我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

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

分析:

1、100 个线程去抢GIL锁,即抢执行权限
2、肯定有一个线程先抢到GIL(暂且称为线程 1 ),然后开始执行,一旦执行就会拿到 lock.acquire()
3、极有可能线程 1 还未运行完毕,就有另外一个线程 2 抢到GIL,然后开始运行,但线程 2 发现互斥锁 lock 还未被线程 1 释放,于是阻塞,被迫交出执行权限,即释放GIL
4、直到线程 1 重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁 lock,然后其他的线程再重复 2 3 4 的过程

代码示范

GIL与多线程

有了GIL的存在,同一时刻同一进程中只有一个线程被执行

听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而 python 的多线程开销小,但却无法利用多核优势,也就是说 python 没用了,php 才是最牛逼的语言?

别着急啊,还没讲完呢。

要解决这个问题,我们需要在几个点上达成一致:

1、cpu 到底是用来做计算的,还是用来做I/O的?

2、多 cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能

3、每个 cpu 一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

一个工人相当于 cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。

如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,

反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高。

情景分析

结论 1:

1、对计算来说,cpu 越多越好,但是对于I/O来说,再多的 cpu 也没用
2、当然对运行一个程序来说,随着 cpu 的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析 python 的多线程到底有无用武之地

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

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

单核情况下,分析结果

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

多核情况下,分析结果

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

结论2

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

多线程性能测试

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

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


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


if __name__ == '__main__':
    lis = []
    print(os.cpu_count())  # 8 核
    start_time = time.time()
    for i in range(8):
        # p = Process(target=func, )
        p = Thread(target=func, )
        lis.append(p)
        p.start()

    for i in lis:
        i.join()
    ent_time = time.time()
    print('使用的时间为:%s' % (ent_time - start_time))

# 多进程花费时间为:7.568458557128906
# 多线程花费时间为:34.62424421310425

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

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


def func():
    time.sleep(2)

if __name__ == '__main__':
    lis = []
    start_time = time.time()
    for i in range(8):
        # p = Process(target=func, )
        p = Thread(target=func, )
        lis.append(p)
        p.start()
    for i in lis:
        i.join()
    end_time = time.time()
    print('使用的时间为:%s' % (end_time - start_time))

# 多进程花费时间为:2.2403011322021484
# 多线程花费时间为:2.0028250217437744

应用

多线程用于IO密集型,如 socket,爬虫,web
多进程用于计算密集型,如金融分析

死锁现象

定义

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

2、此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁。

例子

from threading import Thread, Lock
import time


class MyRLock(Thread):
    mexty = Lock()
    mexty2 = Lock()

    def run(self):
        self.func()
        self.func2()

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

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

        self.mexty.release()

    def func2(self):
        self.mexty2.acquire()
        print('%s 拿到 B 锁' % self.name)
        time.sleep(0.5)

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

        self.mexty2.release()


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

执行结果:出现死锁,整个程序阻塞住

# Thread-1 拿到 A 锁
# Thread-1 拿到 B 锁
# Thread-1 拿到 B 锁
# Thread-2 拿到 A 锁

信号量

定义

信号量也是一把锁,可以指定信号量为 5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有 5 个任务拿到锁去执行

如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小。

解析

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


sm = Semaphore(3)
def func():
    with sm:
        print('%s 正在座位上' % currentThread().getName())
        time.sleep(random.randint(1, 3))


if __name__ == '__main__':
    for i in range(10):
        s = Thread(target=func,)
        s.start()

event事件

线程的一个关键特性是每个线程都是独立运行且状态不可预测。

如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用 threading 库中的 Event 对象。

描述

对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。

在初始情况下,Event 对象中的信号标志被设置为假。如果有线程等待一个 Event 对象,而这个 Event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。

一个线程如果将一个 Event 对象的信号标志设置为真,它将唤醒所有等待这个 Event 对象的线程。如果一个线程等待一个已经被设置为真的 Event 对象,那么它将忽略这个事件, 继续执行。

属性方法

# from threading import Event

# event.isSet():返回 event 的状态值;

# event.wait():如果 event.isSet()==False 将阻塞线程;

# event.set(): 设置 event 的状态值为 True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

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

img

例子

E.wait() 可以设置等待时间,即超过此时间为超时,不在等待。

from threading import Thread, Event
import time

E = Event()


def student(name):
    print('%s 学生正在上课' % name)
    E.wait()
    # E.wait(1)
    print('%s 学生下课' % name)


def teacher(name):
    print('%s 老师正在上课' % name)
    time.sleep(3)
    print('%s 老师正在下课' % name)
    E.set()


if __name__ == '__main__':
    s = Thread(target=student, args=('stu',))
    s2 = Thread(target=student, args=('stu2',))
    s3 = Thread(target=student, args=('stu3',))

    t = Thread(target=teacher, args=('ysg',))

    lis = [s, s2, s3, t]
    for i in lis:
        i.start()

执行结果:

# E.wait()
# stu 学生正在上课
# stu2 学生正在上课
# stu3 学生正在上课
# ysg 老师正在上课
# ysg 老师正在下课
# stu2 学生下课
# stu3 学生下课
# stu 学生下课

# E.wait(1)
# stu 学生正在上课
# stu2 学生正在上课
# stu3 学生正在上课
# ysg 老师正在上课
# stu 学生下课
# stu2 学生下课
# stu3 学生下课
# ysg 老师正在下课

定时器

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

from threading import Timer


def func(name):
    print('hello %s' % name)


if __name__ == '__main__':
    t = Timer(2, func, args=('ysg',))
    t.start()

验证码小例子

from threading import Timer
import random


class Timers:
    def __init__(self):
        super().__init__()
        self.myTimer()

    def myTimer(self):
        self.info = self.make_timer()
        print(self.info)
        self.t = Timer(5, self.myTimer)
        self.t.start()

    def make_timer(self, num=6):
        lis = ''
        for i in range(num):
            s1 = str(random.randint(0, 9))
            s2 = chr(random.randint(65, 90))
            lis += random.choice([s1, s2])
        return lis

    def run(self):
        while True:
            res = input('>>>').strip()
            if res == self.info:
                print('验证成功')
                self.t.cancel()
                break

t = Timers()
t.run()

进程池与线程池

在刚开始学多进程或多线程时,我们迫不及待地基于多进程或多线程实现并发的套接字通信,然而这种实现方式的致命缺陷是:服务的开启的进程数或线程数都会随着并发的客户端数目地增多而增多,这会对服务端主机带来巨大的压力,甚至于不堪重负而瘫痪。

于是我们必须对服务端开启的进程数或线程数加以控制,让机器在一个自己可以承受的范围内运行,这就是进程池或线程池的用途,例如进程池,就是用来存放进程的池子,本质还是基于多进程,只不过是对开启进程的数目加上了限制。

注意:ProcessPoolExecutor 与 ThreadPoolExecutor,用法全部相同。

解析

# 官网:https://docs.python.org/dev/library/concurrent.futures.html

# concurrent.futures 模块提供了高度封装的异步调用接口
# ThreadPoolExecutor:线程池,提供异步调用
# ProcessPoolExecutor: 进程池,提供异步调用

# Both implement the same interface, which is defined by the abstract Executor class.
# 两者都实现相同的接口,该接口由抽象执行器类定义。

基本方法

1、submit(fn, *args, **kwargs)	异步提交任务

2、map(func, *iterables, timeout=None, chunksize=1)	取代 for 循环 submit 的操作

3、shutdown(wait=True)	相当于进程池的 pool.close()+pool.join() 操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管 wait 参数为何值,整个程序都会等到所有任务执行完毕
submit 和 map 必须在 shutdown之前

4、result(timeout=None)	取得结果

5、add_done_callback(fn)	回调函数

进程池

介绍

# The ProcessPoolExecutor class is an Executor subclass that uses a pool of processes to execute calls asynchronously. ProcessPoolExecutor uses the multiprocessing module, which allows it to side-step the Global Interpreter Lock but also means that only picklable objects can be executed and returned.

# class concurrent.futures.ProcessPoolExecutor(max_workers=None, mp_context=None)
# An Executor subclass that executes calls asynchronously using a pool of at most max_workers processes. If max_workers is None or not given, it will default to the number of processors on the machine. If max_workers is lower or equal to 0, then a ValueError will be raised.

代码示例

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


def func():
    print('进程 is run,pid:%s' % os.getpid())
    time.sleep(random.randint(1, 3))


if __name__ == '__main__':
    p = ProcessPoolExecutor(5)
    for i in range(10):
        p.submit(func,)
    p.shutdown()        # 相当于进程池的pool.close()+pool.join()操作
    print('主')

执行结果:可以看出 pid 只使用了 5 个。

# 进程 is run,pid:14440
# 进程 is run,pid:16928
# 进程 is run,pid:14056
# 进程 is run,pid:1788
# 进程 is run,pid:23436
#
# 进程 is run,pid:14440
# 进程 is run,pid:16928
#
# 进程 is run,pid:14056
# 进程 is run,pid:1788
# 进程 is run,pid:23436
#
# 主

线程池

介绍

# ThreadPoolExecutor is an Executor subclass that uses a pool of threads to execute calls asynchronously.
# class concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix='')
# An Executor subclass that uses a pool of at most max_workers threads to execute calls asynchronously.

# Changed in version 3.5: If max_workers is None or not given, it will default to the number of processors on the machine, multiplied by 5, assuming that ThreadPoolExecutor is often used to overlap I/O instead of CPU work and the number of workers should be higher than the number of workers for ProcessPoolExecutor.

# New in version 3.6: The thread_name_prefix argument was added to allow users to control the threading.Thread names for worker threads created by the pool for easier debugging.

代码示例

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

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


if __name__ == '__main__':
    t = ThreadPoolExecutor(5)
    for i in range(10):
        t.submit(func,)
    t.shutdown()
    print('主')

执行结果:可以看出线程的名称只使用了 5 个。

# ThreadPoolExecutor-0_0 is run,pid:16976
# ThreadPoolExecutor-0_1 is run,pid:16976
# ThreadPoolExecutor-0_2 is run,pid:16976
# ThreadPoolExecutor-0_3 is run,pid:16976
# ThreadPoolExecutor-0_4 is run,pid:16976
# 
# ThreadPoolExecutor-0_2 is run,pid:16976
# ThreadPoolExecutor-0_1 is run,pid:16976
# ThreadPoolExecutor-0_0 is run,pid:16976
# 
# ThreadPoolExecutor-0_4 is run,pid:16976
# ThreadPoolExecutor-0_1 is run,pid:16976
# 主

map 方法

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


def func(n):
    print('%s is run' % currentThread().getName())
    time.sleep(random.randint(1, 3))
    return n ** 2


if __name__ == '__main__':
    p = ThreadPoolExecutor(5)
    # for i in range(10):
    #     p.submit(func, i)
    p.map(func, range(1, 10))   # #map取代了for+submit
    print('主')

异步调用与回调机制

可以为进程池或线程池内的每个进程或线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收任务的返回值当作参数,该函数称为回调函数。

同步调用

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


def guahao():
    print('%s 第一步量体温' % currentThread().getName())
    time.sleep(random.randint(1, 3))
    res = random.randint(36, 42)
    return {'Name': currentThread().getName(), 'tiwen': res}


def wenzhen(info):
    name = info['Name']
    tiwen = info['tiwen']
    print('%s 的体温为 %s' % (name, tiwen))


if __name__ == '__main__':
    t = ThreadPoolExecutor(10)
    for i in range(10):
        res = t.submit(guahao,).result()
        wenzhen(res)
    print('主')

执行结果:可以看出,每个人需要拿到体温计把体温测量完成后,第二人才可以进行测量。

# 执行结果
# ThreadPoolExecutor-0_0 第一步量体温
# ThreadPoolExecutor-0_0 的体温为 39
# ThreadPoolExecutor-0_0 第一步量体温
# ThreadPoolExecutor-0_0 的体温为 40
# ThreadPoolExecutor-0_1 第一步量体温
# ThreadPoolExecutor-0_1 的体温为 36
# ThreadPoolExecutor-0_0 第一步量体温
# ThreadPoolExecutor-0_0 的体温为 42
# ThreadPoolExecutor-0_2 第一步量体温
# ThreadPoolExecutor-0_2 的体温为 40
# ThreadPoolExecutor-0_1 第一步量体温
# ThreadPoolExecutor-0_1 的体温为 40
# ThreadPoolExecutor-0_3 第一步量体温
# ThreadPoolExecutor-0_3 的体温为 41
# ThreadPoolExecutor-0_0 第一步量体温
# ThreadPoolExecutor-0_0 的体温为 39
# ThreadPoolExecutor-0_4 第一步量体温
# ThreadPoolExecutor-0_4 的体温为 37
# ThreadPoolExecutor-0_2 第一步量体温
# ThreadPoolExecutor-0_2 的体温为 39
# 主

异步调用

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


def guahao():
    print('%s 第一步量体温' % currentThread().getName())
    time.sleep(random.randint(1, 3))
    res = random.randint(36, 42)
    return {'Name': currentThread().getName(), 'tiwen': res}


def wenzhen(res):
    info = res.result()
    name = info['Name']
    tiwen = info['tiwen']
    print('%s 的体温为 %s' % (name, tiwen))


if __name__ == '__main__':
    t = ThreadPoolExecutor(10)
    for i in range(10):
        t.submit(guahao,).add_done_callback(wenzhen)
    print('主')

执行结果:可以看出,每个人需要拿到体温计第二个人可以在来体温计,体温测量好后,每个人上报各自的体温。

# ThreadPoolExecutor-0_0 第一步量体温
# ThreadPoolExecutor-0_1 第一步量体温
# ThreadPoolExecutor-0_2 第一步量体温
# ThreadPoolExecutor-0_3 第一步量体温
# ThreadPoolExecutor-0_4 第一步量体温
# ThreadPoolExecutor-0_5 第一步量体温
# ThreadPoolExecutor-0_6 第一步量体温
# ThreadPoolExecutor-0_7 第一步量体温
# ThreadPoolExecutor-0_8 第一步量体温
# ThreadPoolExecutor-0_9 第一步量体温
# 主
# ThreadPoolExecutor-0_0 的体温为 36
# ThreadPoolExecutor-0_2 的体温为 39
# ThreadPoolExecutor-0_5 的体温为 36
# ThreadPoolExecutor-0_6 的体温为 41
# ThreadPoolExecutor-0_9 的体温为 41
# ThreadPoolExecutor-0_3 的体温为 42
# ThreadPoolExecutor-0_4 的体温为 38
# ThreadPoolExecutor-0_8 的体温为 38
# ThreadPoolExecutor-0_1 的体温为 40
# ThreadPoolExecutor-0_7 的体温为 38

协程

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

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

img

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

yiled 与串行执行

第一点

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

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

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

从执行结果和执行时间可以看出,单纯地切IO运行时间会变长。

import time

# yiled 方式执行
def func():
    g = func2()
    next(g)
    for i in range(10000000):
        g.send(i)


def func2():
    while True:
        res = yield


start = time.time()
func()
end = time.time()
print('yiled', end - start)     # 0.9751331806182861


# 串行方式执行
def func():
    res = []
    for i in range(10000000):
        res.append(i)
    return res


def func2(int):
    pass


start = time.time()
int = func()
func2(int)
end = time.time()
print('yiled', end - start)     # 0.9056558609008789

第二点

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

yield并不能实现遇到 io 切换。

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

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

1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
2. 作为 1 的补充:可以检测 io 操作,在遇到 io 操作的情况下才发生切换

协程介绍

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

需要强调的是:

1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换

优点如下:

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

缺点如下:

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

总结协程特点:

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

greenlet 模块

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

from greenlet import greenlet


def eat(name):
    print('%s 吃一会饭' % name)
    g2.switch('ysg')
    print('%s 吃一会饭' % name)
    g2.switch('ysg')


def play(name):
    print('%s 玩一会手机' % name)
    g.switch()
    print('%s 玩一会手机' % name)


g = greenlet(eat)
g2 = greenlet(play)

g.switch('ysg')

# 执行结果
# ysg 吃一会饭
# ysg 玩一会手机
# ysg 吃一会饭
# ysg 玩一会手机

greenlet 虽然提供了简洁的切换方式,但当切到一个任务执行时如果遇到 io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

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

gevent 模块

安装:pip3 install gevent

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

语法介绍

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

g2=gevent.spawn(func2)

g1.join()	# 等待 g1 结束

g2.join()	# 等待 g2 结束

# 或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value	# 拿到 func1 的返回值

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

import gevent
import time


def eat(name):
    print('%s 吃一会饭' % name)
    gevent.sleep(3)  # time.sleep(3)  gevent 则无法识别,它只能识别自己的IO
    print('%s 吃一会饭' % name)


def play(name):
    print('%s 玩一会手机' % name)
    gevent.sleep(4)
    print('%s 玩一会手机' % name)


start = time.time()
g = gevent.spawn(eat, 'ysg')
g2 = gevent.spawn(play, 'pei')

g.join()
g2.join()
end = time.time()
print('耗时:%s' % (end - start))      # 耗时:4.010692596435547


# 执行结果
# ysg 吃一会饭
# pei 玩一会手机
# ysg 吃一会饭
# pei 玩一会手机

使用 from gevent import monkey;monkey.patch_all() 相当于把所在行代码一下的所有涉及IO的操作都打了标签。

实际上是把IO操作变为非IO操作。

from gevent import monkey;monkey.patch_all()
import gevent
import time


def eat(name):
    print('%s 吃一会饭' % name)
    time.sleep(3)   #  monkey.patch_all(),相当于把所在行代码一下的所有涉及IO的操作都打了标签。实际上是吧IO操作变为非IO操作
    print('%s 吃一会饭' % name)


def play(name):
    print('%s 玩一会手机' % name)
    time.sleep(4)
    print('%s 玩一会手机' % name)


start = time.time()
g = gevent.spawn(eat, 'ysg')
g2 = gevent.spawn(play, 'pei')

# g.join()
# g2.join()
gevent.joinall([g,g2])      # 相当于上面两行的 g.join(),实现 gevent 异步提交任务

end = time.time()
print('耗时:%s' % (end - start))      # 耗时:4.010692596435547

基于 gevent 模块实现并发的套接字通信

服务端

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


def info(conn):
    while True:
        info = conn.recv(1024)
        if not info: break
        conn.send(info.upper())
    conn.close()


def server(ip, pool):
    gev = socket(AF_INET, SOCK_STREAM)
    gev.bind((ip, pool))
    gev.listen(5)

    while True:
        conn, addr = gev.accept()
        spawn(info, conn)
    gev.close()


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

客户端

from socket import *
from threading import Thread, currentThread


def clien():
    gev = socket(AF_INET, SOCK_STREAM)
    gev.connect(('127.0.0.1', 9033))

    while True:
        gev.send('hello world'.encode('utf-8'))
        info = gev.recv(1024)
        print(currentThread().getName(), info.decode('utf-8'))

    gev.close()


if __name__ == '__main__':
    for i in range(500):
        t = Thread(target=clien, )
        t.start()

各种锁的概念

https://blog.csdn.net/antony1776/article/details/90052315/

共享锁,排他锁

乐观锁,悲观锁

公平锁,非公平锁

可重入锁,不可重入锁

偏向锁、轻量级锁、自旋锁、重量级锁

分布锁

posted @ 2022-11-17 21:36  亦双弓  阅读(167)  评论(0)    收藏  举报