python 从入门到精通——多任务、多线程编程

文章目录

    • 4.1多任务编程
    • 4.3 基于fork的多进程编程
      • 4.3.2进程相关函数
      • 4.3.3孤儿和僵尸
      • 4.3.4群聊聊天室
    • 4.4 multiprocessing 模块创建进程
      • 4.4.1 进程创建方法
      • 4.4.2 进程池实现
    • 4.5 进程间通信(IPC)
      • 4.5.1 管道通信(pipe)
      • 4.5.2 消息队列
      • 4.5.3 共享内存
      • 4.5.4 本地套接字
      • 4.5.5 信号量(信号灯集)
    • 4.6 线程编程(Thread)
      • 4.6.1 线程基本概念
      • 4.6.2 Threading 模块创建线程
      • 4.6.3 线程对象属性
      • 4.6.4 自定义线程类
    • 4.7 同步互斥
      • 4.7.1 线程间通信方法
      • 4.7.2 线程同步互斥方法
    • 4.7.3 死锁及其处理

4.1多任务编程

1.意义:充分利用计算机多核资源,提高程序的运行效率。
2.实现方案:多进程,多任务。
3.并发和并行:

  • 并发:同时处理多个任务,内核在任务间不断的切换,达到好像多个任务被同时执行的效果,实际上每个时刻只有一个任务占用内核。
  • 并行:多个任务利用计算机多核资源同时执行,此时多个任务间为并行关系。
    心得:为什么并发能提高执行效率?
    因为在程序中存在多个IO操作。
    ##4.2进程 Process
    ###4.2.1进程理论基础
    1.定义:程序在计算机中的一次运行。
  • 程序是一个可执行的文件,是静态的占有磁盘。
  • 进程是一个动态的过程描述,占有计算机运行资源,有一定的生命周期。

2.系统的集成是如果产生的——产生过程

  1. 用户控件通过调用程序接口或者命令发起请求。
  2. 操作系统接收用户请求命令,开始创建进程。
  3. 操作系统开始调用计算机资源,确定进程状态等。
  4. 操作系统将创建的进程提供给用户使用。
    进程流程

3.进程基本概念

  • cpu时间片:如果一个进程占有CPU内核,则称这个进程在cpu时间片上。
  • PCB(进程控制块):在内存中开辟的一块空间,用于存放进程的基本信息,也用于系统查找识别进程。
  • 进程ID:系统为每个进程分配一个大于0的整数,作为进程id,每个进程id不重复。
    在Linux查看进程可以使用命令:ps -aux
  • 父子进程:系统中每一个进程(除了系统初始化进程)都有唯一的父进程,可以有0个或者多个子进程。父子进程关系便于进程管理。
    可以查看进程数,linux命令:pstree
  • 进程状态
    • 三态
      • 就绪态:进程具备执行条件,等待分配cpu资源。
      • 运行态:进程占有cpu时间片正在运行。
      • 等待/等待态:进程暂时停止运行,让出CPU。
        查看进程装填,在Linux终端中使用命令:ps -aux,其中的status表示状态。
        s:表示普通的等待态。D:表示不可中断的等待态(不能被外部信号终端)
status 描述
S 等待态
R 执行态
D 等待态
T 等待态
Z 僵尸态

status状态后面有的字母代表的意思:

符号 示意
< 有较高优先级
N 优先级较低
+ 前台进程(在终端有现象显示的)
s 会话组组长
l 有多线程的
  • 进程的运行特征
    1. 进程可以使用计算机多核资源
    2. 进程是计算机分配资源的最小单位
    3. 进程之间运行互不影响,各自独立
    4. 每个进程拥有独立的空间,各自使用自己空间资源

面试要求
1.什么是进程?进程和程序之间有什么区别?
2.进程有哪些状态?状态之间如何转化?


4.3 基于fork的多进程编程

(系统层级的编程接口,在Linux或者Unix下进行)
###4.3.1fork的使用
pid = os.fork()
功能:创建新的进程
返回值:整数,如果进程创建失败则返回负数,如果创建成功,则在原有进程中返回新进程的PID,在新进程中返回0

#fork函数演示
import os
pid = os.fork()
if pid < 0:#创建进程失败
    print("Create process failed")
elif pid == 0:#成功创建子进程
    print("The new prcess is created")
else:#父进程
    print("The old process")

print("fork test over")

注意

  • 子进程会复制父进程全部进程空间,从fork下一句开始执行。
  • 父子进程独立运行,运行顺序不一定。
  • 在pycharm中执行必须基于Linux或者Unix系统,在win系统下或报错。
    AttributeError: module 'os' has no attribute 'fork'
  • 利用父子进程fork返回值的区别,配合if结构让父子进程执行不同的内容几乎是固定分配的。
  • 父子进程有各自特有的特征,比如PID、PCB命令集等。
  • 父进程fork之间开辟的空间子进程同样拥有,父子进程对各自空间的操作互不影响。
    ** 心得 **:

    1.在fork之后会复制父进程的全部代码和空间。
    2.复制成功后,父进程自己执行,子进程在代码的fork之后进行,同时子进程fork的返回值是0.
    3.父进程和子进程之间的体现在于代码块中if的条件不同,因为父进程的fork返回值不是0。
    4.这里的返回值只是返回值,和pid号没有关系。

