Python--第二阶段--04.并发编程多进程和多线程

多任务编程

  1. 意义:充分利用计算机多核资源,提高程序的运行效率,一个程序中同时使用多个任务。
  2. 实现方案:多进程,多线程
  3. 并发与并行
    并发:同时处理多个任务,内核在任务间不断的切换达到好像多个任务同时执行的效果,实际每个时刻只有一个任务占有内核。
    并行:多个任务利用计算机多核资源在同时执行,此时多个任务间并行关系。

进程(process)

进程理论基础

  1. 定义:程序在计算机中的一次运行。

    程序是一个可执行的文件,是静态的占有磁盘。
    进程是一个动态的过程描述,占有计算机运行资源,有一定的生命周期(开始运行生成进程,结束运行结束进程)。

  2. 系统中如何产生一个进程
    【1】用户控件通过调用程序接口或者命令发起请求(应用层发起请求)
    【2】操作系统接收用户请求,开始创建进程(操作系统接收请求)
    【3】操作系统调配计算机硬件资源,确定进程状态等
    【4】操作系统将创建的进程提供给用户使用

  3. 进程基本概念

  • cpu时间片:如果一个进程占有cpu内核则称这个进程在cpu时间片上。

  • PCB(进程控制块):在内存中开辟的一块控件,用于存放进程的基本信息,也用于系统查找识别进程。

  • 进程ID(PID):系统为每个进程分配的一个大于0的整数,作为进程ID。每个进程ID不重复。(linux查看进程ID:ps -aux)

  • 父子进程:系统中每一个进程(除了系统初始化进程)都有唯一的父进程,可以有0个或多个子进程。父子进程关系便于进程管理,父进程初始化了子进程。
    查看进程树:pstree

  • 进程状态:
    三态:

    • 就绪态:进程具备执行条件,等待分配CPU资源
    • 运行态:进程占有cpu时间片正在运行
    • 等待态:进程暂时停止运行,让出cpu
      Python--第二阶段--04.并发编程多进程和多线程_第1张图片
      五态(在三态基础上增加新建和终止)
      新建:创建一个进程,获取资源的过程
      终止:进程结束,释放资源的过程
      Python--第二阶段--04.并发编程多进程和多线程_第2张图片
    • 状态查看命令:ps-aux–>STAT列
      S 等待态
      R 执行态(就绪态)两态之间转换
      D 等待态
      T 等待态
      Z 僵尸态

    < 有较高优先级
    N 优先级较低
    +前台进程
    s 会话组组长
    l 有多线程的

  • 进程的运行特征
    【1】进程可以使用计算机多核资源
    【2】进程是计算机分配资源的最小单位
    【3】进程之间的运行互不影响,各自独立
    【4】每个进程拥有独立的空间,各自使用自己空间资源

  • 面试要求

    1. 什么是进程,进程和程序有什么区别
    2. 进程有哪些状态,状态之间如何转化

fork模块

os模块的多进程,创建进程,返回创建进程的子进程的进程ID,即pid。

孤儿进程

子进程还没结束,但是父进程已经结束了,这时候子进程没有对应的父进程,就称为了孤儿进程,系统会有一个专门回收孤儿进程的父进程,在linux系统开机后就存在。

僵尸进程

  • 僵尸进程的处理方法
    • os.wait
    • 创建二级子进程
    • 信号函数
      import signal
      signal.signal(signal.SIGCHLD,signal.SIG_IGN)

作业:

  1. fork理解和使用
  2. IO函数总结
  3. 群聊思路

multiprocessing多进程

from multiprocessing import Process
from time import sleep
import os

def th1():
    sleep(2)
    print("吃饭")
    print(os.getppid(),'----',os.getpid())

def th2():
    sleep(2)
    print("睡觉")
    print(os.getppid(),'----',os.getpid())
def th3():
    sleep(2)
    print("打豆豆")
    print(os.getppid(),'----',os.getpid())

