Python之并发编程(进程)

一、操作系统的发展史

	首先回顾三大核心硬件
		CPU 是计算机中那个真真一直在运行干活的人
		内存 给CPU准备需要运行的代码 软件
		硬盘 用来存储将可能要被运行的代码 软件

进入主题

	1.穿孔卡片
		1725年,法国人布乔发明了打孔卡,用于贮存纺织机工作过程控制的信息。
		但当时打孔卡并没有广泛应用。19世纪80年代,打孔卡主要用于记录数据,
		曾用于记录美国1890年人口普查数据。
		(CPU利用率非常的低 好处就是程序员可以一个人来独占计算机 想做什么就做什么)

在这里插入图片描述

	2.联机批处理系统
		加载在计算机上的一个系统软件,在它的控制下,计算机能够自动地、成批地处理
		一个或多个用户的作业(这作业包括程序、数据和命令)。
		缩短录入数据的时候 让CPU连续工作的时间变长(提升CPU利用率)

在这里插入图片描述

	3.脱机批处理系统
		为克服与缓解:高速主机与慢速外设的矛盾,提高CPU的利用率
		又引入了脱机批处理系统,即输入/输出脱离主机控制。
		(现代计算机的雏形主要提升CPU利用率)

在这里插入图片描述

	单道技术
		所有的程序排队等着被运行 总耗时是所有程序耗时之和
		(比如打游戏听歌>必须的听完歌等着播完>之后才能去打游戏)
		
	多道技术
		计算机利用空闲时间提前准备好一些数据 提高效率 总耗时较短
		(比如听歌>这一首歌还没唱完>就已经加载好了下一首歌的歌词歌曲)
	
	多道技术简单的来说就是 切换+保存状态
		1.CPU在两种下会切换(去执行其他的程序)
			1> 程序自身进入IO操作(IO:输入输出操作)
				例如:获取用户输入、time.sleep、读取文件、保存文件
		2.保存状态
			每次切换之前要记录下当前执行的状态 之后切换回来基于当前状态继续执行
			 	例如:打游戏撩妹 文字已经打入聊天框 游戏开始了 打完文字还在聊天框内

	eg:
	  一个人要同时使用五台打印机打印资料,请问如何要一个人连续使用打印机打印资料?
		启动第一台打印机打印资料之后,第一台在打印过程中就可以去启动第二台打印机执行打印任务,依次类推...

二、进程基础(操作系统中的概念)

进程(资源单位):一个正在运行的程序,操作系统中正在运行的应用程序。进程之间是相互物理隔离的。因为如果未进行隔离的话,如果这一个程序被植入病毒入侵,那么就可以通过这个进程进而其它的进程,所以进程之间是互相隔离的。进程之间无法直接通信,需要通过特定的技术

线程(执行单位):操作系统能够机械能运行调度的最小单位。它被包含在进程中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程执行不同的任务

进程内必须有一条或之上的线程,每条线程执行不同的任务

1.什么是进程

程序:一堆躺在文件夹里面的代码(还没有运行)

其实是一个死的东西,一堆代码就是程序,它没有生命周期

进程:正在被运行的程序(代码正在运行)

它是有生命周期,这个任务做完,进程就不存在了

举个例子形象的说明:

  • 计算机的核心是CPU,它承担了所有的计算任务,它就像一座工厂,时刻在运行
  • 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
  • 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
  • 一个车间里,可以有很多工人。他们协同完成一个任务。
  • 线程就好比车间里的工人。一个进程可以包括多个线程。

或者用这个来说明:
厨师做饭,厨师做一道菜、应该有菜谱,按照菜谱的程序一步一步的做,在这个过程中,菜谱就是程序,程序就是路程. 做菜的过程就是进程,厨师就是线程,如果做菜完毕了,进程就不存在了,线程是进程中实际做事儿的。

2.进程的调度算法

进程和线程都是由操作系统来调度使用的,我们程序员是不能控制的,这里就涉及到了调度算法

'进程的调度算法'
	先来先服务算法:
		见名知意 第一个打开的程序先运行(针对后面打开耗时较短的程序不友好)

	短作业优先调度
		查看里面用时最少的程序先运行(针对耗时比较长的程序不友好)
	
	时间片轮转法+多级反馈队列
		将固定的时间均分成很多份,所有的程序来了都公平的分一份
		分配多次之后如果还有程序需要运行,则将其分到下一层(保证后面的程序能运行)
		越往下表示程序总耗时越长,每次分的时间片越多,就优先级越低

3.进程的并行与并发