4.3.2进程相关函数

函数名 返回值 功能
os.getpid() 返回当前进程的pid 获取一个进程的pid
os.getppid() 返回父进程的pid 获取父进程的pid号
os._exit(status) 参数:进程的终止条件 结束一个进程
sys.exit([status]) 参数:整数 表示退出状态,字符串 表示退出时打印内容 退出进程

4.3.3孤儿和僵尸

1.孤儿进程:父进程先与子进程退出,此时子进程成为孤儿进程。

特点:孤儿进程会被系统进程收养,此时系统进程就会成为孤儿进程新的父进程,孤儿进程退出该进程会自动处理。

2.僵尸进程:子进程先于父进程退出,父进程又没有处理子进程的退出状态,此时子进程就被成为僵尸进程。

特点:僵尸进程虽然结束,但是会存留部分PCB在内存中,大量的僵尸内存会浪费系统的内存资源。

3.如何避免僵尸进程产生

  • 使用wait函数处理子进程退出。
  • 代码示例:自己写 ,P2-1 02:57
"""
    创建二级子进程,放置僵尸进程
"""
import os
import time

def fun01():
    for i in range(4):
        time.sleep(2)
        print("写代码····")

def fun02():
    for i in range(5):
        time.sleep(1)
        print("测试代码····")

pdidd = os.fork()
if pdidd < 0:
    print("Error")
elif pdidd == 0:
    p = os.fork()#二级子进程 
    if p == 0:
        fun02()
    else:
        os._exit(0)
else:
    os.wait()
    fun01()
  • 通过信号处理子进程的退出
    • 原理:子进程退出时会发出信号给父进程,如果父进程忽略子进程信号,则系统会自动处理子进程退出。
    • 方法:
      使用signal模块在父进程创建子进程前写下如下语句:
import signal
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
特点:非阻塞,不会影响父进程运行,可以处理所有子进程退出。

4.3.4群聊聊天室

功能:类似于QQ群功能

  • 有人进入聊天室需要输入姓名,姓名不能重复。
  • 有人进入聊天室时,其他人会收到通知,XXX进入了聊天室。
  • 一个人发消息,其他人会受到XXX:xxxxxxxxxx
  • 有人退出聊天室时,则其他人也会收到通知:XXX退出了聊天室。
  • 扩展功能:服务器可以向所有用户发送公告,管理员消息:xxxxxxxxxx。

思路
1.技术点

  • 转发模型:客户端——>服务端——>转发给其他客户端
  • 网络通信方式:选择UDP通信.
  • 保存用户信息:[(name,addr)]或者{name:addr…}
  • 收发是同时执行的,不可能是先接受再发送。所以这里可能需要多进程。

2.结构设计

  • 采用什么样的数据结构?函数
  • 原则:编写一个模块,测试一个功能。
  • 注意注释和结构的设计。

3.分析功能模块,制定编写流程

  • 搭建网络连接

  • 进入聊天室

    • 客户端:

      1. 输入姓名
      2. 将姓名发送到服务端
      3. 接受返回的结果,若不允许则重复输入姓名
        4.如果允许则进入聊天室。
    • 服务端:

      1. 接受姓名
      2. 判断姓名是否存在
      3. 将结果反馈到客户端
      4. 如果允许进入聊天室,增加用户信息
      5. 通知其他用户
  • 聊天

  • 客户端

    • 创建新的进程
      • 一个进程循环发送消息
      • 一个进程循环接受消息
    • 服务端:
      • 接受请求,判断请求类别
      • 将消息转发给其他用户
  • 退出聊天室

    • 客户端:
      • 输入quit或者ctrl-c退出
      • 将请求发送给服务端
      • 结束进程(发送消息进程)
      • 接收端接收到服务端EXIT,退出,并结束进程(接收消息进程)
    • 服务端:
      • 接受消息
      • 将退出消息告知其他用户
      • 给该用户发送EXIT
      • 删除用户
  • 管理员消息

    • 接受请求,判断请求类型
    • 将消息转发其他用户

4.协议

  • 如果允许进入聊天室,服务端发送OK给客户端。

  • 如果不允许进入聊天室,服务端发送不允许的原因。

  • 请求类别:

    • L:进入聊天室
    • C:聊天信息
    • Q:退出请求
  • 客户端如果输入quit或者ctrl-c,或者esc表示退出

  • 用户存储结构:{name:addr,…}

