进程理论和实操

2022-08-06

1.什么是进程

​ 进程是只一个正在运行的程序. 是一个抽象的概念
​ 进程是起源于操作系统的. 是操作系统最核心的概念. 操作系统的其他概念都是围绕进程展开的
​ 如果一个人说他精通进程. 那么就是精通操作系统. 要么就什么都不知道

2.为什么使用进程

​ 实现并发

3.并发. 并行. 串行的区别

​ 串行: 一个程序完完整整的运行结束. 才去运行下一个程序
​ 并发: 是伪并行. 即看起来是同时运行. 单个cpu+多道技术就可以实现并发(并行也属于并发)
​ 并行: 真正意义上的同时运行. 需要具有多个cpu才行

4.子进程的创建

​ 创建子进程会先将进程中的全局变量拷贝一份. 然后开启自己的进程. 创建一个独立的内存空间

​ 开启进程之后将与原来的进程无关. 意味着全局变量的改变子进程不会有任何变化

5.进程的状态

​ 就绪. 阻塞. 运行
​ 三种状态之间存在交互:

image-20220805104646476

6.开启进程的两种方式

# 方式1:(常用)
from multiprocessing import Process
import time


def task(name):
    print('%s is running' % name)
    time.sleep(2)
    print('%s is done' % name)


if __name__ == '__main__':
    p = Process(target=task, args=('子进程', ))  # 参数传入元组的格式
    p.start()  # 只是在操作系统中发出一个开启子进程的信号
    print('主')  # 主进程. 上一步已经发送信号. 但是创造子进程较慢


# 方式2: 面向对象(了解)
class Myprocess(Process):
    def __init__(self, name):
        super(Myprocess, self).__init__()  # 重用父进程方法
        self.name = name  # 可以起到更改name的效果

    def run(self):  # 必须要写的类
        print('%s is running' % self.name)
        time.sleep(2)
        print('%s is done' % self.name)


if __name__ == '__main__':
    p = Myprocess('子进程')
    p.start()  # p.run()
    print('主')

7.僵尸进程和孤儿进程

僵尸进程:
    1. 父进程创建的子进程运行完代码都是变成僵尸进程. 等待着父进程调用wait或waitpid去回收pid等等. 也叫'收尸'
        此类僵尸进程是没有害处的. 因为所有内容都会被回收
    2. 程序bug导致的父进程没有回收pid. 使大量僵尸进程累加. 占用pid并导致别的软件应用无法打开.
        这样是有害的僵尸进程. 可以关机重启来释放所有内存

孤儿进程:
    父进程创建完子进程之后自己"死"了. 没有回收子进程的pid及相关参数. 这样的子进程是孤儿进程
    孤儿进程是无害处的. 因为孤儿进程最后会被__init__进程(pid=0)回收并释放. 并不会形成累加

杀死进程: taskkill /F /PID pid编号
查找进程: tasklist | findstr 关键词

8.进程之间彼此隔离

# -*- encoding:utf-8 -*-
# @time: 2022/8/5 22:06
# @author: Maxs_hu
from multiprocessing import Process
import time

x = 100


def task():
    global x
    x = 0  # 在子进程的内存中修改x变量的值
    print('done')


if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    time.sleep(2)  # 等待足够长的时间保证子进程运行完毕
    print(x)

9.进程对象的方法或属性

# -*- encoding:utf-8 -*-
# @time: 2022/8/5 22:33
# @author: Maxs_hu
from multiprocessing import Process
import time
import os

"""
join方法
"""


def task(name, t):
    print('%s is running' % name)
    time.sleep(t)
    print('%s is done' % name)


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

    # 这样可以模拟并发操作. 串行用process不如直接调用. 因为process还多了向操作系统发信号以及切换进程的操作
    p1.start()
    p2.start()
    p3.start()

    p1.join()  # 1s # 等待子进程p1完成之后. 在运行父进程. 且子进程运行完成会被回收
    p2.join()  # 1s
    p3.join()  # 1s

    # 子进程的pid已经被回收. 但是还可以看到. 变成了一个free状态

    print('main')


# 代码精简
def task(name, t):
    print('%s is running' % name)
    time.sleep(t)
    print('%s is done' % name)


if __name__ == '__main__':
    p_ls = []
    for i in range(1, 4):
        p = Process(target=task, args=('子进程%s' % i, i))
        p_ls.append(p)  # 将实例化对象加入列表中
        p.start()  # 只是操作系统发信号的操作

    for p in p_ls:
        p.join()  # 直接调用实例化绑定方法
        # 这里也不需要按顺序去join. 因为运行总时间 = 任务等待时间最长的+进程之间的切换时间

"""
pid方法
"""


# pid方法
def task(n):
    print('%s is running ' % os.getpid())
    time.sleep(n)
    print('%s is done ' % os.getpid())