'并行:统一时刻同时运行'
	多个进程同时执行,必须要有多个CPU核心参与,单核CPU无法实现并行
	eg:
		如果CPU是单核的,同一时刻能不能够做到同时执行多个任务。答:不能
		如果是多核心的,同一时刻是能够做到的
		如果核心是2个,同一时刻最多执行两个任务
		如果核心是4个,同一时刻最多执行四个任务
	
'并发:一段时间内看起来是同时运行'
	多个进程看上去像同时执行,单核CPU可以实现,多核心CPU肯定也可以实现
	eg:看视频、听音乐、浏览网页同时操作,这个单核是可以达到的,CPU的切换达到的

4.进程的三状态

在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入 几个状态:就绪态、运行态和阻塞态

在这里插入图片描述

就绪态

  • 当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。即所有的进程在被CPU执行之前都必须先进入就绪态等待。

运行态

  • 执行/运行(Running)状态当状态已获得处理机,其进程正在处理机上执行,此时的进程状态称为执行状态。

阻塞态

  • 阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如:等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。即进程运行过程中出来了IO操作,阻塞态无法直接进行运行态,需要先进入就绪态。

创建态和终止态:进程开始和结束的状态

代码演示:
在这里插入图片描述

5.同步异步

'同步异步主要用于描述任务的提交状态'
	同步:
		提交完成任务之后再原地等待任务的结果,期间不做任何事(单道技术)
		eg:去银行办理业务,选择排队等待(一直同步等待消息的通知)
	异步:
		提交完成任务之后,不在原地等待直接去做其他事情,结果自动提醒(多道技术)
		eg:取号排队,取号之后就可以做其他事,直到被窗口喊到取的号,再去柜台办理业务

6.阻塞与非阻塞

'阻塞与非阻塞主要用于描述进程的状态'
	阻塞(阻塞态)	等待运行完再接着下一步
	非阻塞(就绪态、运行态)	无需等待直接下一步
'''
当一个任务因调用或IO要获取某个结果,阻塞调用必须等待调用的结果,
非阻塞调用则可以不必等待这个结果而使进程始终处于就绪态或运行态
'''

7.同步异步与阻塞非阻塞综合使用

	同步阻塞:在银行排队,并且在队伍中什么事情都不做
	同步非阻塞:在银行排队,并且在队伍中做点其他事情
	异步阻塞:取号,在旁边座位上等着叫号,期间不可做事
	异步非阻塞:取号,在旁边座位上等着叫号,期间可以做别的事情
	'异步非阻塞效率最高'
'''
很多人会把同步和阻塞混淆,是因为很多时候同步操作会以阻塞的形式表示出来,
同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会再真正的IO操作处被阻塞。
'''

三、如何创建进程

'''
创建进程的两种方法:
    1.鼠标双击软件图标(打开程序 底部代码运行就是创建进程)
    2.代码创建进程(两种方法)  需要用到模块mutiprocessing、Process
'''

说明:
在不同的操作系统中创建进程底层原理不一样
	windows
	     以导入模块的形式创建进程,
	     需要用到if __name__ == '__main__':启动脚本(不然报错)
	      
	linux/mac
	     以拷贝代码的形式创建进程
	     不需要启动脚本 但是为了更好的兼容性 加上启动脚本更好


'''方法1:创建函数'''
通过Process类实例化得到一个对象,然后传入任务函数,调用对象.start开启进程

from multiprocessing import Process   # 导入模块
import time

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


'''现在这个写法还没有开启进程'''
'''在windows平台上必须写在__main__里面'''
if __name__ == '__main__':
    'def __init__(self, group=None, target=None, name=None, args=(), kwargs={},'
    'p1 = Process(target=任务函数,args=(任务需要的参数))'
    p1 = Process(target=task, kwargs={'name': 'tom', 'age': 19})  # 实例出来一个进程类,让这个进程执行task任务
    # p1 = Process(target=task,args=('jack',18))  # 位置传参
    p1.start()  # 真正的开启进程(异步)
    '''
    操作系统是负责把这个进程开起来
    开启一个进程来执行task任务,真正是谁执行的这个任务?是线程,进程里面至少有一个线程
    '''
    # task('chen',20)  # (同步)
    
    '进程的属性、方法'
    print(p1.name)  # 查看进程名称
    print(p1.pid) 	# 查看进程号
    
    print(p1.is_alive())  # 查看当前进程是否存活
    p.terminate() 	# 杀死进程
    
    '''
    	开启进程和杀死进程
       	只是通知操作系统去开进程,并不是立马把进程开起来,它需要消耗一定的时间,
       	侧面的反应了开启进程其实消耗很大
    '''
    
    print('执行主进程')