代码示例(完整代码)
注意:因为使用fork函数,win下不行,必须在Linux或者Unix下运行,(同时这里并没有图像界面,所以这里只是在终端运行,测试收发消息。)

"""
    群聊天服务端
"""
import socket,os,sys

ADDR = ('0.0.0.0',10006)
user = {}
def do_login(s,name,addr):
    if name in user or "管理员" in name:
        s.sendto("该用户已经存在".encode(),addr)
        return
    s.sendto(b'OK',addr)

    #通知其他人
    msg = "欢迎%s进入聊天室!"%name
    for i in user:
        s.sendto(msg.encode(),user[i])
    #将用户加入
    user[name] = addr

def do_chat(s,name,text):
    """
        聊天
    :param s:socket套接字对象
    :param name:姓名
    :param text:聊天内容
    :return:
    """
    msg = "%s : %s"%(name,text)
    for i in user:
        if i != name:
            s.sendto(msg.encode(),user[i])

def do_quit(s,name):
    """
    服务端退出聊天室
    :param s:
    :param name:
    :return:
    """
    msg = "%s退出聊天室"%name
    for i in user:
        if i != name:
            s.sendto(msg.encode(),user[i])
        else:
            s.sendto(b'EXIT',user[i])
    #将用户删除
    del user[name]

def do_request(sockfd):
    """
        处理请求
    :param sockfd:
    :return:
    """
    while True:
        data,addr = sockfd.recvfrom(4029)
        # print(data.decode())
        msg = data.decode().split(' ')
        #区分请求类别
        if msg[0] == 'L':
            do_login(sockfd,msg[1],addr)
        elif msg[0] == 'C':
            text = ' '.join(msg[2:])
            do_chat(sockfd,msg[1],text)
        elif msg[0] == 'Q':
            do_quit(sockfd,msg[1])

#创建网络连接
def main():
    #套接字
    sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sockfd.bind(ADDR)
    pid = os.fork()
    if pid < 0:
        return
    elif pid == 0:#子进程专门用来发送管理员消息
        """
            注意:由于创建新进程,同时会创建父进程中所有的数据,当然也是会创建user的数据
                但是,所有添加用户都是在父进程中执行的,所以子进程中的user是一直为空。
                因为子进程要将管理员消息发送给所有用户,所以?
                可以考虑,服务端将管理员消息发送给服务端,即发送给自己,通过协议C表示聊天信息,
                让子进程发送的消息发送给父进程,让父进程去处理。
                还需要考虑的是:因为是子进程发送给父进程,父进程判断name为“管理员消息”,如果
                user表中已存在“管理员消息”这个用户该怎么办?
        """
        while True:
            msg = input("管理员消息:")
            msg = "C 管理员消息 " + msg
            sockfd.sendto(msg.encode(),ADDR)
    else:
        #请求处理
        do_request(sockfd)#父进程专门处理客户端请求

if __name__ == '__main__':
    main()


"""
    群聊天客户端
"""
import socket,os,sys

ADDR = ('127.0.0.1',10006)
def send_msg(s,name):
    """
    发送消息
    :param s:socket套接字对象
    :param name:发送者姓名
    :return:
    """
    while True:
        try:
            text = input("发言:")
        except KeyboardInterrupt:
            text = 'quit'
        if text == 'quit':#表示退出聊天室
            msg = "Q "+ name
            s.sendto(msg.encode(),ADDR)
            sys.exit("退出聊天室")
        msg = "C %s %s"%(name,text)#定义协议C:发言内容
        s.sendto(msg.encode(),ADDR)
def recv_msg(s):
    """
    接受消息
    :param s: socket套接字对象
    :return:
    """
    while True:
        data,addr = s.recvfrom(4096)
        #服务端发送exit,表示让客户端退出
        if data.decode() == "EXIT":
            sys.exit()
        print(data.decode())

#创建网络连接
def main():
    sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while True:
        name = input(">>")
        msg = "L " + name#定义请求类别,L为进入聊天室
        sockfd.sendto(msg.encode(),ADDR)
        #等待回应
        data,addr = sockfd.recvfrom(4096)
        if data.decode() == 'OK':
            print("已允许进入聊天室")
            break
        else:
            print(data.decode())
    #创建新的进程
    pid = os.fork()
    if pid < 0:
        print("Error")
    elif pid == 0:
        send_msg(sockfd,name)
    else:
        recv_msg(sockfd)


if __name__ == '__main__':
    main()

心得

