信号量、事件、队列补充、进程池和线程池(概念、方法、同步提交和异步提交)、协程

【一】信号量

  • 信号量Semahpore(同线程一样)

【1】什么是信号量

  • 互斥锁:允许在同一时刻只能有一个线程或进程同资源进行修改
  • 信号量:允许指定数量的进程或线程对资源继续修改

【2】例子

  • 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
  • Lock 锁住一个马桶,同时只能有一个人
  • Semaphore 锁住的是公共厕所,同时可以来一堆人

【3】示例

  • 如果指定信号量为3,那么来一个人获得一把锁,计数加1,当计数等于3时,后面的人均需要等待。
  • 一旦释放,就有人可以获得一把锁。
  • 信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念。
from multiprocessing import Semaphore, Process
import time
import random


def go_wc(sem, user):
    # 【1】对信号量加锁,一个进程占用则锁池 -1
    sem.acquire()

    print(f'{user} 占到一个茅坑')
    # 模拟每个人上厕所速度不一样,0代表有的人蹲下就起来了
    time.sleep(random.randint(0, 3))

    # 【2】释放信号量锁
    sem.release()
    print(f' {user} 释放结束')


def main():
    print(f'main process start ...')
    # 【一】创建一个信号量的池子 和互斥锁一样要先有一把锁来进行限制
    sem = Semaphore(3)
    # 用来存储所有的子进程
    p_l = [Process(target=go_wc, args=(sem, f'user_{i}:>>>',)) for i in range(5)]
    # 启动多线程
    [i.start() for i in p_l]
    # 等待所有子进程执行完毕
    [i.join() for i in p_l]

    print(f'main process end ...')

if __name__ == '__main__':
    main()


# main process start ...
# user_0:>>> 占到一个茅坑
# user_3:>>> 占到一个茅坑
# user_1:>>> 占到一个茅坑
#  user_1:>>> 释放结束
# user_2:>>> 占到一个茅坑
#  user_2:>>> 释放结束 user_3:>>> 释放结束
# 
# user_4:>>> 占到一个茅坑
#  user_0:>>> 释放结束
#  user_4:>>> 释放结束
# main process end ...

【二】事件

【1】什么是事件

  • Event(同线程一样)

【2】事件处理方法

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

  • 事件处理的机制:

    • 全局定义了一个“Flag”
    • 如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞
    • 如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
    • clear:
      • 将“Flag”设置为False
    • set:
      • 将“Flag”设置为True
from multiprocessing import Process, Event, Manager
import time
import random

color_red = '\033[31m'
color_reset = '\033[0m'
color_green = '\033[32m'


# 定义车辆行为函数,它会根据事件对象(event)的状态来决定是否等待或通过路口
def car(event, n):
    '''
    模拟汽车行为
    :param event: 事件对象,用于同步操作,红绿灯状态的标识
    :param n: 第几辆车的标识
    :return: 无返回值
    '''

    # 创建一个无限循环直到车辆离开
    while True:
        # 如果事件未设置,表示红灯 False
        if not event.is_set():
            # 红灯亮时,车辆等待,并输出提示信息
            print(f'{color_red}红灯亮{color_reset},car{n} :>>> 正在等待 ... ')
            # 阻塞当前进程,等待event被设置(绿灯)
            event.wait()
            # 当event被设置后,打印绿灯信息
            print(f'{color_green}车 {n} :>>>> 看见绿灯亮了{color_reset}')
            # 模拟通过路口需要的时间
            time.sleep(random.randint(3, 6))
            # 防止在sleep期间event状态改变,再次检查状态
            if not event.is_set():
                continue
            # 通过路口
            print(f'car {n} :>>>> 安全通行')
            # 退出循环
            break


# 定义 警车 行为函数, 警车 在红灯时等待一秒后直接通过
def police_car(event, n):
    '''
    模拟 警车 行为
    :param event: 事件对象,用于同步操作,红绿灯状态的标识
    :param n: 第几辆车的标识
    :return: 无返回值
    '''
    while True:
        # 判断是否为红灯
        if not event.is_set():
            print(f'{color_red}红灯亮{color_reset},car{n} :>>> 正在等待 ... ')
            #  等待1秒,不完全遵守交通规则
            event.wait(1)
            print(f'灯的是{event.is_set()},警车走了,car{n}')
            # 通过后立即结束循环
            break


