并发编程(中)

  • 同步与异步
  • 阻塞与非阻塞
  • 创建进程的多种方式
  • 进程join方法
  • 进程间数据隔离
  • 进程间通信之IPC机制
  • 进程对象诸多方法
  • 生产者消费者模型
  • 互斥锁

引言:

在了解接下里的概念需要先明白一些基本知识

  1. 任务:操作系统可以同时运行多个任务。例如:一边上网,一边听音乐。这就是操作系统的多任务。操作系统会轮流让各个任务交替执行,但由于CPU的执行实在是太快了,给我们感觉就像所有任务都在同时进行一样。
  2. 进程:一个任务就是一个进程
  3. 线程:线程是进程的基本单位,一个任务最少有一个线程
  • 同步与异步

同步和异步说的都是编程语言在执行代码时的一个状态,简而言之的描述就是用来表达人物的提交方式

  1. 同步
    当提交任务后会等待返回结果再向下执行,相当与如果没有返回代码会一直等待

  2. 异步
    当提交任务后不会等待返回的结果,代码则继续执行,如果有结果会通过反馈机制返回

  • 阻塞与非阻塞

阻塞和非阻塞说的就是当前程序的执行状态

  1. 阻塞
    程序运行中的三状态的阻塞态

  2. 非阻塞
    程序运行中的三状态的就绪态和运行态

需要明白的是一般我们再说这几种状态时,会组合着说

  1. 同步阻塞
  2. 同步非阻塞
  3. 异步阻塞
  4. 异步非阻塞(效率最高)
  • 创建进程的方式
  1. 在使用桌面操作系统时,当我们使用鼠标双击一个应用程序,程序运行就相当于创建了一个进程
  2. 例如电脑上的qq可以开多个,这也就是相当于开了多进程
    那么在python中如何开启多个进程,代码实例如下

小知识:win和linux/mac 操作系统创建经常方式的区别

  1. win中在创建进程时会以模块导入的方式将代码导入内存中执行,这样会导致重复执行所以在win中创建多经常需要添加main来判断是不是执行文件,否则会出现重复导入的问题
  2. linux或者mac系统同在创建多进程时,会直接将创建进程上面的代码复制到内存当中,所以在linux中或者mac中不存在这个问题
  • 关于创建子进程,UNIX和windows
      1.相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。
      2.不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows系统来说,从一开始父进程与子进程的地址空间就是不同的。
# 创建进程的方式
# 需要使用multiprocessing 这个包下的Process
from multiprocessing import Process
import time


def task():
    print('task is running', 'wesley')
    time.sleep(3)
    print('task is over', 'wesley')


if __name__ == '__main__':
    p1 = Process(target=task)  # 使用Process生成进程对象
    p1.start()  # 启动子进程,
    print('主') 
"""
这里需要注意,在启动子进程时上面的函数就会被启动,并且是异步的方式执行
"""
  • 创建进程传值方式
# 创建进程并传值
from multiprocessing import Process
import time


def task(name):
    print('task is running', name)
    time.sleep(3)
    print('task is over', name)

if __name__ == '__main__':
    p1 = Process(target=task, args=('wesley',))
    p1.start()
    print('主')
"""
第一种传值方式使用使用 args来定义变量的值,在函数中使用变量进行调用
"""
  • 创建进程并使用键值的方式传值
# 在创建进程时使用对Process的继承方法进行重写

from multiprocessing import Process
import time


class MyProcess(Process):
    def __init__(self, name, age):
        super().__init__()
        self.name = name
        self.age = age

    def run(self):
        print('run is running', self.name, self.age)
        time.sleep(3)
        print('run is over', self.name, self.age)


if __name__ == '__main__':
    obj = MyProcess('wesley', 123)
    obj.start()
    print('主')
    
"""
需要注意的是这种方法本质上来说是对类中定义的值的一个重新复复赋值的一个操作
"""
  • 进程间数据隔离

严格意义上来说 一台物理机上的进程是相互隔离的其中的数据无法被调用(默认情况)

  1. 在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)
  2. 在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。
from multiprocessing import Process
import time

money = 1000  # 全局变量


def task():
    global money
    money = 666
    print('子进程的task函数查看money', money)

if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start()
    time.sleep(5)
    print(money)

子进程的task函数查看money 666
1000
"""
由于进程默认数据是全部隔离的,所以这里子进程的数据是666,主进程的数据是1000
"""

  • 进程的join方法
# 进程的join方法
from multiprocessing import Process
import time


def task(name, n):
    print('%s is running' % name)
    time.sleep(n)
    print('%s is over' % name)
if __name__ == '__main__':
    p1 = Process(target=task, args=('wesley1', 1))
    p2 = Process(target=task, args=('wesley2', 2))
    p3 = Process(target=task, args=('wesley3', 3))
    """主进程代码等待子进程代码运行结束再执行"""
    start_time = time.time()
    p1.start()  # 异步
    p2.start()  # 异步
    p3.start()  # 异步
    p1.join()
    p2.join()
    p3.join()
    print(time.time() - start_time)