1.主要采用UDP形式,但是是多线程。
2.客户端创建多线程,因为客户端按照之前的学习是先发送再接受,按照这样的顺序,但是,现在可能进入聊天室,但是giant客户端可能还没有发言就有可能接收到其他人发送的消息,所以,这里的收发消息不是按照一定顺序的,它们的程序之间是相互独立的,那么就要使用多进程关系。

  • 子进程用于发送消息。
  • 父进程用户接受消息。
    (当然,这个创建新进程前提条件是,你已经进入到聊天室)
    3.发送管理员消息的时候也要注意:
  • 因为管理员(服务端)是随时随地可以发送管理员消息的。而客户端在收发消息时是处于阻塞状态,所以管理员在发送消息时必须采用新的进程。
  • 子进程:专门发送管理员消息(将管理员消息发送给父进程,由父进程处理)
  • 父进程:专门收发消息(处理消息)

4.4 multiprocessing 模块创建进程

(python自己封装的一个包)

4.4.1 进程创建方法

1.流程特点

  1. 将需要子进程执行的事件封装诶函数。
  2. 通过模块的process类创建进程对象,关联函数。
  3. 可以通过进程对象设置进程信息及属性。
  4. 通过进程对象调用 start 启动进程。
  5. 通过进程对象调用 join 回收进程。

2.接口基本使用

Process()
    功能:创建进程对象
    参数:
        target:绑定要执行的目标函数(必选)
        args  :元组,用于给target函数位置传参,即函数的参数。(可选)
        kwargs:字典,给target函数键值传参,函数的参数。(可选)
        name  :进程对象名称。(可选)
p.start()
    功能:启动进程

注意
启动进程此时target绑定函数开始执行,该函数作为子进程执行内容,此时进程真正被创建。

p.join([timeout])
    功能:阻塞等待回收函数。(可选参数)
        p.join(3)3秒后结束。否则知道p退出后进程才会回收。
    参数:超时时间。

代码示例:

#multiprocess实例
import multiprocessing as mp
from time import sleep

#作为子进程函数
def fun01():
    print("子程序开始执行")
    sleep(3)
    print("子程序执行完毕")

#创建进程对象
p = mp.Process(target=fun01)
#启动进程
p.start()

sleep(2)
print("父进程执行的功能")

#回收进程
p.join()

代码示例2:

import multiprocessing as mp
import os
from time import sleep


def fun01():
    sleep(3)
    print("吃饭")
    print(os.getppid(),"------",os.getpid())

def fun02():
    sleep(2)
    print("睡觉")
    print(os.getppid(),"------",os.getpid())

def fun03():
    sleep(4)
    print("打豆豆")
    print(os.getppid(),"------",os.getpid())

thing = [fun01,fun02,fun03]
jobs = []
for th in thing:
    p = mp.Process(target=th)
    jobs.append(p)#p由于会被重复赋值,那么可以用列表保存进程对象
    p.start()

for i in jobs:
    i.join()

代码示例3:

#multiprocess示例3
import multiprocessing as mp
import os
from time import sleep
"""
    带参数的进程函数
"""
def worker(m,name):
    """
    :param m: 秒数
    :param name: 姓名
    :return:
    """
    for i in range(3):
        sleep(m)
        print("I am %s"%name)
        print("开始工作了")

p = mp.Process(target=worker,args=(2,"Jack"))#位置传参
 #p = mp.Process(target=worker,kwargs = {'name':"Jack",'s':2})#关键字传参
 #p = mp.Process(target=worker,args=(2,),kwargs = {'name':"Jack"})#位置传参和关键字传参混合使用
p.start()
p.join()
在win下运行出现RuntimeError错误,但是在Linux下正常运行,可能和fork有关,这里需要深究下。

注意

  • 使用multiprocess创建子进程同样是子进程复制父进程空间和代码段(这里并不是只是函数的代码,连全局变量都会复制),父子进程运行互不影响。
  • 子进程只运行target绑定的函数部分,其余内容均是父进程执行的内容。
  • multiprocess中父进程往往只用来创建子进程回收子进程,具体事件由子进程完成。(思想上是这样)
  • multiprocess创建子进程中无法使用标准输入。(如inpu输入等,否则会报异常)

3.进程对象属性
p.name 集成名称
p.id 对应子进程的pid号
p.is_alive() 查看子进程是否在声明周期,在生命周期则返回True,否则返回False。
p.daemon 设置父子进程的退出关系(守护进程)

  • 如果设置为True,则子进程会岁父进程的退出而结束。
  • 要求必须在start()前设置。
  • 如果daemon设置成True,通常就不会使用join()

4.4.2 进程池实现

1.必要性

  • 进程的创建和销毁过程消耗的资源较多。
  • 当任务量众多,每个人物在很短时间内完成时,需要频发的创建和销毁过程,此时对计算机压力较大。
  • 进程池技术很好的解决了以上问题。