'''
运行结果为:
    同步方式下,先执行了task函数在进行主进程:
        task is running chen 20
        task is over chen 20
        主进程
    
    异步方式下:先执行了主进程,在进行了task函数
        主进程
        task is running tom 19
        task is over tom 19
'''


'''方法2:创建类'''
写一个类,继承Process,重写类的run方法,然后实例化得到对象,对象.start开启进程

from multiprocessing import Process  # 导入模块
import time

class MyProcess(Process):   #  继承Process类
    def __init__(self,name):
        super().__init__()
        self.name = name

    def run(self):
        print('start run', self.name)
        time.sleep(1)  # 睡眠1秒后运行
        print('end run', self.name)


if __name__ == '__main__':
    obj = MyProcess('jack')
    obj.start()  # 启动子进程,只有对象才能去点子进程
    print('主进程')

'''底层原理都是,先运行完父类的代码,创建一个进程空间运行子类的代码'''

Process的属性、方法

	'''Process的属性、方法'''
from multiprocessing import Process
import time


def task(name, age, gender):
    print(name, age, gender)
    print('子进程')


if __name__ == '__main__':
    '''
    只是通知操作系统去开进程,并不是立马把进程开起来,它需要消耗一定的时间,侧面的反应了开启进程其实消耗很大
    '''
    p = Process(target=task, args=(), kwargs={'name': 'chen', 'age': 18, 'gender': 'male'})
    # 实例化出来一个进程类,让这个进程执行task任务
    p.start()  # 真正的开启进程(异步)
    # 操作系统是负责把这个进程开起来,开启一个进程来执行task任务
    # 真正谁执行这个任务,是线程,进程里面至少要有一个线程
    '''进程的几个属性:1.进程名,2.进程号pid'''
    # 如何查看进程的名称
    # print(p.name)  # Process-1

    '如何修改进程名称'
    p.name = 'hello'
    print(p.name)  # hello

    '如何查看进程号'
    print(p.pid)  # Process pid

    print(p.is_alive())  # True 查看
    p.terminate()  # 杀死进程,结束任务
    # 系统并不是第一时间立马就杀死进程,需要给点时间
    time.sleep(1)
    print(p.is_alive())  # False

    p.join()  # 等待子进程的代码全部执行完毕,在走主进程的
    print('主进程')

如何开启多进程

	'''如何开启多进程'''
# 开启多进程就意味着可以同时做多个任务,一个进程做一个任务,多个进程肯定是做多个任务
from multiprocessing import Process
import time

def task(name):
    print(name)
    print('子进程')
    time.sleep(1)

if __name__ == '__main__':
    '''理论上你是可以一直开进程,但是你需要考虑资源的消耗情况'''
    start_time = time.time()
    ll = []
    for i in range(3):
        p = Process(target=task, kwargs = {'name':'jack'})  # 实例化一个进程类
        p.start()
        # p.join()  # 放在里面执行是同步串行 
        ll.append(p)

    for i in ll:
        i.join()

    print('主进程,总时间:',time.time() - start_time)
    

'''这种形式和下面的一样的意思,都是异步并发'''
if __name__ == '__main__':
    '''理论上你是可以一直开进程,但是你需要考虑资源的消耗情况'''
    start_time = time.time()

    p1 = Process(target=task, kwargs = {'name':'jack'})  # 实例化一个进程类
    p2 = Process(target=task, kwargs = {'name':'tom'})  # 实例化一个进程类
    p3 = Process(target=task, kwargs = {'name':'chen'})  # 实例化一个进程类
    '''
    这种就是异步并发,
    同一个时刻执行所有的任务
    结果耗时为1秒多
    '''
    p1.start()
    p2.start()
    p3.start()
    p2.join()
    p1.join()
    p3.join()
    '''
    这种就是同步串行,
    会等上一个执行完毕才执行下一个
    结果耗时为3秒多
    '''
    # p1.start()
    # p1.join()
    # p2.start()
    # p2.join()
    # p3.start()
    # p3.join()

    print('主进程,总时间:',time.time() - start_time)

基于TCP协议的高并发程序

import socket
'''一个服务端不能够同时给多个客户端发送数据'''
from multiprocessing import Process

def task(conn):
    while True:
        try:
            # 异常一个bug:粘包现象
            data = conn.recv(2048) # 括号里面写的是接收的字节数,最多接收2048个

            if len(data) == 0:
                continue
            print(data)

            conn.send(data.upper())
        except Exception as f:
            print(f)
            break

    conn.close()


