网络编程:同步与异步、阻塞与非阻塞、创建进程的多种方式、进程间数据隔离、进程的join方法、IPC机制、生产者消费者模型、进程对象的多种方法及拓展知识、僵尸进程与孤儿进程守护进程、多进程数据错乱问题、作业

一、同步与异步

  • 同步与异步两个名次是用来表达任务的提交方式。
  • 根据进程和函数之间的通信机制,函数可以分为异步和同步。

同步

同步:进程调用函数后,函数执行完成之后,才会有返回值,没有执行完成之前,不会有返回值。

例子:

打开一个程序,我们等待他进行加载,期间不做别的操作,等他打开后再接着使用。

异步

异步:进程调用函数后,函数会直接返回收到(可以理解为:已收到,正在处理),等到处理完成,函数会通过回调或通知的方式,将结果发送给进程。

例子:

当我们打开一个软件,会先进入加载界面,这个时候我们不等待,在加载的时候去打开另一个软件。然后当另一个软件进入加载状态的时候第一个软件就已经加载好,这时候我们可以通过软件的变化或是提示得知第一个软件已经可以使用了。

二、阻塞与非阻塞

  • 阻塞与非阻塞是用来表达任务的执行状态
  • 根据进程等待函数调用时的状态,函数可以分为阻塞和非阻塞。

阻塞

在得到函数返回值之前,该进程处于挂起状态,不属于工作队列(可运行状态进程组成的队列),不会占用CPU资源。

非阻塞

进程调用函数之后,无论是否返回结果,进程都会继续运行,进程仍处于可运行状态。

  • 昨天学习了进程的三种状态,他们可以归类为阻塞态与非阻塞态

三、综合使用

1.同步阻塞

客户揣发送请求给服务揣,此时服务端处理任务时间很久,则客户端则被服务端堵塞了,所以客户端会一直等待服务端的响应,此时客户端不能做其他任何事,服务端也不接受其他客户揣的请求。这种通信机制比较简单租暴,但是效率不高。

举例:

一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,cpu就不给你用了,让你进入阻塞状态。然后你因为在等待返回结果,没有继续运行别的函数,所以你处于同步状态。

2.同步非阻塞:

客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候虽然客户端会一直等待响应,但是服务端可以处理其他的请求,过一会回来处理原先的。这种方式很高效,一个服务端可以处理很多请求,不会在因为任务没有处理完而堵着,所以这是非阻塞的。

举例:

一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,但是他没有直接不给你用,在给你用的同时他也处理别的进程的任务,这个时候就是非阻塞态。但是进程虽然用着cpu,可是因为没收到返回值代码一直停着没继续往下,所以他还是在同步态。

3.异步阻塞:

客户揣发送请求给服务端,此时服务端处理任务时间很久,但是客户端不会等待服务器响应,它可以做其他的任务,等服务器处理完毕后再把结果响应给客户端,客户端得到回调后再处理服务端的响应。这种方式可以避免客户端一直处于等待的状态,优化了用户体验,其实就是类似于网页里发起的ajax异步请求。

举例:

一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,cpu就不给你用了,让你进入阻塞状态。然后这个进程不会一直等待最终的结果,他会继续运行别的函数,当结果出来的时候,通知一下这个进程就好了。因为进程停下来等待结果,所以处理异步状态。

4.异步非阻塞:

客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候的任务虽然处理时间会很久,但是客户端可以做其他的任务,因为他是异步的,可以在回调函数里处理响应;同时服务端是非阻塞的,所以服务端可以去处理其他的任务,如此,这个模式就显得非常的高效了 。

举例:

一个进程运行,执行内部函数的时候进程要等待返回结果,这个时候cpu发现你现在在等待,但是他没有直接不给你用,在给你用的同时他也处理别的进程的任务,这个时候就是非阻塞态。然后这个进程不会一直等待最终的结果,他会继续运行别的函数,当结果出来的时候,通知一下这个进程就好了。因为进程停下来等待结果,所以处理异步状态。

ps:将来使用的基本都是异步非阻塞

四、创建进程的多种方式

进程的创建

  但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

  而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:

  1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)

  2. 一个进程在运行过程中开启了子进程(如python代码创建进程等)

  3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

  4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

  无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。 

multiprocessing模块