if __name__ == '__main__':
    p1 = Process(target=task, args=(10,))
    # print(p1.pid)
    p1.start()
    print(p1.pid)  # 父进程内查看子pid的方式
    print('主')


# ppid方法
def task():
    print('自己的id:%s 父进程的id:%s ' % (os.getpid(), os.getppid()))
    time.sleep(200)


if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start()
    print('主', os.getpid(), os.getppid())  # 这里ppid拿到的是pycharm的pid
    # 爹=》主--》儿子

10.守护进程

# -*- encoding:utf-8 -*-
# @time: 2022/8/6 10:08
# @author: Maxs_hu

"""
什么是守护进程:
    守护进程相当于 "子进程"
    守护进程会伴随着子进程的代码运行完毕后而死掉

为什么使用守护进程:
    关键字两个:
        1.进程:
            当父进程需要一个任务去并发执行. 需要将该任务放在一个子进程中
        2.守护:
            当该子进程中内的代码在父进程代码运行完毕后就没有存在的意义了.
            就要应该将该进程设置为守护进程. 并在父进程代码结束后死掉
    在生产者消费中模型中会用到
"""

from multiprocessing import Process
import time


def task(name, t):
    print('%s is running' % name)
    time.sleep(t)
    print('%s is done' % name)


if __name__ == '__main__':
    p1 = Process(target=task, args=('守护进程', 2))
    p2 = Process(target=task, args=('子进程', 3))
    p1.daemon = True  # 守护进程

    p1.start()
    p2.start()

    print('main')

    

# 守护进程的案例
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  # 设置守护进程

    p1.start()
    p2.start()

    print('main')

# 运行结果可能会出现各种格式.主要依据机器性能.并根据并发思维去思考
"""
main
456
end456
"""

11.互斥锁

# -*- encoding:utf-8 -*-
# @time: 2022/8/6 11:16
# @author: Maxs_hu
"""
互斥锁和join的区别:
    互斥锁:
        可以将执行任务的部分代码(只涉及到修改共享数据的代码)编程串行
    join:
        将所有执行任务的代码变成串行

互斥锁是增加运行时间去保护共享数据安全.
互斥锁是什么可以举个例子: 假设你和别人合租房. 可能会与很多公共资源. 有些是可以同时使用. 有些不能同时使用.
    那么不能同时使用的(比如厕所)可以加上一把锁. 先到先得. 等使用完成了下一个人再继续抢锁. 那么这个锁在我们的程序中就是互斥锁
"""
from multiprocessing import Process, Lock
import time
import os
import json


def check():
    time.sleep(1)
    with open('db.txt', 'rt', encoding='utf8') as f:
        dic = json.load(f)  # 将文件数据读出. 注意读出json数据一定要使用双引号
    print('%s 查看余票[%s]' % (os.getpid(), dic['count']))


def get():
    time.sleep(2)
    with open('db.txt', 'rt', encoding='utf8') as f:
        dic = json.load(f)
    if dic['count'] > 0:
        dic['count'] -= 1
        with open('db.txt', 'wt', encoding='utf8') as f:
            json.dump(dic, f)  # 将数据写入
        print('%s 已购买余票' % os.getpid())
    else:
        print('%s 没有余票' % os.getpid())


def task(mutex):
    # 查看余票
    check()
    # 购票
    mutex.acquire()  # 互斥锁不能连续的acquire. 只能是release之后才能进行acquire
    get()
    mutex.release()
    # with mutex():
    #     get()


if __name__ == '__main__':
    mutex = Lock()
    for i in range(10):  # 模拟十个人抢票
        p = Process(target=task, args=(mutex,))
        p.start()

12.IPC机制(进程通信)

# -*- encoding:utf-8 -*-
# @time: 2022/8/6 13:12
# @author: Maxs_hu

# IPC: 进程间通信
# PIPE: 管道
# Queue: pipe+锁

from multiprocessing import Queue

q = Queue(3)  # 先进先出
q.put('123', block=True, timeout=3)
q.put(['python', 'go'], block=True, timeout=3)
q.put({'security': 1000}, block=True, timeout=3)
# q.put(123, block=True, timeout=3)  # 超过队列的长度. 会报错queue.Full

print(q.get())  # 从队列中get也可以加参数block. timeout
print(q.get())
print(q.get())


from multiprocessing import Queue
import time

q = Queue(3)
q.put_nowait('123')  # 相当于q.put('123', block=False)
q.put_nowait(['python', 'Java'])
q.put_nowait({'security': 6000})

time.sleep(1)  # ???

print(q.get_nowait())
print(q.get_nowait())
print(q.get_nowait())
# print(q.get_nowait())  # 队列中没有数据也会报错 -> _queue.Empty

13.生产者消费者模型