if __name__ == '__main__':

    things = [th1,th2,th3]
    jobs = []
    for th in things:
        p = Process(target=th)  # 可以用args元组的位置传参,也可以用kwargs用字典键值传参
        p.start()
        jobs.append(p)   # 将进程对象保存

    for i in jobs:    # 遍历进程列表,回收进程
        i.join()

muutiprocessing传参

from multiprocessing import Process
from time import sleep

def worker(sec,name):
    for i in range(3):
        sleep(sec)
        print("I'm %s"%name)
        print("i am working")

if __name__ == '__main__':

    # args传参
    p = Process(target=worker,args=(2,'sunhao'))
    # kwargs传参
    # p = Process(target=worker,kwargs={'sec':2,'name':'sunhao'})
    p.start()
    p.join()

进程间的互相访问(IPC)

管道通信 
	函数:Pipe()/send()/recv()
		fd1,fd2 = Pipe(duplex = True)    #默认True,双向通道,fd1写的只能是fd2读,False,单向通道,一个只能读,一个只能写
fd1.send()
fd2.recv()

消息队列:Queue()/q.get()/q.put()
共享内存:Value() Array()
信号量:Semaphore()/acquire()/release()

进程池

原理:创建一定数量的进程来处理事件,事件处理完成进程不退出,而是继续处理其他事件,直到所有事件全部处理完毕统一销毁,增加进程的重复利用,降低资源消耗。
就是因为原始的多进程方式会不断的创建进程,关闭进程,造成资源浪费,但是进程池就是创建进程后就创建好的这几个进程来回执行任务,直到任务完成,不会频繁创建和关闭。
Windows下实现进程池的代码,注意,建立进程池必须在main下,否则在Windows下运行不起来,具体原因参照:一吱大懒虫的博客 https://blog.csdn.net/qq_36708806/article/details/79731276

from multiprocessing import Pool
from time import ctime,sleep
# 进程池事件
def worker(msg):
    sleep(2)
    print(ctime(),'--',msg)
if __name__ == '__main__':

    # 创建进程池
    pool = Pool(2)
    # 向进程池队列添加事件
    for i in range(20):
        msg = 'sunhao %d'%i
        pool.apply_async(func=worker,args=(msg,))
	# 关闭进程池
    pool.close()
    # 回收进程池
    pool.join()

线程编程

线程的基本概念

  1. 什么是线程
    【1】 线程被称为轻量级的进程
    【2】 线程也可以使用计算机多核资源,是多任务编程方式
    【3】 线程是系统分配内核的最小单元
    【4】 线程可以理解为进程的分支任务
  2. 线程的特征
    【1】 一个进程中可以包含多个线程
    【2】 线程也是一个运行行为,消耗计算机资源
    【3】 一个进程中的所有线程共享这个进程的资源
    【4】 多个线程之间的运行互不影响各自运行
    【5】 线程的创建和销毁消耗资源远小于进程
    【6】 各个线程也有自己的ID等特征

treading模块创建线程

【1】 创建线程对象

from threading import Thread 

t = Thread()
功能:创建线程对象
参数:target 绑定线程函数
 args   元组 给线程函数位置传参
 kwargs 字典 给线程函数键值传参

【2】 启动线程

 t.start()

【3】 回收线程

t.join([timeout])

示例:各线程拥有相同的内存空间,所以共有的变量a只要一边改变了,另一边也改变。

from threading import *
from time import sleep
import os

a = 1
# 线程函数
def music():
    global a
    a = 10000
    for i in range(3):
        sleep(2)
        print(os.getpid(),'播放:黄河大合唱')
# 创建线程对象
t = Thread(target=music)
# 开启线程
t.start()
for i in range(4):
    sleep(1)
    print(os.getpid(),'播放:葫芦娃')
    print(a)
# 回收线程
t.join()

传参及如何批量创建线程

from threading import  Thread
from time import sleep

def fun(sec,name):
    print('线程函数参数')
    sleep(sec)
    print('%s执行完毕'%name)

jobs = []
# 循环创建线程
for i in range(5):
    t = Thread(target=fun,args=(2,),kwargs={'name':'T%d'%i})
    jobs.append(t)
    t.start()