3.057915687561035  # 最总运行时间是3s,这和我们预期的6s不符,为什么
"""
1. join 这个指令是等待上一个指令完成后向后执行
2. 但是当我执行p1 p2 p3 .start() 时,程序是异步执行,并不会等待,这也就是说,也就是说,这3个代码是同时执行的,最终我耗时最长的为3秒,所以这里也就是3秒,这种就叫做并发
"""
  • IPC进程间通讯-队列

创建队列,Queue是多进程安全队列,可以实现多进程间的数据交互
Queue([maxsize])
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。


from multiprocessing import Queue
# 使用消息队列需要再multiprocessing中的Queue

q = Queue(3)  # 在括号内可以指定存放消息的数据量,如果不写 默认没有限制

q.put(111)  # 向消息队列中存放数据

q.full()  # 判断队列是否已满,返回bool值

q.empty()  # 判断队列是否为空,返回bool值

q.get()  # 从消息队列中取出数据

q.get_nowait() # 同get()方法一样

"""
full()  和  empty()  方法在多进程中不可以使用,可能出现极限存储数据情况,这时方法的返回将不再准确
"""

"""关于mac使用Queue队列产生的默认启动fork方式不一致带来的问题
spawn: 
使用此方式启动进程, 只会执行和target参数或者run()方法相关的代码. Windows平台只能使用此方法. 此方式启动进程的效率最低.
fork: 
使用此方式启动的进程, 基本等同于主进程(复刻主进程, 主进程拥有的资源, 子进程都有). 所以, 该方式创建的子进程会从创建位置起, 和主进程一样执行程序中的代码. 此启动方式适用于Unix和Linux.
forserver: 
使用此方式, 程序将会启动一个服务器进程. 即当程序每次请求启动新进程时, 父进程都会连接到该服务器进程, 请求有服务器进程来创建新进程. 通过这种方式启动的进程不需要从父进程继承资源. 此方式使用与Unix,
"""


from multiprocessing import Queue, set_start_method, get_context


def product(q):
    q.put('子进程p添加的数据')

def consumer(q):
    print('子进程获取队列中的数据', q.get())


if __name__ == '__main__':
    # set_start_method('fork') # 有mac 使用fork带来的问题 解决方式一
    q = Queue()
    ctx = get_context('fork')

    # 主进程往队列中添加数据
    q.put('我是主进程添加的数据')
    p1 = ctx.Process(target=consumer, args=(q,))
    p2 = ctx.Process(target=product, args=(q,))
    p1.start()
    p2.start()
    print('主')

  • 生产者和消费者模型
    生产者: 负责产生数据的服务
    消费者: 负责处理数据的服务
    以上两种概念在IT行业中多用于消息队列
# 进程对象的多种方法
# 1. 如何查看进程号
from multiprocessing import Process, current_process
print(current_process().pid) # 获取当前进程PID
import os
# 获得当前进程的PID
print(os.getpid())
# 获得当前进程的父PPID
print(os.getppid())

----------------------------------------------------
# from multiprocessing import Process,current_process
# import os
# def task():
#     print(current_process())
#     print(current_process().pid)
#     pass
#
# if __name__ == '__main__':
#     p = Process(target=task, name="我是子进程呦")
#     # p = Process(target=task)
#     p.start()
#     p.join()
#     print(p.pid, "我我我")
#     print(os.getpid())
  • 守护进程
from multiprocessing import Process
import time


def task(name):
    print('德邦总管:%s' % name)
    time.sleep(3)
    print('德邦总管: %s' % name)

if __name__ == '__main__':
    p1 = Process(target=task, args=('大张红',))
    p1.daemon = True
    p1.start()
    time.sleep(1)
    print('恕瑞玛皇帝:小吴勇嗝屁了')

  • 僵尸进程和孤儿进程
僵尸进程
	进程执行完毕后并不会立刻销毁所有的数据 会有一些信息短暂保留下来
 	比如进程号、进程执行时间、进程消耗功率等给父进程查看
 	ps:所有的进程都会变成僵尸进程
孤儿进程
	子进程正常运行 父进程意外死亡 操作系统针对孤儿进程会派遣福利院管理
  • 抢票程序通过互斥锁保证数据安全
from multiprocessing import get_context, Lock
import time
import json
import random


#
#
# 对票数查询

def search(name):
    with open(rf'/Users/wesley/PycharmProjects/pythonProject5/package.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    print('%s正在查票,当前余票为:%s' % (name, data.get('ticket_num')))


# 用户买票
def buy(name, lock):
    # 再次确认
    lock.acquire()
    with open(r'/Users/wesley/PycharmProjects/pythonProject5/package.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    # 模拟网络延时
    time.sleep(random.randint(1, 3))
    # 判断是否有票

    if data.get('ticket_num') > 0:
        data['ticket_num'] -= 1
        with open(r'/Users/wesley/PycharmProjects/pythonProject5/package.json', 'w', encoding='utf8') as f:
            json.dump(data, f)
        print('%s 买票成功' % name)
        lock.release()

    else:
        print('%s 很遗憾,没有抢到票' % name)
        lock.release()


def run(name, lock):
    search(name)
    buy(name, lock)


if __name__ == '__main__':
    lock = Lock()
    for i in range(20):
        c = get_context('fork')
        p = c.Process(target=run, args=('用户%s' % i, lock))
        p.start()


posted @ 2025-03-13 13:50  樵夫-  阅读(6)  评论(0)    收藏  举报