# 定义交通灯控制函数,周期性切换红绿灯状态
def traffic_lights(event, interval):
    '''
    模拟 交通灯 行为
    :param event: 事件对象,用于同步操作,红绿灯状态的标识
    :param interval: 间隔(比如10秒)改变信号灯
    :return: 无返回值
    '''
    # 无限循环,持续控制交通灯状态
    while True:
        # 按照给定间隔(比如10秒)改变信号灯
        time.sleep(interval)
        # 如果当前是绿灯
        if event.is_set():
            # 切换到红灯状态
            # event.is_set() ---->False
            event.clear()
        else:
            # 如果当前是红灯,则切换到绿灯状态
            # event.is_set() ----> True
            event.set()


def main():
    # 初始化事件对象,初始状态为清除(即红灯)
    event = Event()
    # 使用Manager创建一个可跨进程共享的Event对象
    # manager = Manager()
    # event = manager.Event()

    # 创建并启动多个 警车 进程
    for i in range(5):
        police_car_process = Process(target=police_car, args=(event, i))
        police_car_process.start()

    # 启动交通灯控制进程
    # 交通灯变化周期为10秒
    traffic_lights_process = Process(target=traffic_lights, args=(event, 10))
    traffic_lights_process.start()

    # 打印分割线,表明程序运行开始
    print(' ------------交通开始------------- ')


if __name__ == '__main__':
    main()
    #  ------------交通开始------------- 
    # 红灯亮,car2 :>>> 正在等待 ... 
    # 红灯亮,car0 :>>> 正在等待 ... 
    # 红灯亮,car3 :>>> 正在等待 ... 
    # 红灯亮,car1 :>>> 正在等待 ... 
    # 红灯亮,car4 :>>> 正在等待 ... 
    # 灯的是False,警车走了,car2
    # 灯的是False,警车走了,car0
    # 灯的是False,警车走了,car3
    # 灯的是False,警车走了,car1
    # 灯的是False,警车走了,car4
    
    # print("\033[31m这是红色文本\033[0m")  # 红色前景色
    # print("\033[32m这是绿色文本\033[0m")  # 绿色前景色
    # print("\033[1;33m这是加粗的黄色文本\033[0m")  # 加粗并设置黄色前景色
    # print("\033[44m这是蓝色背景文本\033[0m")  # 蓝色背景色
    # print("\033[34;47m这是蓝色前景色和白色背景色的文本\033[0m")  # 蓝色前景和白色背景

【三】队列补充

from queue import Queue, LifoQueue, PriorityQueue

# 【一】正常队列
# maxsize 过不给默认值,这个队列的容量就是无限大
# queue = Queue(5)

# 放
# queue.put(timeout=1)
# queue.put_nowait()
# 取
# queue.get(timeout=1)
# queue.get_nowait()

# 判断队列是否为空
# print(queue.empty())
# 判断是满的
# print(queue.full())

# 获取当前队列中的个数
# print(queue.qsize())

# 【一】Queue 先进先出
print(f'----------Queue-------------')
queue_normal = Queue(3)
queue_normal.put(1)
queue_normal.put(2)
queue_normal.put(3)

print(queue_normal.get())
print(queue_normal.get())
print(queue_normal.get())
# 【二】LifoQueue 后进先出
print(f'----------LifoQueue-------------')
queue_lifo = LifoQueue(3)

queue_lifo.put(1)
queue_lifo.put(2)
queue_lifo.put(3)

print(queue_lifo.get())
print(queue_lifo.get())
print(queue_lifo.get())

# 【三】PriorityQueue : 根据优先级数字越小的先出
print(f'----------PriorityQueue-------------')
queue_priority = PriorityQueue(3)
# 可以给放进队列的元素设置优先级:数字越小优先级越高!
queue_priority.put((50, 111))
queue_priority.put((0, 222))
queue_priority.put((100, 333))

print(queue_priority.get())
print(queue_priority.get())
print(queue_priority.get())

# (0, 222)
# (50, 111)
# (100, 333)

【四】进程池和线程池

【1】池的概念

  • 池就是用来保证计算机硬件安全的情况下最大限度地利用计算机
  • 池降低了程序的运行效率,但是保证了计算机硬件的安全,从而保证程序的正常运行

【2】线程池

  • 语法