# 循环关闭线程
for i in jobs:
    i.join()

线程对象的属性

t.name 线程名称
t.setName() 设置线程名称
t.getName() 获取线程名称

t.is_alive() 查看线程是否在生命周期

t.daemon 设置主线程和分支线程的退出关系
t.setDaemon() 设置daemon属性值
t.isDaemon() 查看daemon属性值

daemon为True时主线程退出分支线程也退出。要在start前设置,通常不和join一起使用。

线程池模块:threadpool

"""
thread_attr.py
线程属性示例
"""

from threading import Thread
from time import sleep

def fun():
    sleep(3)
    print("线程属性示例")

t = Thread(target = fun,name = "Tarena")

t.setDaemon(True) # 主线程退出分支线程也退出

t.start()

t.setName("Tedu")
print("Name:",t.getName()) # 线程名称
print("is alive:",t.is_alive()) # 是否在生命周期
print("Daemon:",t.isDaemon())
# t.join()

自定义线程类

  1. 创建步骤:①继承Thread,②重写__init__使用super加载父类属性,③重写run方法。
  2. 使用方法:①实例化对象,②调用start自动执行run,③调用join回收线程

线程的同步互斥

同步互斥的方法

  1. Event() wait()set() clear()
  2. Lock() acquire()release()

死锁:由于上锁造成的程序阻塞