if __name__ == '__main__':
    '''
    1.买手机
    # SOCK_STREAM  ====> 代表的是TCP协议
    # socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp协议
    '''
    server = socket.socket()  # 默认是TCP协议

    '''
    2.买手机卡
     # '0.0.0.0'  =====> 代表允许任何的ip链接
    # server.bind(('0.0.0.0', 8000)) # 服务端绑定一个地址
    '''
    server.bind(('127.0.0.1', 12306))  # 服务端绑定一个地址

    '''
    3.开机
    '''
    server.listen(1)  # 监听 半连接池
    print('服务端正在准备接收客户端消息:')
    while True:
        conn,addr = server.accept()
        # 接收,程序启动之后,会在accept这里夯住,阻塞
        p = Process(target = task, args=(conn,))
        p.start()


'''可以同时运行多个客户端并且实现并发'''
'''客户端'''
import socket

client = socket.socket()

# 连接服务端的IP地址和端口号
client.connect(('127.0.0.1',12306))
while True:
    # 发送数据给服务端
    mag = input('请输入发送的数据>>>>:').strip()
    client.send(mag.encode('utf-8'))

    # 接收服务端回应的数据 最大字节数
    data = client.recv(2048)
    print(f"接收都服务端的数据:",data.decode('utf-8'))

# 断开连接
client.close()

四、如何查看进程号

'''
如何查看进程号
    已知我们现在可以使用Process内置的属性来查看
    现在使用另一种方式来查看,需要使用到os模块
    os.getpid()  # 查看当前所在位置的进程号
    os.getppid()    # 查看当前所在位置的进程的父进程号
'''
import os
from multiprocessing import Process
'''有了进程号,我们就可以通过进程号来结束进程的执行, kill 进程号'''
def task():
    # os.getpid()写在那个位置就会输出那个位置的进程的进程号
    print('task进程的进程号:',os.getpid())
    print('task进程的父进程号:',os.getppid())
    

if __name__ == '__main__':
    p = Process(target=task,)
    p.start()
    # print(p.pid)  # 这是Process属性的查询方式

    print('子进程的进程号:',p.pid)
    print('主进程的进程号:',os.getpid())

得到的结果为:
>>>:子进程的进程号: 8956
>>>:主进程的进程号: 21516
>>>:task进程的进程号: 8956
>>>:task进程的父进程号: 21516

五、进程间的数据默认隔离

'''
进程间数据默认隔离
    多个进程数据彼此之间默认是互相隔离的(每个有各自的进程空间,互不干扰)
    如果真的想要交互,需要借助 管道和队列
'''
from multiprocessing import Process
money = 100  # 父进程的全局数据

def task():
    global money
    money = 666  # 子进程的局部money数据
    print('子进程打印的money:',money)


if __name__ == '__main__':
    p = Process(target= task,)
    p.start()
    p.join()  # 等待子进程运行完毕后执行父进程
    print('父进程打印的money:',money)
'''
最后得到的结果为:
    子进程打印的money: 666
    父进程打印的money: 100
所以证明了是互相隔离的,互不干扰
'''

'''那么我们如何让进程与进程之间数据通信呢?'''

在这里插入图片描述

六、进程之间的通信:IPC机制

队列

进程彼此之间相互隔离,如果要实现进程间的通信(IPC),multiprocessing模块提供了几种特别好用的方式,队列、管道。这两种方式都是实现消息传递的。这里主要学习队列的使用

'''
说到队列,就可以想到很多数据结构:
    链表、单链表、双链表、循环链表、栈、队列、树、二叉树、图
    
队列的特点:先进先出
栈的特点:先进后出
'''

from multiprocessing import Queue
# 括号内是最大可入列的数量,
Queue(5)  # 可以指定最大数量,不写的默认是无上限

队列的主要方法,以及属性

	'''队列的主要方法,以及属性'''
from multiprocessing import Queue

q = Queue(4)  # 指定队列可以存入的数据量,队列原则:先进先出

# 先put进入的,会被get先出来
q.put(1)  # 向队列中存入数据值
q.put(2)
q.put(3)
q.put(4)
'''block=False,如果往队列中放入数量值超过设定的队列大小,会立马报错,报队列已满的错误信息'''
# q.put(5)  # 当队列满了之后,再往里面放数据值,就会进入阻塞状态,也就会一直等待队列get值出来
q.put(5,block=False)  # 当队列满了之后直接抛出异常,不写的话,block是默认True
'''timeout:超时,如果在指定的时间内,没有放进去,就会报错'''
q.put(5, timeout=3)