from concurrent.futures import ThreadPoolExecutor

# 默认开设当前计算机 cpu 个数五倍数的线程数
# 可以指定线程总数
pool = ThreadPoolExecutor(5)
  • 原理
    • 池子造出来后,里面固定存在五个线程
    • 这五个线程不会存在出现重复创建和销毁地过程
  • 优点
    • 避免了重复创建五个线程地资源开销

【3】使用方法

(1)任务的提交方式

  • 同步:提交任务之后原地等待任务的返回结果,期间不做任何事
  • 异步:提交任务之后不等待任务的返回结果,继续执行代码

(2)同步提交

from concurrent.futures import ThreadPoolExecutor
import time

# 构造线程池,指定线程总数
# 在后台的空间中就已经开辟好了五个池子,就等到子线程去用这个池子
# 避免了资源的重复开设和销毁
pool = ThreadPoolExecutor(5)


def work_add(result):
    return result + result


# 【3】定义多线程任务
def work(name):
    print(f'{name} is starting ... ')
    sleep_time = random.randint(1, 4)
    print(f'{name} start sleeping for {sleep_time}s')
    time.sleep(sleep_time)
    print(f'{name} end sleeping for {sleep_time}s')
    print(f'{name} is ending ... ')
    result = name * name
    result = work_add(result)
    print(f'{name} 的 result ::>>>> {result} ')


# 同步调教 : 提交任务之后原地等待结果,不做任何事
def main_thread():
    print(f'main process start ... ')
    for i in range(1, 3):
        poll.submit(work, i)
    print(f'main process end ... ')

if __name__ == '__main__':
    main_thread()
    
# main process start ... 
# 1 is starting ... 
# 1 start sleeping for 1s
# 2 is starting ... 
# 2 start sleeping for 1s
# main process end ... 
# 2 end sleeping for 1s
# 2 is ending ... 
# 2 的 result ::>>>> 8 
# 1 end sleeping for 1s
# 1 is ending ... 
# 1 的 result ::>>>> 2 

(3)异步提交

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

poll = ThreadPoolExecutor(5)

def work_add(result):
    return result + result


# 【3】定义多线程任务
def work(name):
    print(f'{name} is starting  pid {os.getpid()} ppid {os.getppid()}... ')
    sleep_time = random.randint(1, 4)
    print(f'{name} start sleeping for {sleep_time}s')
    time.sleep(sleep_time)
    print(f'{name} end sleeping for {sleep_time}s')
    print(f'{name} is ending ... ')
    return name * name


# 异步提交 : 提交任务之后不需要原地等待结果,去做其他事
def main():
    task_list = []
    print(f'main process start pid {os.getpid()} ppid {os.getppid()}... ')
    for i in range(1, 5):
        task_obj = poll.submit(work, i)
        task_list.append(task_obj)
    # 所有子线程结束后才能获取到所有的结果
    poll.shutdown()
    # 逐个获取每一个对象的结果
    for task_obj in task_list:
        print(task_obj.result())
    print(f'main process end ... ')

if __name__ == '__main__':
    main()
    
# main process start pid 16468 ppid 11380... 
# 1 is starting  pid 16468 ppid 11380... 
# 1 start sleeping for 1s
# 2 is starting  pid 16468 ppid 11380... 
# 2 start sleeping for 1s
# 3 is starting  pid 16468 ppid 11380... 
# 3 start sleeping for 3s
# 4 is starting  pid 16468 ppid 11380... 
# 4 start sleeping for 3s
# 1 end sleeping for 1s
# 1 is ending ... 
# 2 end sleeping for 1s
# 2 is ending ... 
# 3 end sleeping for 3s
# 3 is ending ... 
# 4 end sleeping for 3s
# 4 is ending ... 
# 1
# 4
# 9
# 16
# main process end ... 

(4)异步返回值

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

poll = ThreadPoolExecutor(5)


def work_add(result):
    return result + result


# 【3】定义多线程任务
def work(name):
    print(f'{name} is starting  pid {os.getpid()} ppid {os.getppid()}... ')
    sleep_time = random.randint(1, 4)
    print(f'{name} start sleeping for {sleep_time}s')
    time.sleep(sleep_time)
    print(f'{name} end sleeping for {sleep_time}s')
    print(f'{name} is ending ... ')
    return name * name