2.原理
创建一定数量的进程来处理事件,事件处理完进程不退出而是继续处理其他事件,知道所有事件全都处理完毕统一销毁。增加进程的重复利用,降低资源消耗。
3.进程池实现

  1. 创建进程池对象,放入适当的进程。
 from multiprocess import pool
 Pool(processes)
     功能:创建进程池对象
     参数:指定进程数量,默认根据系统自动判定。(可选)
  1. 将事件加入到进程池队列执行
 pool.apply_async(func,args,kwds)
     功能:使用进程池执行func事件
     参数:func 事件函数
             args 元组,给func按位置传参
             kwds 字典,给func按照键值传参
     返回值:返回函数事件对象
  1. 关闭进程池(进程池不再接受新事件)
 pool.close()
 功能:关闭进程池
  1. 回收进程池中进程(阻塞等待现有的进程执行完毕)
pool.join()
 功能:回收进程池中进程

代码示例:

from multiprocessing import pool
from time import sleep,ctime
"""
进程池代码示例
"""
#进程池事件
def worker(msg):
    sleep(2)
    print(msg)
#创建进程池
pool = pool.Pool()
#进程池中添加事件
for i in range(10):
    msg = "Hello %d"%i
    pool.apply_async(func=worker,args= (msg,))

#关闭进程池
pool.close()
#回收进程池
pool.join()

4.5 进程间通信(IPC)

1.必要性
进程间空间独立,资源不共享,此时需要在进程间数据传输时就需要特定的手段进行数据通信。
2.常用的进程间通信方法
管道
消息队列
共享内存
信号
信号量
套接字

4.5.1 管道通信(pipe)

1.通信原理
在内存中开辟管道空间,生成管道操作对象,多个进程使用同一个管道对象进行读写即可实现管道通信。
2.实现方法

当问单向管道时,fd1只读,fd2只写

from multiprocess import Pipe
fd1,fd2 = Pipe(duplex = True)
    功能:创建管道
    参数:默认表示双向管道
    如果duplex为False,则表示单向管道
    返回值:表示管道两端的读写对象
            如果是双向管道均可读写
            如果是单向管道,##fd1只读,fd2只写##。
fd.recv()
    功能:从管道获取内容
    返回值:获取到的数据
fd.send(data)
    功能:向管道写入数据
    参数:要写入的数据,非字节串,只要是数据即可

代码示例:(以下示例只能在Linux或者Unix下运行)

#管道通信示例
import multiprocessing as mp
from multiprocessing import Pipe
import time
import os
#创建管道
fd1,fd2 = Pipe(duplex=True)

def fun01(name):
    time.sleep(3)
    #向管道写入数据
    fd1.send({name:os.getpid()})

jobs = []
for i in range(5):
    p = mp.Process(target = fun01,args= (i,))
    jobs.append(p)
    p.start()

for i in range(5):
    #读取管道
    data = fd2.recv()
    print(data)

for i in jobs:
    i.join()

4.5.2 消息队列

1.通信原理(自己写的模块名称不要叫做queue)
在内存中建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信。
队列:先进先出
使用场景:多个进程对一个进程发送请求时,或者一个进程被多个进程使用时。
2.实现方法

from multiprocess import Queue
q = Queue(maxsize = 0)
    功能:创建队列对象
    参数:最多存放消息个数,默认根据系统给定。
    返回值:队列消息

q.put(data,[block,timeout])
    功能:向队列存入消息
    参数:data 要存入的内容
        [block,timeout] 
            block:设置是否阻塞,False 为非阻塞
                阻塞的情况:如果队列满了
            timeout:超时检测
    凡是有timeout的,基本上都是则色函数。

q.get([block,timeout])
    功能:从队列取出消息
    参数:block:设置是否阻塞,False 为非阻塞
    timeout:超时检测
    返回值:返回获取到的内容

q.full() #判断队列是否为满
q.empty() #判断队列是否为空
q.qsize() #获取队列中消息个数
q.close() #关闭队列

代码示例:

#消息队列通信
import multiprocessing as mp
from time import sleep
from random import randint
#创建消息队列
q = mp.Queue(3)#消息队列大小
def request():
    for i in range(10):
        x = randint(0,100)
        y = randint(0,100)
        q.put((x,y))
def handle():
    while True:
        sleep(0.5)
        try:
            x,y = q.get(timeout=3)
        except:
            break
        else:
            print("%d + %d = %d"%(x,y,x + y))
#相当于两个子进程间的通信
p1 = mp.Process(target=request)
p2 = mp.Process(target=handle)
p1.start()
p2.start()
p1.join()
p2.join()

4.5.3 共享内存

1.通信原理
字啊内存中开辟一块空间,进程可以写入内容或读取内容完成通信,但每次写入内容会覆盖之前内容。
2.实现方法

from multiprocessing import Value,Array

obj = Value(ctype,data)
    功能:开辟共享内存
    参数:ctype:表示共享内存空间类型 'i' 整型,'f' 浮点型,'c' char型
        data:共享内存空间初始数据
    返回值:共享内存对象
如:obj = Value('i',10)

obj.value 对象属性的修改查看,即对共享内存读写