仔细说来,multiprocess不是一个模块而是python中一个操作、管理进程的包。 之所以叫multi是取自multiple的多功能的意思,在这个包中几乎包含了和进程有关的所有子模块。由于提供的子模块非常多,为了方便大家归类记忆,我将这部分大致分为四个部分:创建进程部分,进程同步部分,进程池部分,进程之间数据共享。

第一种创建进程的方式

直接创建函数,用函数创建进程

同步状态:
import time  def task(name):    print('task is running',name)    time.sleep(3)    print('task is over',name)  if __name__ == '__main__':    task('zzh')  # 同步    print('主')

结果如下:

img

异步状态:
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=('jason',))  # 第一种传参方式:位置参数(注意这里需要是元组)
    p1 = Process(target=task, kwargs={'name':'jason123'})  # 第二种传参方式:关键字参数
    p1.start()  # 异步 告诉操作系统创建一个新的进程 并在该进程中执行task函数
    print('主')

结果如下:

img

不同的操作系统中创建进程底层原理的区别

windows
以导入模块的形式创建进程

因为是以导入模块的形式创建的,因此需要跟上面的代码一样,用上

if __name__ == '__main__':

来区分导入的文件是否为被执行文件,否则就会出现循环导入导致报错。

linux/mac
以拷贝代码的形式创建进程(就没有windows那些问题了)

第二种创建进程的方式

面向对象的方式

使用派生方法定义一个新的类,然后创建对象,对象调用方法创建进程

from multiprocessing import Process
import time


class MyProcess(Process):
    def __init__(self, name, age):
        # 这里主要是为了传参才定义的双下init,但是super需要在上面,因为调用process中的init的时候会给对象的属性绑定一个默认值。
        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('jason', 123)
    obj.start()
    print('主')

五、进程间的数据隔离

同一台计算机上的多个进程数据是严格意义上的物理隔离(默认情况下)

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(3)  # 主进程代码等待3秒
    print(money)  # 主进程代码打印money

结果如下:

img

上面我们提到,在windows中创建进程是相当于导模块的操作,因此我们可以看成子进程的代码相当于在另外一个py文件中执行,虽然用上了global改变全局变量,因为跟主进程不在一个文件,可以看成产生了数据隔离。

六、进程的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=('jason1', 1))
    p2 = Process(target=task, args=('jason2', 2))
    p3 = Process(target=task, args=('jason3', 3))
    # p.start()  # 异步
    '''主进程代码等待子进程代码运行结束再执行'''
    # p.join()
    # print('主')
    start_time = time.time()
    p1.start()
    p1.join()
    p2.start()
    p2.join()
    p3.start()
    p3.join()
    # p1.join()
    # p2.join()
    # p3.join()
    print(time.time() - start_time)  # 3秒多

七、IPC机制

  • IPC(Inter-Process Communication):进程间通信(消息队列)
  • 进程间通信——队列(multiprocess.Queue)
  • 创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
  • 说直观一点就是可以创建一个可以用于多进程间数据传输的队列
from multiprocessing import Queue


q = Queue(3)  # 括号内可以指定存储数据的个数
# 往消息队列中存放数据
q.put(111)
# print(q.full())  # 判断队列是否已满
q.put(222)
q.put(333)
# print(q.full())  # 判断队列是否已满
# 从消息队列中取出数据
print(q.get())
print(q.get())
# print(q.empty())  # 判断队列是否为空
print(q.get())
# print(q.empty())  # 判断队列是否为空
# print(q.get())
print(q.get_nowait())

"""
full() empty() 在多进程中都不能使用!!!
"""


from multiprocessing import Process, Queue


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

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


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

八、生产者消费者模型

生产者消费者模型

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

生产者
负责产生数据的'人'
消费者
负责处理数据的'人'

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

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

什么是生产者消费者模式

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

举例:

我们去包子店买包子,老板就是生产者,客人就是消费者。在引入生产者消费者模型之前,就相当于老板生产出来的包子要直接塞客人嘴里让他吃掉,客人要一直处于吃包子的状态。这明显有问题,一方面可能客人太多老板的包子不够吃了,另一方面可能出现客人太少,老板把客人撑爆了。因此我们引入一个桌子,老板把刚蒸出来的包子放桌上,客人要吃的时候自己去桌上拿,如果没包子了就等老板做出来再拿。

九、进程对象的多种方法及拓展知识

进程对象的诸多方法