def call_back(n):
    print(f'call_back :>> n :>>> {n}')
    # <Future at 0x2a83d694f10 state=finished returned int>
    print(f'call_back :>> n.result() :>>> {n.result()}')
    # call_back :>> n.result() :>>> 16


def main():
    print(f'main process start pid {os.getpid()} ppid {os.getppid()}... ')
    for i in range(1, 5):
        # task_obj = poll.submit(work, i)
        # 没有加 add_done_callback 时候的 task_obj:  <Future at 0x1c21ab89e50 state=running>
        # 增加一个 add_done_callback : 子线程任务结束后自动调用异步回到函数 call_back
        poll.submit(work, i).add_done_callback(call_back)
        # 加了 add_done_callback  时候的 task_obj : None
    # 所有子线程结束后才能获取到所有的结果
    poll.shutdown()
    print(f'main process end ... ')


if __name__ == '__main__':
    main()
    
# main process start pid 17436 ppid 3244... 
# 1 is starting  pid 17436 ppid 3244... 
# 1 start sleeping for 4s
# 2 is starting  pid 17436 ppid 3244... 
# 2 start sleeping for 1s
# 3 is starting  pid 17436 ppid 3244... 
# 3 start sleeping for 3s
# 4 is starting  pid 17436 ppid 3244... 
# 4 start sleeping for 1s
# 2 end sleeping for 1s
# 2 is ending ... 
# call_back :>> n :>>> <Future at 0x28e1c403970 state=finished returned int>
# call_back :>> n.result() :>>> 4
# 4 end sleeping for 1s
# 4 is ending ... 
# call_back :>> n :>>> <Future at 0x28e1c403f10 state=finished returned int>
# call_back :>> n.result() :>>> 16
# 3 end sleeping for 3s
# 3 is ending ... 
# call_back :>> n :>>> <Future at 0x28e1c403c10 state=finished returned int>
# call_back :>> n.result() :>>> 9
# 1 end sleeping for 4s
# 1 is ending ... 
# call_back :>> n :>>> <Future at 0x28e1c397df0 state=finished returned int>
# call_back :>> n.result() :>>> 1
# main process end ...

【五】协程理论

  • 进程 ---》线程(进程下的进程)----》协程(线程下的线程)

【1】并发的本质

  • 本节的主题是基于单线程来实现并发

    • 即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发
    • 为此我们需要先回顾下并发的本质:
      • 切换+保存状态
  • CPU正在运行一个任务

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

【2】yield关键字

  • 保存当前的状态,让下一次基于当前状态继续处理

  • 可以让cpu能够雨露均沾

  • 实现看起来所有任务都被“同时”执行的效果

  • 但是这种切换反而会降低效率。

  • 为此我们可以基于yield来验证

    • yield本身就是一种在单线程下可以保存任务运行状态的方法
1 yield可以保存状态
	yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
2 send可以把一个函数的结果传给另外一个函数
	以此实现单线程内程序之间的切换

(1)串行执行

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)
# 0.8158173561096191

(2)基于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)

【3】实现遇到IO自动切换

  • 第一种情况的切换。

  • 在任务一遇到IO情况下

  • 切到任务二去执行

  • 这样就可以利用任务一阻塞的时间完成任务二的计算

  • 效率的提升就在于此。

  • yield不能检测IO

    • 实现遇到IO自动切换
import time


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


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


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

  • 协程的本质就是在单线程下

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

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

【六】协程介绍

【1】什么是协程

  • 是单线程下的并发,又称微线程,纤程。英文名Coroutine。

  • 一句话说明什么是线程:

    • 协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
  • 需要强调的是:

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

【2】优点

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

【3】缺点

  • 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
  • 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程(多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地,该线程内的其他的任务都不能执行了)

【4】总结

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


def work(name):
    print(f'{name} start ... ')
    time.sleep(random.randint(1, 3))
    print(f'{name} end ... ')
    main(cls=Thread)


def main(cls):
    task_list = [cls(target=work, args=(i,)) for i in range(10)]
    [task.start() for task in task_list]
    [task.join() for task in task_list]


def main_process():
    main(cls=Process)


if __name__ == '__main__':
    main_process()

# 千万不要去尝试上面的代码运行起来,否则后果很严重
posted @ 2024-05-29 08:50  光头大炮  阅读(23)  评论(0编辑  收藏  举报