# -*- encoding:utf-8 -*-
# @time: 2022/8/6 16:37
# @author: Maxs_hu
"""
1. 什么是生产者消费者模型:
    生产者: 程序中负责生产数据的代码块
    消费者: 程序中负责处理数据的代码块

    生产者->共享的介质(队列)<-消费者

2. 为何使用:
    实现了生产者消费者的解耦合. 生产者可以不停的生产. 消费者可以不停的消费. 提升了程序运行效率

    什么时候使用:
        当程序中存在明显的两类任务. 一类负责生产数据. 另一类负责处理数据
        我们此时就应该考虑使用生产者消费者模型去提高程序运行效率
        一般情况下. 消费者的速度会大于生产者
"""


# 初始模型(有bug) -> 在队列被消费者去空之后. 消费者会死等下一波数据. 导致主进程无法回收消费者. 主进程无法停止
import time
import os
import random
from multiprocessing import Process, Queue


def producer(q):
    for i in range(10):
        res = '包子%s' % i
        time.sleep(random.randint(1, 3))
        # 将数据放入队列
        q.put(res)
        print('\033[45m%s 生产了包子%s\033[0m' % (os.getpid(), i))  # \033[45m可以在终端有颜色打印


def consumer(q):
    while True:
        # 从队列中循环拿数据
        res = q.get()
        time.sleep(random.randint(1, 3))
        print('\033[46m%s 吃了 %s\033[0m' % (os.getpid(), res))


if __name__ == '__main__':
    q = Queue()
    # 创建生产者们
    p = Process(target=producer, args=(q, ))
    # 创建消费者们
    c = Process(target=consumer, args=(q, ))

    p.start()
    c.start()

    print('main')


# 生产者消费者 终极模型
# 1. 使用JoinableQueue
# 2. 消费者设置守护进程. 伴随着主进程的代码运行完毕自己死掉
# 3. 先p.join()等待生产者生产完毕. 再q.join()等待队列被取干净
from multiprocessing import Process, JoinableQueue
import time
import random


def producer(name, food, q):
    for i in range(3):
        res = f'{food}{i}'
        time.sleep(random.randint(1, 3))
        print(f'\033[47m{name}生产了{res}\033[0m')
        q.put(res)


def consumer(name, q):
    while True:
        res = q.get()
        time.sleep(random.randint(1, 3))
        print(f'\033[43m{name}吃了{res}\033[0m')
        q.task_done()


if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=('Max_hu', 'pizaa', q))
    p2 = Process(target=producer, args=('Mokeke', 'pig', q))
    p3 = Process(target=producer, args=('xiaoergu', 'xiang', q))

    c1 = Process(target=consumer, args=('longge', q))
    c2 = Process(target=consumer, args=('chunge', q))
    c1.daemon = True  # 设置守护进程. 主线程运行完毕即代表着消费者也没有存在的意义了
    c2.daemon = True

    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()

    # 等待生产者全部生产完毕
    p1.join()
    p2.join()
    p3.join()

    q.join()  # 等待队列被取干净
    # q.join() 意味着:
    # 主线程代码运行完毕 --> (生产者运行完毕) + 队列中的数据也被取干净了 --> 消费者没有存在的意义了.

补充

部分操作系统原理

1. 什么是操作系统:
    精简来说: 操作系统是一个协调.管理和控制计算机控制计算机硬件资源和软件资源的控制程序(硬件和软件之间)
    作用:
        1. 将硬件复杂的接口封装成简单的接口. 给用户或者应用程序使用
        2. 让多个应用程序对硬盘的竞争变的有序

2、串行:
    一个任务完完整整地运行完毕后,才能运行下一个任务

3、并发:
    看起来多个任务是同时运行的即可,单核也可以实现并发. 一个任务没有运行完就去执行下一个任务(将io的时间节省下来)

4、并行:
    真正意义上多个任务的同时运行,只有多核才实现并行

5、cpu的功能:
    cpu是用来做计算,cpu是无法执行IO操作的,一旦遇到io操作,应该让cpu去执行别的任务

6、多道技术(*****). 三代计算器还没有实现进程之间的物理层面的隔离
    1、空间上的复用 -> 多个进程共用一个内存条,各进程之间要通过物理层面隔离. 为cpu在多个进程之间的切换提供铺垫
    2、时间上的复用 -> 多个进程复用同一个cpu的时间
        cpu遇到IO切换:可以提升效率
        一个进程占用cpu时间过长或者说有另外一个优先级更高的进程,也会切走:为了实现并发效果不得已而为之,反而会降低程序的执行效率

7、我们写的携程尽量减少io操作. 只要涉及到io. cpu就会切换到别的进程.等io运行完毕. 这样就会浪费时间
    怎么减少io操作. 优化到时候最后就是减少与硬盘打交道. 从硬盘中读取数据就是最典型的io操作