1.如何查看进程号(PID)
	from multiprocessing import Process, current_process
 	current_process()
 	current_process().pid  
	import os
 	os.getpid()
  	os.getppid()
2.终止进程
	p1.terminate()
	ps:计算机操作系统都有对应的命令可以直接杀死进程
3.判断进程是否存活
	p1.is_alive()
4.创建子进程
start()
5.join()

拓展

  • 当一个进程运行起来后,我们可以在cmd中,使用tasklist命令查看当前运行的进程信息。
  • PID(Process Identification)操作系统里指进程识别号,也就是进程标识符。操作系统里每打开一个程序都会创建一个进程ID,即PID。

含义

只要运行一程序,系统会自动分配一个标识。

是暂时唯一:进程中止后,这个号码就会被回收,并可能被分配给另一个新进程。

只要没有成功运行其他程序,这个PID会继续分配给当前要运行的程序。

如果成功运行一个程序,然后再运行别的程序时,系统会自动分配另一个PID。

  • taskkill,杀死进程(也就是关掉),可以使用help taskkill命令查看说明。

我们在cmd中输入:

taskkill /F /PID 进程对应的PID

就可以干掉那个PID对应的进程

十、守护进程

主进程创建守护进程

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

  其二:守护进程内无法再开启子进程,否则抛出异常:

AssertionError: daemonic processes are not allowed to have children

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

eg: 吴勇是张红的守护进程 一旦张红嗝屁了 吴勇立刻嗝屁

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:所有的进程都会变成僵尸进程(不过吧就是存在的时间不长,可能就几秒钟)

孤儿进程

子进程正常运行,父进程意外死亡(比如我们跑到cmd中用taskkill关掉父进程),操作系统针对孤儿进程会派遣福利院管理那么这些进程将会成为孤儿进程。孤儿进程将会被init进程(进程号为1)所收养,并由init进程对他们完成状态收集工作。

十二、多进程数据错乱问题

模拟抢票软件

from multiprocessing import Process
import time
import json
import random


# 查票
def search(name):
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    print('%s在查票 当前余票为:%s' % (name, data.get('ticket_num')))