obj = Array(ctype,data)
    功能:开辟共享内存
    参数:ctype:表示共享内存数据类型
        data:整数则表示开辟空间的大小,其他数据类型
    返回值:共享内存对象

Array 共享内存读写,通过遍历obj可以得到每一个值,直接可以通过索引
    *可以直接使用obj.value直接打印共享内存中的字符串。

代码示例 (这个是存放单一数值,存放字符串的同理)

#共享内存
import multiprocessing as mp
import time
import random
#创建共享内存
money = mp.Value('i',10)
#操作共享内存
def man():
    for i in range(5):
        time.sleep(0.5)
        money.value += random.randint(0,100)
def girl():
    for i in range(30):
        time.sleep(1)
        money.value -= random.randint(5,90)
m = mp.Process(target=man)
g = mp.Process(target=girl)

m.start()
g.start()
m.join()
g.join()
#获取共享内存
print("一个月余额:",money.value)

4.5.4 本地套接字

1.功能:
用于本地两个程序之间进行数据的收发,交互时王权不适用网络,而是用本地的套接字文件进行通信。
2.套接字文件
用于本地套接字之间进行通信时,进行数据传输的介质。
扩展内容:
在Linux文件中,cookie:
b c d - l s p

  • b:块设备
  • c:字符设备
  • d:目录
  • -:普通文件
  • l:连接
  • s:套接字文件
  • p:管道文件

3.创建本地套接字

  • 创建本地套接字
sockfd = socket(AF_UNIX,SOCK_STREAM) #只能在Linux或者Unix中运行
  • 绑定本地套接字文件
sockfd,bind(file)
  • 监听,接受客户端连接,消息收发
listen() ->accpet() ->recv(),send()

代码示例:

#接收端作为服务端
from socket import  *
import os
#确定本地套接字文件
sock_file = './sock'#不需要手动创建,程序运行会自动创建
#判断文件是否存在,存在就删除
if os.path.exists(sock_file):
    os.remove(sock_file)
#创建本地套接字
sockfd = socket(AF_UNIX,SOCK_STREAM)
#绑定本地套接字
sockfd.bind(sock_file)
#监听,连接
sockfd.listen(3)
while True:
    c,addr = sockfd.accept()
    while True:
        data = c.recv(1024)
        if not data:
            break
        print(data.decode())
    c.close()
sockfd.close()

#本地套接字
from socket import *
#确保两遍使用相同的套接字文件
sock_file = './sock'
sockfd = socket(AF_UNIX,SOCK_STREAM)
socket.connect(sock_file)
while True:
    msg = input(">>")
    if not msg:
        break
    sockfd.send(msg.encode())

sockfd.close()

4.5.5 信号量(信号灯集)

1.通信原理 (08.多任务编程P3-2 03:27)
给定一个数量对多个进程可见,多个进程都可以操作该数量增减,并更具数量值决定自己的行为。
2.实现方法 p3-2 03:40

from multiprocessing import Semaphore
sem = Semaphore(num)
    功能:创建信号量对象
    参数:信号量的初始值
    返回值:信号量对象
sem.acquire() #将信号量减1,当信号量为0是阻塞
sem.release() #将信号量加1
sem.get_value() #获取信号量数量

代码示例:

#信号量
from multiprocessing import Semaphore,Process
from time import sleep
import os

#创建信号量,服务程序最多允许3个进程同时执行事件
sem = Semaphore(3)

def handle():
    print("%d  想执行事件"%os.getpid())
    #想执行必须获取信号量
    sem.acquire()
    print("%d 开始执行操作!"%os.getpid())
    sleep(3)
    print("%d 完成操作"%os.getpid())
    sem.release()#增加信号量
jobs = []
#10个进程请求执行事件
for i in range(10):
    p = Process(target=handle)
    jobs.append(p)
    p.start()
for i in jobs:
    i.join()

作业:
1.使用multiprocess创建两个进程,同时复制一个文件中的上下两部分,各自复制到一个新的文件中。
00:46

import multiprocessing as mp
import os