Python线程GIL

  1. Python线程的gil问题(全局解释器)
    什么是GIL:由于Python解释器设计中加入了解释器锁,导致Python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。

    导致后果:因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以,Python多线程在执行多阻塞高延迟IO时可以提升程序效率,其他情况下并不能对效率提升。
    GIL问题建议:
    尽量使用进程完成无阻塞的并发行为;
    不适用c作为解释器(java C#)

  2. 结论
    在无阻塞状态下,多线程程序和单线程程序执行效率几乎差不多,甚至还不如单线程效率。但是多进程运行相同内容却可以有明显的效率提升。

进程线程的区别

区别联系:

并发网络通信模型

  • 常见模型分类
  1. 循环服务器模型:循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。
    优点:实现简单,占用资源少
    缺点:无法同时处理多个客户端请求
    适用情况:处理的任务可以快速完成,客户端无需长时间占用服务端程序。UDP比tcp更适合循环。
  2. IO并发模型:利用IO多路复用,异步IO等技术,同时处理多个客户端IO请求。
    优点:资源消耗少,能同时高效处理多个IO行为
    缺点:只能处理并发产生的IO事件,无法处理CPU计算
    使用情况:HTTP请求,网络传输等都是IO行为。
  3. 多进程/线程网络并发模型:每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程。
    优点:能同时满足多个客户端长期占有服务端请求,可以处理各种请求。
    缺点:资源消耗较大
    使用情况:客户端同时连接量较少,需要处理行为较复杂情况。

基于fork的多进程网络并发模型

'''
基于fork的多进程网络并发模型server
'''

import os,signal
from socket import *
# signal处理僵尸进程
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
# 创建TCP套接字
ADDR = ('0.0.0.0',8887)
sk = socket()
sk.bind(ADDR)
# 允许端口复用
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
sk.listen()

# 循环创建子进程进行连接
while True:
    conn, addr = sk.accept()
    pid = os.fork()
    if pid == 0:
        sk.close()
        while True:
            try:
                data = conn.recv(1024)
                print(data.decode())
                conn.send(b'OK')
            except Exception as e:
                print(e)
                os._exit(0)
    else:
        conn.close()
sk.close()

练习:根据fork多进程并发网络模型思路,完成基于process的多进程并发网络模型

'''
基于processing的多进程并发网络模型
'''

from multiprocessing import Process
import os,signal
from socket import socket
signal.signal(signal.SIGCHLD,signal.SIG_IGN)

ADDR = ('0.0.0.0',8887)
sk = socket()
sk.bind(ADDR)
sk.listen()


def connect_do(conn):
    while True:
        try:
            data = conn.recv(1024)
            print(data.decode())
            conn.send(b'OK')
        except Exception as e:
            print(e)
            os._exit(0)

while True:
    conn, addr = sk.accept()
    p = Process(target=connect_do,args=(conn,))
    p.daemon = True
    p.start()

基于threading的多线程网络并发

'''
基于threading的多线程并发网络模型
'''

from threading import Thread
import os,signal
from socket import socket
signal.signal(signal.SIGCHLD,signal.SIG_IGN)

ADDR = ('0.0.0.0',8885)
sk = socket()
sk.bind(ADDR)
sk.listen()


def connect_do(conn):
    while True:
        try:
            data = conn.recv(1024)
            print('recv',data.decode())
            conn.send(b'OK')
        except Exception as e:
            print(e)
            break
            # os._exit(0)

while True:
    conn, addr = sk.accept()
    p = Thread(target=connect_do,args=(conn,))
    p.daemon = True
    p.start()

IO并发

  • IO分类
    阻塞IO,非阻塞IO,IO多路复用,异步IO等
  • 阻塞IO
  1. 定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。
  2. 效率:阻塞IO是效率很低的一种IO。但是由于逻辑简单,所以是默认IO行为。
  3. 阻塞情况:
    • 因为某种执行条件没有满足造成的函数阻塞:accept、input、recv—可以通过修改属性行为来完成
    • 处理IO的时间较长产生的阻塞状态:网络传输,大文件读写—只能提高硬件
  • 非阻塞IO
    定义:通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。

    • 设置套接字为非阻塞IO
      sockfd.setblocking(bool)
      功能:设置套接字为非阻塞IO
      参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞
    • 超时检测:设置一个最长阻塞时间,超过该时间后则不再阻塞等待。
      sockfd.settimeout(sec)
      功能:设置套接字的超时时间
      参数:设置的时间
  • IO多路复用

  1. 定义
    同时监控多个IO时间,当哪个IO时间准备就绪就执行哪个IO事件,以此形式可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高IO的执行效率。
  2. 具体方案:通过select模块实现
    • select方法:Windows Linux Unix
    • poll方法:linux Unix
    • epoll方法:Linux

select模块

select方法(最多监控1024个)
  • rs, ws, xs = select(rlist, wlist, xlist, timeout)
  • 功能:监控IO事件,阻塞等待IO发生
  • 参数:
    • rlist 列表 存放关注的等待发生的IO事件
    • wlist 列表 存放关注的要主动处理的IO事件
    • xlist 列表 存放关注的出现异常要处理的IO
    • timeout 超时时间
  • 返回值:
    • rs 列表 rlist中准备就绪的IO
    • ws 列表 wlist中准备就绪的IO
    • xs 列表 xlist中准备就绪的IO
  • 代码:select_server_tcp服务
import select
from socket import socket
# 创建TCP套接字
sk = socket()
sk.bind(('0.0.0.0',8888))
sk.listen()
# 初始化rlist读监控事件列表
rlist = [sk,]
while True:
    rs,ws,xs = select.select(rlist,[],[])
    for i in rs:
        if i == sk:
            conn,addr = i.accept()
            print(addr,'connected')
            rlist.append(conn) # 将连接套接字加入到监听list中
        else:
            data = i.recv(1024)
            print(data.decode())
            try:
                i.send(b'OK')
            except Exception as e:
                print(e)
                i.close()
                rlist.remove(i)# 将连接调节自从监听list中删除
位运算
* 运算符号(二进制)
	* & 按位与---一0则0    (判别属性是否存在)
	* | 按位或---一1则1   (增加属性)
	* ^ 按位抑或---相同为0,不同为1
	* << 向左移动低位补0
	* >> 向右移动去掉低位
* 作用:属性中如果都是bool型的,那可以用二进制数字来代替这些属性,规定成一个属性。
poll方法(监控个数很多)
  • p = select.poll()
    功能:创建poll对象
    返回值:poll对象

  • p.register(fd,event)

    • 功能:注册关注的IO事件
    • 参数:
      • fd 要关注的IO
      • event 要关注的IO时间类型
        • 常见类型:
          • POLLIN(读IO事件rlist)
          • POLLOUT(写IO事件wlist)
          • ROLLERR(异常IOxlist)
          • ROLLHUP(断开连接)
          • eg. 用按位或连接多个类型:p.register(sockfd,POLLIN|POLLERR)
  • p.unregister(fd)

    • 功能:
    • 参数:fd可以是监控对象,也可以是fileno(文件描述符)
  • events = p.poll()

    • 功能:阻塞等待监控的IO事件发生
    • 返回值:返回发生的IO
    • events格式 [(fileno,event),()…]
    • 通过文件描述符找到对应的IO对象,建立fileno:IO对象的字典

poll_server代码:

from select import *
from socket import socket
# 创建TCP套接字
sk = socket()
sk.bind(('0.0.0.0',8888))
sk.listen()
# 创建poll对象
p = poll()
# 初始化fileno和对应套接字的字典
event_dict ={sk.fileno():sk}
# 将套接字加入到关注中
p.register(sk,POLLIN)
while True:
    events = p.poll()  #阻塞监控
    # 循环取events中的fileno,从event_dict中获取对应的套接字
    for f,e in events:
        print(f,e)
        print(event_dict[f])
        # 判断套接字类型,并根据相关套接字类型做不同的操作。
        if event_dict[f] == sk:
            conn,addr = event_dict[f].accept()
            p.register(conn, POLLIN)
            event_dict[conn.fileno()] = conn
        else:
            data = event_dict[f].recv(1024)
            print(data.decode())
            try:
                event_dict[f].send(b'OK')
            except Exception as e:
                print(e)
                event_dict[f].close()          
                p.unregister(event_dict[f]) # 将断开的连接剔除出监听
epoll方法(逻辑上和poll一样,语法相似)
  1. 使用方法:基本与poll相同
    • 生成对象改为epoll()
    • 将所有时间类型改为EPOLL类型
  2. epoll特点
    • epoll效率比select和poll高
    • epoll监控IO数量比select多
    • epoll的触发方式比poll要多(EPOLLET边缘触发)
      epoll_server代码:
from select import *
from socket import socket

sk = socket()
sk.bind(('0.0.0.0', 8881))
sk.listen()

ep = epoll()

event_dict = {sk.fileno(): sk}
ep.register(sk, EPOLLIN)
while True:
    events = ep.poll()
    print(events)
    for f, e in events:
        print(f, e)
        print(event_dict[f])
        if event_dict[f] == sk:
            conn, addr = event_dict[f].accept()
            ep.register(conn, EPOLLIN)
            event_dict[conn.fileno()] = conn
        else:
            data = event_dict[f].recv(1024)
            print(data.decode())
            try:
                event_dict[f].send(b'OK')
            except Exception as e:
                print(e)
                ep.unregister(event_dict[f])

作业:

  1. 重点代码自己能写;
  2. http1.0和http协议;
  3. 做一个复习计划,3周左右。函数编程、面向对象、闭包和装饰器、数据结构算法、进程线程网络

协程技术

基础概念

  1. 定义:纤程,微线程。是允许在不同入口点不同位置暂停或开始的计算机程序,简单来说,协程就是可以暂停执行的函数。
  2. 协程原理:记录一个函数的上下文,协程调度切换时会将记录的上下文保存,在切换回来时进行调取,恢复原有的执行内容,以便从上一次执行位置继续执行。
  3. 协程优缺点:
    • 优点:
      • 协程完成多任务占用计算机资源很少
      • 由于协程的多任务切换在应用层完成,因此切换开销少
      • 协程为单线程程序,无需进行共享资源同步互斥处理
    • 缺点:
      • 由于是个单线程,无法利用计算机多核资源

标准库协程实现

asyncio和async/await,由于生态不好,所以不常用

第三方协程模块

1.greenlet模块

你可能感兴趣的:(Python全栈,python,多线程)