# 买票
def buy(name):
    # 再次确认票
    with open(r'data.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'data.json', 'w', encoding='utf8') as f:
            json.dump(data, f)
        print('%s买票成功' % name)
    else:
        print('%s很倒霉 没有抢到票' % name)


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


if __name__ == '__main__':
    for i in range(10):
        p = Process(target=run, args=('用户%s'%i, ))
        p.start()

通过上面的代码,运行之后我们发现虽然设置成只有1张票,但是每个人都会显示买到票了。这个时候就出现了数据错乱。

但是有的时候又会发现,有时候又是正常的逻辑顺序进行抢票。

多进程操作数据很可能会造成数据错乱,解决方案>>>:互斥锁
互斥锁
将并发变成串行,牺牲了效率但是保障了数据的安全

十三、作业

1.将TCP服务端使用多进程实现并发效果

聊天全部采用自动发送 不要用input手动输

服务端

# tcp 服务器端编程
import socket,threading,time

def tcplink(sock, addr):
    print('Accept new connection from %s:%s...' % addr)
    sock.send(b'Welcome!')
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        if not data or data.decode('utf-8') == 'exit':
            break
        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
    sock.close()
    print('Connection from %s:%s closed.' % addr)

# 服务器程序
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 监听端口
s.bind(('127.0.0.1', 9998))
s.listen(5)
print('Waiting for connection...')

while True:
    # 接受一个新连接
    sock,addr = s.accept()
    # 创建新线程来处理 TCP 连接
    t = threading.Thread(target=tcplink, args=(sock,addr))
    t.start()

客户端

# 与服务器通讯的客户端

import socket

# 客户端程序
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接
s.connect(('127.0.0.1', 9998))
# 接受欢迎消息
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
    # 发送数据
    s.send(data)
    print(s.recv(1024).decode('utf-8'))

s.send(b'exit')
s.close()

结果展示:

img

2.查询IT行业可能出现的锁名称及概念

锁的概念

同步访问共享数据时,为了保证数据操作的原子性,需要使用锁进行访问控制。锁,本质上是一个访问权限的标记。

在程序的世界里,有各种形式的数据,它们以各种形式被访问和存储。CPU 寄存器组,一级、二级缓存,内存,磁盘,设备缓存,而从进程的角度来说,变量,栈,堆,数据库,还有各种各样的缓存。

存在数据的地方,就存在访问控制!

锁,可以有多种实现策略,只要满足标记修改的原子性,及标记本身的可见性。

常见的锁

共享锁,排他锁
乐观锁,悲观锁
公平锁,非公平锁
可重入锁,不可重入锁
偏向锁、轻量级锁、自旋锁、重量级锁
分布锁

3.整理理论内容 尝试编写cs架构的软件 实现数据的上传与下载

服务端

"""
编写cs架构的软件,实现客户端可以下载服务端的文件,如图片、视频、文本等
"""
import socketserver
import os
import json
import struct


def base_path(*paths):
    return os.path.normpath(os.path.join(__file__, '..', 'db', *paths))


class MyRequestHandler(socketserver.BaseRequestHandler):
    DB_PATH = base_path()

    def header(self, request=False, warn=None, info=None, file_name=None, file_size=None):
        # 定制头部
        dic = {
            'request': request,
            'file_name': file_name,
            'file_size': file_size,
            'warn': warn,
            'info': info,
        }
        dic_json_bytes = json.dumps(dic).encode('utf-8')
        header_bytes = struct.pack('i', len(dic_json_bytes))
        self.request.send(header_bytes)
        self.request.send(dic_json_bytes)

    def verify_path(self, path_json):
        # 逻辑判断
        current_path = base_path(*path_json.split('/'))
        print('current_path:', current_path)
        if not os.path.isfile(current_path):
            return
        if len(self.DB_PATH) >= len(current_path):
            return
        return current_path

    def send(self, current_path):
        file_size = os.path.getsize(current_path)
        file_name = os.path.basename(current_path)
        info = '执行命令成功! 正在为您下载文件....'
        self.header(True, file_size=file_size, file_name=file_name, info=info)
        with open(current_path, 'rb') as f:
            for line in f:
                # 发送数据
                self.request.send(line)

    def handle(self):
        print('客户端访问:', self.client_address)
        while True:
            try:
                path_json = self.request.recv(1024).decode('utf-8')
                print('path_json:', path_json)
                if not path_json:
                    break
                # 1. 定制头部

                # 2. 逻辑判断
                current_path = self.verify_path(path_json)

                if not current_path:
                    self.header(warn='对不起, 您的文件路径不存在!')
                    continue

                # 3. 发送数据
                self.send(current_path)
            except ConnectionResetError:
                break
            except Exception:
                break
        print("客户端断开连接:", self.client_address)
        self.request.close()


s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandler)
s.serve_forever()

客户端

"""
编写cs架构的软件,实现客户端可以下载服务端的文件,如图片、视频、文本等
"""
import os
import json
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

def progress(percent, symbol='#', width=50):
    if percent > 1:
        percent = 1
    show_progress = ("[%%-%ds]" % width) % (int(percent * width) * symbol)
    print("\r%s %.2f%%" % (show_progress, percent * 100), end='')

while True:
    cmd = input("[下载: get /路径/路径../文件]请输入命令>>: ").strip()
    if not cmd:
        continue

    cmd_list = cmd.split()
    if len(cmd_list) != 2:
        print('对不起, 输入错误!')
        continue

    cmd, path = cmd_list
    if cmd != 'get':
        print("对不起, 下载文件请执行命令get")
        continue
    # 1. 发送命令
    client.send(path.encode('utf-8'))

    # 2. 解包
    import struct

    header_bytes = client.recv(4)
    json_dic_bytes_length = struct.unpack('i', header_bytes)[0]
    json_dic_bytes = client.recv(json_dic_bytes_length)
    dic = json.loads(json_dic_bytes.decode('utf-8'))
    print(dic)

    # 3. 逻辑判断
    request = dic['request']
    if not request:  # 失败
        print(dic['warn'])
        continue
    info, file_size, file_name = dic['info'], dic['file_size'], dic['file_name']

    # 接收数据
    print(info)
    print("start recv........")
    with open(file_name, 'wb') as f:
        recv_size = 0
        while recv_size < file_size:
            data_bytes = client.recv(1024)
            f.write(data_bytes)
            recv_size += len(data_bytes)
            percent = recv_size / file_size
            progress(percent)
    print("\nend recv........")

client.close()
posted @ 2022-11-18 17:06  wwwxxx123  阅读(29)  评论(0编辑  收藏  举报