class Copy_file:
    def __init__(self,file_path):

        self.file_path = file_path
        self.size = os.path.getsize(self.file_path)

    def top(self,save_path):
        """
        :param save_path:
        :return:
        """
        f = open(self.file_path,'rb')
        n = self.size // 2
        fw = open(save_path,'wb')
        fw.write(f.read(n))
        f.close()
        fw.close()

    def bot(self,save_path):
        """
        :param save_path:
        :return:
        """
        f = open(self.file_path,'rb')
        fw = open(save_path,'wb')
        f.seek(self.size // 2,0)
        while True:
            data = f.read(1024)
            if not data:
                break
            fw.write(data)
        f.close()
        fw.close()

    def main(self):
        p_top = mp.Process(target=self.top,args = ("top_123.jpg",))
        p_bot = mp.Process(target=self.bot,args = ("bot_123.jpg",))
        p_bot.start()
        p_top.start()
        p_top.join()
        p_bot.join()

if __name__ == '__main__':
    cf = Copy_file("123.jpg")
    cf.main()
注意: 在上述代码中,在top和bot函数中,如果提取f = open(self.file_path,'rb'),使得的打开操作写在进程之外,会有问题,因为写在进程之外,top和bot操作对于file文件的打开会共用同一套文件属性,如偏移量等信息,使得两个进程在分开进行的时候共用一套文件属性复制出来的数据可能有问题。 如果父进程中打开文件,创建进程通信对象,或者创建套接字,而子进程从父进程内存空间获取这些内容,那么父子进程对该对象的操作会有一定的属性关联影响。

4.6 线程编程(Thread)

4.6.1 线程基本概念

1.什么是线程

  • 线程被称为轻量级的进程。
  • 线程也可以使用计算机多核资源,是多任务变成方式。
  • 线程是系统分配内核的最小单元。
  • 线程可以理解成进程的分支任务。

2.线程特征

  • 一个进程中可以包含多个线程。
  • 线程也是一种运行行为,消耗计算机资源。
  • 一个进程中所有线程共享这个进程的资源。
  • 多个线程之间的运行互不影响各自运行。
  • 线程的创建和销毁消耗的资源远小于进程。
  • 各个线程也拥有各自的id等特征。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bfXcxcfW-1591682720464)(./img/线程与进程.png)]

4.6.2 Threading 模块创建线程

  1. 创建线程对象
from threading import Thread
t = Thread()
    功能:创建线程对象
    参数:target 绑定线程函数
         args 元组 给线程函数位置传参
         kwargs 字典 给线程函数键值传参
         name 线程名称
  1. 启动线程
t.start()
  1. 回收线程
t.join([timeout])

代码示例:

"""
线程示例(主线程和分支线程同时执行)
"""
from threading import Thread
from time import sleep
#线程函数
def music():
    for i in range(4):
        sleep(3)
        print("播放歌曲")

#创建线程对象
t = Thread(target= music)
t.start()
#主线程任务
for i in range(3):
    sleep(2)
    print("播放电影")

t.join()

4.6.3 线程对象属性

  1. t.name 线程名称
  2. t.setName() 设置线程名称
  3. t.getName() 获取线程名称
  4. t.is_alive() 查看线程是否在声明周期
  5. t.daemon 设置主线程和分支线程之间的退出关系
  6. t.setDaemon() 设置daemon属性值
  7. t.isDaemon() 查看daemon属性值

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

其他:python线程池第三方模块:threadpool

4.6.4 自定义线程类

1.创建步骤:

  • 继承Thread类
  • 重写__init__方法,添加自己的属性,使用super加载父类属性。
  • 重写run方法。

2.使用方法

  • 实例化对象
  • 调用start自动执行run方法。
  • 调用join回收进程。

注意
关于线程函数的高内聚,有可能需要一个线程配合执行多个函数,而t - Thread(target = ?)中只有一个方法参数,如果实现功能实现特别复杂,那么就需要自定义线程类。
代码示例:

from threading import Thread
class ThreadClass(Thread):
    def __init__(self,attr):
        self.attr = attr
        super().__init__()#采用父类的init方法
    #多个方法配合实现具体功能
    def fun01(self):
        print("步骤一")

    def fun02(self):
        print("步骤二")

    #重写run方法
    def run(self):
        self.fun01()
        self.fun02()

t = ThreadClass("xxxxx")
t.start()
t.join()

代码示例2:

from threading import Thread
from time import sleep,ctime

class MyThread(Thread):
    def __init__(self,target = None,args= (),
                 kwargs= {},name = "Tedu"):
        super().__init__()
        self.target = target
        self.args = args
        self.kwargs = kwargs
        self.name = name
    def run(self):
        self.target(*self.args,**self.kwargs)
###############################
"""
    通过完成上方MyThread类,让整个程序可以正常运行
    当调用start时,player作为一个线程功能函数运行
    注意,函数的名称和参数并不确定,player只是测试函数
"""
def player(sec,song):
    for i in range(2):
        print("Playing %s:%s"%(song,ctime()))
        sleep(sec)
t = MyThread(target = player,args=(3,),
             kwargs={'song':'凉凉'},name = 'happy')
t.start()
t.join()

4.7 同步互斥

4.7.1 线程间通信方法

1.通信方法
线程间使用全局变量进行通信。
2.共享资源争夺

  • 共享资源:多个进程和线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。
  • 影响:对共享资源的无序操作,可能会带来数据的混乱,或者操作失误。此时需要同步互斥机制协调操作顺序。

3.同步互斥机制
同步:同步是一种协调关系,为完成操作,多进程或线程间形成一种协调,按照必要的步骤有序执行操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oyuPxFRA-1591682720468)(./img/同步原理.png)]
互斥:互斥是一种制约机制,当一个进程或线程占有资源时会进行枷锁处理,此时其它进程线程无法操作此资源,直到解锁后才能操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H1wolPA2-1591682720469)(./img/互斥原理.png)]