print(q.get())  # 获取队列的第一个元素
print(q.get())  # 获取队列的第二个元素
print(q.get())  # 获取队列的第三个元素
print(q.get())  # 获取队列的第四个元素

q.get() # 当队列中没有值可以获取的时候,那么就会阻塞在原地,和上面的原理一样,get阻塞则等待队列put值出来
q.get(block=False)  # False则表示如果队列没有值可以取则抛出异常,默认为block=True(阻塞等待)
q.get(timeout = 3)  # 表示如果在3秒内,队列没有值可以get出来则报错

'''现在队列里面的数据值在哪里存着?在内存中存着'''
'专业的消息队列:kafka,rabbitmq等专业的消息队列 它们能够解决一些特殊场景的问题'


'''队列的其他方法、属性'''
from multiprocessing import Queue

q = Queue(3)
# 1.判断队列是否已满
print(q.full())  # 判断队列是否已满 False
q.put(1)
q.put(2)
q.put(3)
print(q.full())  # True

print(q.empty()) # 判断队列是否为空

# get_nowait()同get()方法一样,取值
print(q.get())  # 数据拿不到值,就会一直到等待,处于阻塞态
print(q.get_nowait())  # 如果队列没有数据,会直接报错
print(q.get_nowait())

'''
full() empty() 在多进程都不能使用,判断会出现失误,
执行q.full()是True,在极限情况下,当队列满的时候,当一个进程执行q.get(),队列的数据会被拿走
执行q.empty()是True,在极限情况下,队列是空的时候,你执行q.empty()是空的下一秒有一个进程往里面存数据
此时的队列不是空的
'''

七、守护进程

'''
什么是守护进程
    伴随着守护对象的存活而存活 死亡而死亡
    需要用到daemon
'''
from multiprocessing import Process
import time

def task():
    print('start')
    time.sleep(1)
    print('end')


if __name__ == '__main__':
    p = Process(target=task,)
    p.daemon = True  # 将子进程设置为守护进程,主进程代码结束,子进程立即结束
    p.start()
    # p.join()
    # p.daemon 一定要在p.start之前执行,如果在之后执行会报错

    print('执行主进程')

八、僵尸进程与孤儿进程

僵尸进程
	# 为什么主进程默认需要等待子进程结束才会结束
	所有的子进程在运行结束之后都会变成僵尸进程(死了没死透)
	还保留着pid和一些运行过程的中的记录便于主进程查看(短时间保存)
	这些信息会被主进程回收(僵尸彻底死了)
	  1.主进程正常结束
	  2.调用join方法
 
孤儿进程
	# 子进程存活着 父进程意外死亡
	子进程会被操作系统自动接管(儿童福利院)

九、生产者与消费者模型

'''
    生产者:产生数据
    消费者:处理数据
        例如爬虫 生产者:获取网页数据的代码(函数)爬
                消费者:从网页数据中筛选符合条件的数据(函数)筛选
    完整的生产者消费者模型至少有三个部分
        生产者 消息队列/数据库 消费者
'''

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

from multiprocessing import Process,Queue,Lock
import time,random
# 生产者
def producer(q,name,food,lock):
    # 先生产是个包子
    for i in range(1,4):  # 每种食物产出3个
        lock.acquire()  # 上锁
        q.put(food+str(i))
        print(f"{name}产出了{i}{food}")
        time.sleep(random.randint(1,3))
        lock.release()  # 释放锁
    q.put(None)

# 消费者
def consumer(q,name,lock):
    while True:  # 循环执行吃的过程
        res = q.get()  # 获取队列中的食物
        if res is None:  # 当队列数据值为空时执行
            break

        lock.acquire()  # 上锁
        time.sleep(random.randint(1,3))
        print(f"{res}{name}吃掉了")
        lock.release()  # 释放锁

if __name__ == '__main__':
    q = Queue(20)  #
    lock = Lock()  # 创建一把锁

    # 定义生产者
    p1 = Process(target=producer, args=(q,'jack','包子',lock,))
    p2 = Process(target=producer, args=(q,'tom','糯米鸡',lock,))
    p3 = Process(target=producer, args=(q,'ankn','烧麦',lock,))

    # 开始生产food
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()

    # 定义消费者
    c1 = Process(target=consumer, args=(q,'jason',lock,))
    c2 = Process(target=consumer, args=(q,'oscer',lock,))

    # 开始吃food
    c1.start()
    c2.start()
    c1.join()
    c2.join()

    # p.join()
    # c.join()
    # q.put(None)

    print('执行主进程')
  • 15
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值