4.7.2 线程同步互斥方法

  1. 线程event
from threading import Event
e = Event() #创建线程事件对象
e.wait([timeout]) #阻塞等待e被set,timeout超时时间
e.set() #设置e,使得wait结束阻塞
e.clear() #使e回到未被设置状态
e.is_set() #查看当前e是否被设置

代码示例:

#event事件代码示例
from threading import Event
from threading import Thread
from time import sleep

s = None #全局变量,模拟对暗号
e = Event()
def 杨子荣():
    print("杨子荣前来拜上头")
    global s
    s = "天王盖地虎"
    e.set() #共享资源操作完毕

t = Thread(target=杨子荣)
t.start()
print("说对口令就是自己人")
#每次验证之前进行阻塞等待
e.wait()
if s == "天王盖地虎":
    print("宝塔镇河妖")
    print("口令正确")
else:
    print("枪毙")
t.join()
  1. 线程锁 LOCK
from threading import lock
lock = Lock() #创建锁对象
lock.acquire() #上锁 如果lock已经上锁,再调用会阻塞
lock.release() #解锁

with lock: #上锁
  ···
  ···

    wait代码块结束后自动解锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lr6HgnVj-1591682720470)(./img/同步互斥原理LOCK.png)]

代码示例:

#同步互斥原理LOCK代码示例

from threading import Lock,Thread

a = b = 0 #全部变量,共享资源
lock = Lock()
def value():
    while True:
        lock.acquire()
        if a != b:
            print("a = %d,b = %d"%(a,b))
        lock.release()

t = Thread(target=value)
t.start()
while True:
    with lock:
        a += 1
        b += 1
t.join()

4.7.3 死锁及其处理

  1. 定义
    死锁实质两个或两个以上的线程在执行过程中,由于竞争资源或者彼此通信二造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t4WmHBjE-1591682720472)(./img/死锁现象.png)]

  2. 死锁发生的必要条件:

  • 互斥条件:指的是线程对所分配的资源进行排它性使用,即在一段时间内某资源只有一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,知道占有资源的进程用完并释放。
  • 请求和保持条件:指的是线程已经保持至少一个资源,担忧提出了新的资源请求,而该资源已经被其它集成占有,此时请求线程阻塞,但又对自己已经获得的其它资源保持不放。
  • 不剥夺条件:指的是线程已经获得的资源,在使用玩之前不能被剥夺,只能在使用完后由自己释放,通常CPU内存资源是可以被系统强行调配剥夺的。
  • 环路等待条件:指的是在发生死锁时,必然会产生一个线程——资源的环形链,即进程集合{t0,t1,t2…tn}中t0正在等待一个t1占用的资源,t1正在等待t2占用的资源…,tn正在等待t0占用的资源。

死锁产生的原因:
- 当线程拥有其他线程需要的资源。
- 当前线程等待其它线程已拥有的资源。
- 都不放弃自己拥有的资源。

阻塞代码示例:

import threading
import  time
class Account:
    """
    交易类
    """
    def __init__(self,_id,balance,lock):
        """
        :param _id: 用户
        :param balance: 存款
        :param lock: 锁
        """
        self.id = _id
        self.balance = balance
        self.lock = lock

    def withdraw(self,amount):
        """
        :param amount:
        :return:
        """
        self.balance -= amount

    def deposit(self,amount):
        """
        :param amount:
        :return:
        """
        self.balance += amount

    #查看账户金额
    def get_balance(self):
        return self.balance

#转账函数
def transfer(from_,to,amount):
    if from_.lock.acquire():# 锁着自己的账户,上锁成返回true
        from_.withdraw(amount) # 自己账户金额减少
        if to.lock.acquire():
            to.deposit(amount) # 对方账户金额增加
            to.lock.release()# 对方账户解锁
        from_.lock.release()# 自身账户解锁
    print("转账完成")

#创建两个账户
Abby = Account('Abby',5000,threading.Lock())
Levi = Account('Levi',3000,threading.Lock())

t1 = threading.Thread(target=transfer,args=(Abby,Levi,1500))
t2 = threading.Thread(target=transfer,args=(Levi,Abby,1500))
t1.start()
t2.start()
t1.join()
t1.join()
print("Abby:",Abby.get_balance())
print("Levi:",Levi.get_balance())
  1. 如何避免死锁
  • 使用定时锁
  • 使用重入锁RLock(),用法同lock。rlock内部维护这一个lock和counter变量,counter记录了acquire的次数,从而使得资源能够被多次require。直到一个线程的所有acquire都被release,其他线程才能获取资源。

未完待续…

你可能感兴趣的:(python,从入门到精通)