python网络通信,多线程,迭代器

网络通信,多线程,迭代器

4.python网络基础

网络通信概述

  • 什么是网络:

    一些相互连接的,以共享资源目的计算机的集合.

  • 为什么学习网络编程:

    能编写基于网络通讯的软件,与其他计算机的软件进行数据通讯

[重点]ip地址(重点)

  • ip地址:用来在网络中标记一台计算机,是网络设备给每个计算机分配的唯一标识

  • IP 地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址), 是IP Address的缩写。IP 地址是IP协议提供的一种统一的地址格式.

  • ip地址的作用:用来在网络中标记一台电脑,是网络设备为网络中的每台计算机分配的一个唯一标 识。比如192.168.1.1;在本地局域网上是唯一的。

  • ip地址格式:xxx.xxx.xxx.xxx 点分十进制(IPv4)**

  • 常用:A,B类(欧洲等发达国家使用),C类(国内)

  • 公网ip

    1.0.0.1-126.255.255.254

    128.1.0.1-191.255.255.254

    192.0.1.1-223.255.255.254

  • 局域网ip、私有ip:

    10.xxx.xxx.xxx 内网

    172.16.xxx.xxx - 172.31.xxx.xxx 内网

    192.168.0.0-192.168.255.255 常用

  • 特殊ip

    127.0.0.1

    localhost

  • IPv6 : 冒号分十六进制,号称地球上每一粒沙子分配一个IP地址

  • IPv6 支持测试地址:http://test-ipv6.com/

Linux命令(ping、ifconfig等)

  • 虚拟机网络模式:
    • NAT(网络地址转换模式):则虚拟机会使用主机VMnet8这块虚拟网卡与我们的真实机进行通信
    • Bridged(桥接模式):虚拟机如同一台真实存在的计算机,在内网中获取和真实主机同网段IP地址
      • 优点:不需要任何设置,虚拟机就可以直接和我们真实主机通信。
      • 缺点:虚拟机需要占用真实机网段的一个IP。
  • 查看ip地址
    • Linux/Mac: ifconfig
    • Windows: ipconfig
  • 测试网络是否联通 ping
    • ping 192.168.16.33
    • ping www.baidu.com

osk:调出键盘

[重点]端口(重点)

  • "端口"是英文port的意译,可以认为是设备与外界通讯交流的出口
  • 端口分类:0~65535
    • 知名端口 0~1023
      • 21 FTP文件服务
      • 22 SSH远程安全连接
      • 80 HTTP超文本传输端口
    • 注册端口 1024-49151
      • 三方编写的应用占用的端口:飞秋2425
    • 动态端口 49152~65535
      • 如果一个软件没有指定端口,系统会临时自动分配一个,数据交换完毕,收回此端口

[重点]网络传输方式

  • 两种传输方式

    • 面向无连接(UDP协议)

    数据传输前,不需要建立连接,可以直接收发数据

    UDP (User Datagram Protocol )不提供复杂的控制机制, 如果传输过程中出现丢包, UDP 也不 负责重发. 甚至当出现包到达顺序乱掉时候也没有纠正的功能. 由于 UDP 面向无连接, 它可以随时 发送数据. 再加上 UDP 本身的处理既简单又高效, 因此常用于以下几个方面:
    1.包总量较少的通信(DNS). 视频、音频等多媒体通信(即时通信).

    2.限定于 LAN 等特定网络中的应用通信.

    1. 广播通信(广播、多播)

    UDP协议

    • 面向有连接(TCP协议)

    需要先建立连接,才能进行数据收发

    TCP 协议, 传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向 连接的、可靠的、基于字节流的传输层通信协议.
    两者区别
    TCP提供的是面向连接的、可靠的数据流传输;
    UDP提供的是非面向连接的、不可靠的数据流传输。 TCP提供可靠的服务,通过TCP连接传送的数据,无差错、不丢失,不重复,按序到达
    UDP尽最大努力交付,即不保证可靠交付。
    34
    TCP面向字节流;
    UDP面向报文。
    TCP连接只能是点到点的; UDP支持一对一、一对多、多对一和多对多的交互通信。

    TCP协议

  • 查看端口号及进程id

    • Windows: netstat -ano | findstr 2425
    • Linux/Mac: netstat -an | grep 21
  • Linux查看进程名称&PID&端口

    sudo lsof -i :21

  • 查看服务器socket

    sudo netstat -ntl

[重点]socket简介

  • socket 套接字,网络通讯的基本单元,使用其提供的函数可以让不同的应用程序之间进行数据的传输。
  • GBK Windows中文编码集
  • UTF-8 万国码
  • Python2默认使用系统编码,Python3使用UTF-8编码集

[重点]udp网络程序-发送数据

[重点]udp网络程序-发送并接收数据

  • 导入依赖模块

    import socket
    
  • 创建套接字socket对象

    # 参数一:AddressFamily 地址类型
    #   socket.AF_INET   IPv4(默认)
    #   socket.AF_INET6  IPv6
    # 参数二:传输协议类型
    #   socket.SOCK_DGRAM UDP协议
    #   socket.SOCK_STREAM TCP协议(默认)
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
  • 发送数据

    """
    参数一:data
        要发送的数据的二进制形式,bytes字节数组
    参数二:address
        接收者的(IP地址,端口号),元组
        元素1:字符串IP地址
        元素2:整数类型端口号
    """
    udp_client_socket.sendto("你好!".encode(), ("192.168.16.64", 8888))
    
  • 接收数据

    # bufsize recvfrom(1024) 执行一次接收的最大字节数
    # 没有人给当前socket发数据,此代码会一直阻塞
    # 一旦收到了数据,recvfrom会自己解除阻塞
    # 接收到的数据(b'\xd4\xbc123', ('192.168.16.58', 8888)) 元组
        # 元素1:接收到的消息(字节数组)
        # 元素2:发送者的IP和端口号的元组
    recv_data = udp_client_socket.recvfrom(1024)
    # print(recv_data)
    try:
        print(recv_data[0].decode()) # 消息
    except Exception as e:
        # print("解码错误,尝试使用GBK解码")
        print(recv_data[0].decode("GBK")) # 消息
    
    print(recv_data[1])          # 地址
    
  • 关闭套接字

    udp_client_socket.close()
    

[重点]python3编码转换

# encoding字符集
	utf-8 万国码, gbk汉字
# errors错误处理方式,
	strict严格模式(默认),ignore忽略错误

[重、难点]udp绑定端口-接收端

UDP广播

  • 添加广播权限

    # 3)开启广播权限
    # 参数1:SOL_SOCKET 当前socket对象
    # 参数2:SO_BROADCAST 要修改的属性名
    # 参数3:属性值
    udp_socket.setsockopt(SOL_SOCKET, SO_BROADCAST, True)
    
  • 发送地址

    255.255.255.255

    xxx.xxx.xxx.255

[难点]案例:udp聊天器(一)

网络通信-UDP/TCP

*tcp简介(Transmission Control Protocol,缩写为 TCP)

  • TCP协议:面向连接的,可靠的,基于字节流的传输层通信协议
  • 这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP 协议。
  • TCP协议特点:
    • 面向连接
    • 可靠传输
      1. 应答机制
      2. 超时重传
      3. 错误校验
      4. 流量控制&阻塞管理
  • TCP通讯特点:
    • 严格区分客户端和服务器

*tcp网络程序-客户端 &

  • 导入模块

  • 创建套接字对象

    socket.SOCK_STREAM

  • 连接TCP服务器

    tcp_client_socket.connect((“192.168.16.58”, 8080))

  • 收发数据

    # 发送数据到服务器
    tcp_client_socket.send("哈哈哈哈,你打不我吧!".encode())
    # 等待接收数据
    recv_data = tcp_client_socket.recv(1024)
    print(recv_data.decode("GBK"))
    
  • 关闭连接

*tcp网络程序-服务器 &

  • 导入模块

  • 创建socket对象

  • 绑定IP和端口号

    tcp_server_socket.bind(("", 7788))

  • 开启监听,设置为被动模式listen

    tcp_server_socket.listen(128)

  • 等待客户端的接入accept

    # 代码会阻塞,直到有客户端接入,释放阻塞
    # 收到元组:
    # 元素1:服务器为客户端创建的socket对象
    # 元素2:客户端的address(IP,port)
    tcp_client_socket, ip_port = tcp_server_socket.accept()
    
  • 使用新的客户端socket对象收发数据

    # 使用新的客户端的socket对象,进行数据的【接收&发送】操作
    recv_bytes = tcp_client_socket.recv(1024)
    print("收到{}消息:{}".format(ip_port,recv_bytes.decode("GBK")))
    
    # 发送数据
    tcp_client_socket.send("下课!".encode())
    
  • 关闭连接

    # 7)关闭与客户端的连接
    tcp_client_socket.close()
    
    # 8)关闭服务器套接字
    tcp_server_socket.close()
    

*tcp网络程序-服务器增强

  • 能够接收一个客户端发来的多条信息

    # 6)recv/send接收发送数据
    while True:
        # 使用新的客户端的socket对象,进行数据的【接收&发送】操作
        recv_bytes = tcp_client_socket.recv(1024) # 阻塞
        if recv_bytes:
            print("收到{}消息:{}".format(ip_port,recv_bytes.decode("GBK")))
            # 发送数据
            tcp_client_socket.send("收到!".encode())
        else:
            print("客户端已断开连接:", ip_port)
            break
    
  • 能够接受多个客户端连接

    # 5)accept等待客户端的链接
    while True:
        # 代码会阻塞,直到有客户端接入,释放阻塞
        # 收到元组:
        # 元素1:服务器为客户端创建的socket对象
        # 元素2:客户端的address(IP,port)
        tcp_client_socket, ip_port = tcp_server_socket.accept()
        print("有新的客户端接入:",ip_port)
    
        # 6)recv/send接收发送数据
        while True:
            # 使用新的客户端的tcp_client_socket对象,进行数据的【接收&发送】操作
            ...
    
  • 只能同时和一个客户端进行数据的收发,下一个客户端必须等上一个断开了,才能接入

*案例:文件下载器(一)&

*案例:文件下载器(二)&

tcp的3次握手

  • 客户端和服务器建立连接时,需要总共发送3个包以确认连接的建立
  • 为什么是3次,不是2,4,5
  • 1.为什么需要三次握手,两次不可以吗?或者四次、五次可以吗?
  • 答:我们来分析一种特殊情况,假设客户端请求建立连接,发给服务器SYN包等待服务器确 认,服务器收到确认后,如果是两次握手,假设服务器给客户端在第二次握手时发送数据, 数据从服务器发出,服务器认为连接已经建立,但在发送数据的过程中数据丢失,客户端认 为连接没有建立,会进行重传。假设每次发送的数据一直在丢失,客户端一直SYN,服务器 就会产生多个无效连接,占用资源,这个时候服务器可能会挂掉。这个现象就是我们听过 的**“SYN的洪水攻击”。**
    总结:第三次握手是为了防止:如果客户端迟迟没有收到服务器返回确认报文,这时会 放弃连接,重新启动一条连接请求,但问题是:服务器不知道客户端没有收到,所以他会收 到两个连接,浪费连接开销。如果每次都是这样,就会浪费多个连接开销。

tcp的4次挥手

  • 客户端和服务器断开连接时,需要总共发送4个包以确认连接的断开
  • 服务器收到FIN请求时,不能及时的关闭服务器,只能先发个ACK确认包给客户端,等数据处理完毕后,再发送FIN包确认关闭。
  • 等待两个MSL是为了确保客户端收到了最后一个ACK回复。谁主动断开,谁需要等待。

IP地址和域名

域名,简称DN(全称:Domain Name),域名可以理解为是一个网址,就是一个特殊的名 字。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3QQiS65S-1581259279621)(img/45.png)]

DNSS(Domain Name System域名解析系统)及浏览器请求服务器的过程

HTTP(HyperText Transfer Protocol)协议概述

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络 协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收 HTML页面的方法。
HTTP是一个客户端和服务器端请求和应答的标准(TCP)。客户端是终端用户,服务器端是 网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口 (默认端口为80)的HTTP请求。
超文本传输协议是一种应用层协议。

*HTTP协议格式查看

  • Chrome & 火狐
    • F12
    • 右键-检查

*HTTP请求报文格式

  • 请求报文
    • 请求行
      • 请求方式(GET/POST) 资源路径 协议版本(HTTP/1.1)
    • 请求头
      • 协议项 (协议名: 协议值)
    • 空行
    • 请求体

*HTTP响应报文格式

  • 响应报文

    • 响应行、状态行

      • 协议版本(HTTP/1.1) 状态码 状态描述
    • 响应头

      • 协议项 (协议名: 协议值)
    • 空行

    • 响应体

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

长连接和短连接

  • 长连接

    • 一次连接,多次数据收发,比较消耗资源,如果连接过多,可能导致其他用户无法连接,一旦连上,访问速度有保障。
  • 短连接

    • 每次收发数据都需要建立连接,请求数量大时,访问速度慢。

多任务—线程

多任务的概念

  • 多任务:同一时间,操作系统执行多个任务
  • 单任务和多任务区别:
    • 单任务:多个任务只能按顺序执行
    • 多任务:同一时间段,多个任务交替执行(同时执行)
    • Python默认是单任务

[重点]线程的基本使用

线程,可简单理解为是程序执行的一条分支,也是程序执行流的最小单元。线程是被系统独 立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源, 但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

  • 主线程:程序启动时,操作系统会默认创建并启动一个线程,就是主线程
  • 主线程作用:1)用来创建子线程 2)等待所有子线程完毕后再关闭
  • 创建子线程步骤:
    • 导入依赖模块threading
    • 创建threading.Thread对象,将要执行的函数传给target参数
    • 执行线程对象的start函数,开启子线程
import time
import threading      #导入依赖模块threading


def sing():
    for i in range(1, 5):
        print('正在唱歌....', i)
        time.sleep(0.5)


def dance():
    for i in range(1, 5):
        print('正在跳舞....', i)
        time.sleep(0.8)


if __name__ == '__main__':
	# 创建threading.Thread对象,将要执行的函数传给target参数
    t = threading.Thread(target=sing)
    d = threading.Thread(target=dance)
    # 执行线程对象的start函数,开启子线程
    t.start()
    d.start()

[重点]线程-线程名称、总数量

  • 获取当前活动线程的数量:threading.active_count()

    当前活跃的线程对象列表:threading.enumerate()

    得到列表长度len(threading.enumerate())

  • 获取当前线程名称

    threading.current_thread()

[重点]线程-参数及顺序

  • 线程参数传递3种方式:

    • 元组

      # 1)传递args元组 threading.Thread(target=子线程函数名, args=元组) 
      # 参数必须和函数形参顺序一致
      sing_thread = threading.Thread(target=sing, args=(10, 100, 1000))
      
    • 字典

      # 2)传递kwargs字典 threading.Thread(target=子线程函数名, kwargs=字典)
      # 字典里的参数顺序可以和形参顺序不同
      sing_thread = threading.Thread(target=sing, kwargs={"a":10, "c":1000, "b":100})
      
    • 元组合字典的混合

      # 3)元组和字典的混合
      # 如果形参里的参数有默认值,创建Thread时可以不传,否则必传
      sing_thread = threading.Thread(target=sing, args=(10,) , kwargs={"b":100})
      
  • 线程执行顺序:

    • 线程的执行是无序的,线程是由CPU执行的,CPU会根据系统运行状态通过线程调度算法来决定运行哪个线程

[重点]线程-守护线程

  • 守护线程

    • 子线程和主线程的一种约定
    • 当主线程退出时,守护线程也随之退出
    # 必须在start之前执行
    thread.setDaemon(True)
    thread.start()
    

并行和并发

  • 并发:任务数量大于CPU核心数,由于CPU调度速度极快,看上去就像在共同执行
  • 并行:任务数量小于等于CPU核心数,任务才可能真正同时运行

[重、难点]自定义线程类

  • 自定义线程类的流程

    • 创建一个类继承threading.Thread

      class MyThread(threading.Thread)
      	pass
      
    • 重写父类的run函数

      def run(self):
          pass
      
    • 创建对象并调用start函数,启动自定义线程

      thread = MyThread(10)
      # 内部会自动触发run函数执行
      thread.start()
      
  • 更多

    • 调用start函数,其内部会转调run函数
    • 父类run函数内部执行target函数,重写后会执行新的内容
    • 重写构造函数记得调用父类的构造函数super().__init__()
    • 子类继承父类的 name 属性(保存的是线程的名称)

[重点]多线程-共享全局变量

  • 多个线程之间是可以共享变量的

[难点]多线程-共享全局变量-问题

  • 线程资源竞争问题,多个线程同时修改同一个资源,可能导致资源混乱
  • 初级解决方法:使多个线程按顺序执行
    • 调用 线程名.join 函数让其他线程等待其执行完毕再执行
    • 此解决方案并不好,使多线程变成了单线程的执行效果

同步&异步

  • 同步:多任务执行时,任务按照一定的顺序执行,某个任务执行时,其他任务在等待
  • 异步:多任务执行时,任务不按照顺序执行,多个任务同时执行
  • 解决资源竞争问题:
    • 线程锁(mutex)机制:当线程获取到资源时,立即给该资源加上锁,资源操作完毕后,再释放锁,其他线程才可以通过锁机制获取该资源。

[重点]互斥锁

  • 使用步骤

    • 创建互斥锁

      my_lock = threading.Lock()
      
    • 资源修改前获取锁

      my_lock.acquire()
      
    • 资源修改后释放锁

      my_lock.release()
      
  • 注意事项

    • 互斥:竞争资源的多个线程要使用同一把锁。
    • 加锁和解锁是成对出现的
    • 在衡量代码执行效率的情况下,尽可能少的进行加锁、解锁操作。

死锁

  • 什么是死锁:多个任务在同一时间,都在等待对方释放锁,这个状态就是死锁状态

  • 如何避免:在获取锁后,资源操作完毕,及时释放锁

  • 代码演示:

    #  index      0  1  2  3  4
    value_list = [1, 3, 5, 7, 9]
    
    # 获取锁, 代码可能阻塞
    my_lock.acquire()
    
    # 对资源的操作
    if index >= len(value_list):
        print("角标越界: " , index)
    
        # 释放
        my_lock.release()
        return
    
    print("value: ", value_list[index])
    
    # 释放
    my_lock.release()
    

    #  index      0  1  2  3  4
    value_list = [1, 3, 5, 7, 9]
    
    # 获取锁, 代码可能阻塞
    my_lock.acquire()
    
    try:
        # 对资源的操作
        print("value: ", value_list[index])
    except:
        print("角标越界: " , index)
    finally:
        # 确保锁可以释放
        my_lock.release()
    
    

案例:多任务版udp聊天器

  • 将UDP聊天器改为多任务版

    • 重新修改用户菜单,删除接收消息栏,改为默认在后台接收消息

    • 开启子线程循环接收消息

    • 主线程退出时,子线程也随之退出(设置守护线程)

      def recv_msg(udp_socket):
          """
          需要在子线程循环接收消息
          :param udp_socket:
          :return:
          """
          while True:
              # 1) 使用socket的recvfrom函数接收数据, 使子线程进入阻塞状态
              bytes_data, ip_port = udp_socket.recvfrom(1024)
              # 2) 解码并打印收到的数据
              try:
                  # 尝试使用Linux默认的utf-8编码集进行解码
                  print("收到{}发来的消息:{}".format(ip_port, bytes_data.decode()))
              except:
                  # 如果解码失败, 则使用GBK进行解码
                  print("收到{}发来的消息:{}".format(ip_port, bytes_data.decode("GBK")))
                  
      # 开启子线程,在子线程接收消息
      thread_recv = threading.Thread(target=recv_msg, args=(udp_socket,))
      
      # 使子线程设置为守护线程,当主线程退出时,该线程也退出
      thread_recv.setDaemon(True)
      
      thread_recv.start()
      
      

[重点]TCP服务端框架

  • TCP服务器代码

    • 导入模块
    • 创建socket对象
    • 设置地址重用
    • 绑定ip端口
    • 开启监听,套接字对象由主动变被动
    • 接收客户端连接
    • 接收客户端发来的消息
    • 解码打印客户端信息
    • 关闭和客户端的连接
  • 开启子线程同时接收多个客户端的接入,同时接收每个客户端的多条消息

    • 每个新来的客户端分配一个子线程

      while True:
          # 6. 接收客户端连接 (线程1:主线程 - 通过循环支持多个客户端的接入)
          tcp_client_socket, client_ip_port = tcp_server.accept()  # 会阻塞等待客户端接入
      
          # 给新接入的客户端单独开启一个新的子线程, 循环接收这个客户端的多条消息
          # recv_msg(tcp_client_socket, client_ip_port)
          new_client_thread = threading.Thread(target=recv_msg,
                                               args=(tcp_client_socket, client_ip_port))
      
          # 设置子线程为守护线程,随着主线程退出而自动关闭
          new_client_thread.setDaemon(True)
      
          # 开启新线程,此代码不会阻塞主线程
          # 线程开启后,主线程又立即进入accept状态,等待客户端接入
          new_client_thread.start()
      
      
    • 在每个子线程中,while True循环接收客户端消息, recv函数阻塞的是子线程,不影响主线程接收新客户端接入

def recv_msg(tcp_client_socket, client_ip_port):
    print("有新的客户端接入:{},线程名:{}".format(\
                                      client_ip_port, threading.current_thread()))
    while True:
        # 线程2..:子线程 循环接收某一个客户端的多条消息
        # 7. 接收客户端发来的消息 (阻塞)
        recv_data = tcp_client_socket.recv(4096)
        if recv_data:
            # 8. 解码打印客户端信息
            print("收到{}发来的消息:{}".format(\
                                        client_ip_port, recv_data.decode("GBK")))
        else:
            print("客户端{}已断开".format(client_ip_port))
            # 9. 关闭和客户端的连接
            tcp_client_socket.close()
            break

线程补充:

GIL锁

无论有多少个cpu,python在执行时会在同一时刻只允许一个线程运行。

python下想要充分利用多核CPU,就用**多进程。**因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,有多核CPU情况下,多进程的执行效率优于多线程。

import threading, multiprocessing

查看cpu个数: count = multiprocessing.cpu_count()

递归锁

当某个线程申请到一个锁,其余线程不能再申请。于是有了递归锁(其实就是内部维护了一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。)

多任务-进程

进程以及状态

  • 进程:是操作系统分配资源的最小单位,也是线程的容器
  • 进程状态:
    • 新建
    • 就绪
    • 运行
    • 等待(阻塞)
    • 死亡

[重点]进程-基本使用

  • 导入模块

    import multiprocessing
    
  • 创建Process对象

    process = multiprocessing.Process(target=work)
    
  • 启动继承

    process.start()
    

进程相关的api

is_alive():判断进程子进程是否还在活着

join([timeout]):是否等待子进程执行结束,或等待多少秒

terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:

name:当前进程的别名,默认为Process-N,N为从1开始递增的整数

pid:当前进程的pid(进程号)

[重点]进程-名称、PID

  • 获取进程名称:

    multiprocessing.current_process().name
    
  • 获取进程pid

    multiprocessing.current_process().pid
    os.getpid()
    
  • 父进程pid (ppid)

    os.getppid()
    
  • 进程的销毁

    Windows:可以在任务管理器中根据PID杀死进程,也可以使用kill pid关闭指定进程

    Linux&OSX:使用kill -9 pid强行杀死进程

  • 主进程和子进程特点:

    • 主进程挂掉,子进程也随之退出
    • 子进程被杀死,主进程仍可运行

[重点]进程-参数传递、全局变量问题

  • 进程参数的传递

    # 元组args
    process = multiprocessing.Process(target=work, args=(10, 100, 1000))
    # 字典kwargs
    process = multiprocessing.Process(target=work, kwargs={"c":1000, "b":100, "a":10})
    # 混合
    process = multiprocessing.Process(target=work, args=(10, 100),kwargs={"c":1000})
    
  • 进程之间不能共享全局变量的。IPC

[重点]进程-守护主进程

  • 守护进程:主进程和子进程一种约定,主进程退出时,子进程也随之退出

  • 实现方式:

    • 设置守护进程

      # 方式1:设置子进程为守护进程
      process.daemon = True
      
    • 提前终止子进程

      # 方式2:提前终结子进程
      process.terminate()
      

进程、线程对比

  • 对比进程和线程
    • 进程是操作系统分配资源的基本单位;线程CPU调度和分派的基本单位
    • 进程拥有独立的内存空间;线程需要必不可少的运行资源(进程提供)
    • 进程稳定性高,安全性好;线程效率高,速度快
    • 进程创建、销毁、切换慢;线程创建、销毁、切换快;
    • 进程适合CPU密集型任务,线程适合I/O密集型任务
    • 线程不能独立运行,必须运行在进程中
  • 不要陷入“非此即彼”的误区。进程+线程工作。

[重点]消息队列-基本操作

  • 队列的创建: 进程间通信 IPC实现的一种

    初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头);

    # 1) 导入模块
    import multiprocessing
    # 2)创建队列对象(大小)
    queue = multiprocessing.Queue(3)
    
  • 阻塞式的数据添加&获取

    • 添加queue.put("abc")

    • 获取value queue.get()

      设置timeout,可以在如果阻塞超过timeout时间时,抛出异常

      value = queue.get(timeout=3)
      print(value)
      
  • 非阻塞式数据的添加&获取

    • xxx_nowait():
      • 添加queue.put_nowait(123) 如果队列已满,抛出异常queue.Full
      • 获取value = queue.get_nowait() 如果队列为空,抛出异常_queue.Empty

[消息队列-常见判断

# 判断队列是否为空,Python低于3.7版本上,可能会出现empty在queue刚添加完数据后,empty为True
print("empty: ", queue.empty())

# 判断队列是否已满
print("full: ",queue.full())

print(queue.get())
print(queue.get())
# 获取队列元素个数
print("qsize: ", queue.qsize())

[重点]Queue实现进程间通信

  • 消息队列:实现进程间的数据交互

  • 使用步骤:

    • 创建消息队列并将queue传给每个进程

      queue = multiprocessing.Queue(5)
      
    • 创建并启动一个或多个进程

      p1 = multiprocessing.Process(target=work1, args=(queue,))
      p2 = multiprocessing.Process(target=work2, args=(queue,))
      
      p1.start()
      p2.start()
      
    • 添加数据

      def work1(queue):
          """ 进程work1不断地放入数据 """
          for i in range(10):
              print("+++进程1向queue添加数据:", i)
              queue.put(i)
              queue.put(i)
              queue.put(i)
              time.sleep(0.5)
      
    • 获取数据

      try:
          while True:
              value = queue.get(timeout = 3)
              print("---进程2从queue获取数据:", value)
      except:
          print("3秒之内没有获取到数据,主动关闭掉自己")
      
    • 两种方式处理消息队列

      • 使其中一个进程使用.join()完成数据的写入后,另一个进程读取
      • 直接使用put和get,在get时,使用timeout设置N秒之内没有获取到数据(阻塞超过N秒),主动关闭掉自己

[重点]进程池Pool

初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么 就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求 就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务。

import multiprocessing import time
def copy_work():    
    print("拷贝中....",multiprocessing.current_process().pid)   
    time.sleep(0.3)
if __name__ == '__main__':
    # 创建进程池    # Pool(3) 表示创建容量为3个进程的进程池    
    pool = multiprocessing.Pool(3)
    for i in range(10):        
   # 利用进程池同步拷贝文件,进程池中的进程会必须等上一个进程退出才能执行下一个进程 					    pool.apply_async(copy_work)
    pool.close()    # 注意:如果使用异步方式执行copy_work任务,主进程不再等待子进程执行完毕再退出!       pool.join()


close():关闭Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():等待进程池中的所有进程执行完毕再退出, 必须在close或terminate之后使用;

进程池中的Queue

如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue()
用法: queue = multiprocessing.Manager().Queue(3)

案例:文件夹copy器(多进程版)

文件夹copy器

源文件夹       ->         目标文件夹
./test       ->       C:\Users\Poplar\Desktop\test

def 文件拷贝: 源文件夹, 目标文件夹, 文件名
    1. 声明并拼接源文件&目标文件的路径
    2. 打开源文件&创建目标文件
    3. 读取源文件&写入目标文件(循环)
    
1. 定义变量保存源文件夹&目标文件夹
2. 创建目标文件夹
3. 获取并遍历源文件夹的所有文件
4. 定义函数执行文件的拷贝(遍历)

案例:网游服务器(多进程版)

迭代

我们已经知道可以对list、tuple、str等类型的数据使用for…in…的循环语法从其中依次拿到数据进 行使用,我们把这样的过程称为遍历,也叫迭代

我们把可以通过for…in…这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象 (Iterable)。

判断一个对象是否可以迭代

from collections.abc import Iterable

# 判断列表是否可以迭代
print(isinstance([1, 2, 3], Iterable))

可迭代对象的本质

可迭代对象的本质是:一个对象所属的类中含有 iter() 方法,该对象就是一个可迭代对象。

from collections.abc import Iterable


class MyClass(object):

    def __iter__(self):
        pass


my_class = MyClass()
print(isinstance(my_class, Iterable))

迭代器

​ 我们分析对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for…in…中每循环一次) 都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程 中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们 把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator)

[重点]可迭代对象及检测方法

  • 可迭代对象
    • 拥有一个可以记住遍历的位置的迭代器,迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
    • 可遍历的对象就是可迭代的对象
    • list, tuple, set, dict, str, range … 是可迭代对象
    • 数组、自定义对象… 不可迭代对象
    • 当一个自定义类声明了__iter__(self):的魔法函数,变成了可迭代对象
    • 可迭代对象的本质:对象所述的类中包含__iter__(self):魔法函数
  • 怎么检测对象是否可以迭代
    • isinstance(被检测的对象,Iterable)
    • True可迭代,False不可迭代

[重、难点]迭代器及其使用方法

  • 迭代器

    1. 记录当前迭代位置
    2. 配合**next()**获取可迭代对象的下一条数据
  • 获取可迭代对象的迭代器

    迭代器 = iter(可迭代对象)

  • 获取可迭代对象的下一条数据

    数据 = next(迭代器)

  • 自定义迭代对象

    # 自定义的可迭代对象类
    # 声明`__iter__`
    class MyClass:
    
        def __iter__(self):
            # return iter([1,2,3,4])
            # return [1,3,5].__iter__()
            return MyIterator()
        
    # 自定义迭代器类
    # 声明2个函数`__iter__`,`__next__`
    class MyIterator:
        """自定义迭代器"""
        def __iter__(self):
            pass
    
        def __next__(self):
            pass
    

注意,当我们已经迭代完最后一个数据之后,再次调用next()函数会抛出StopIteration的异常,来 告诉我们所有数据都已迭代完成,不用再执行next()函数了。

[重、难点]自定义迭代对象、迭代器

  • 一、创建自定义类
    1)构造函数
    2)声明addItem()添加数据
    3)声明__iter__()返回迭代器

  • 二、创建迭代器类
    1)构造函数
    2)声明__iter__()函数
    2)声明__next__()函数

  • 三、实现目标

    lst = MyList()
    lst.addItem("永强")
    lst.addItem("赵四")
    lst.addItem("安红")
    
    for item in lst:
        print(item)
    

迭代器案例:斐波那契数列

  • 迭代器:可以根据特定的规则动态的生成数列
  • 斐波那契数列:
    • 变量a保存前一个值
    • 变量b保存最新的值
    • 更新两个值self.a, self.b = (self.b, self.a + self.b)

[重点]生成器-基本使用

  • 生成器的创建有两种方式:

    • 类似推导式的结构

      lst_genrator = (i * 2 for i in range(5))
      print(type(lst_genrator),lst_genrator)
      # <class 'generator'> <generator object calc at 0x003B7C70>
      for item in lst_genrator:
          print(item)
      
    • yield

      • 声明函数
      • 在函数中使用yield
      • 调用函数时,创建了一个生成器对象,函数并没有执行
      • 通过next(生成器)获取下一个值

      得到既是可迭代对象,也是迭代器

生成器案例:斐波那契数列

  • yield特点:
    • 充当return的效果,把值导出
    • 会冻结/挂起当前代码
    • 直到生成器执行下一次next时,从刚刚的位置唤醒,返回下一次的yield值
def fibonacci(num):
    # 1)初始化a,b
    a, b = 1, 1
    # 2)初始化位置索引
    current_index = 0
    print("a" * 30)

    # 3)循环修改a、b
    while current_index < num:
        print("b" * 30)
        # 临时保存a
        yield a
        # 更新a、b值
        a, b = b, a+b
        # 更新位置索引
        current_index += 1
        print("c" * 30)

if __name__ == '__main__':
    fib = fibonacci(10)

    value = next(fib)
    print(value)

    try:
        while True:
            value = next(fib)
            print(value)
    except:
        pass

生成器-使用拓展

  • send
    • 也可以用来在yield唤醒生成器
    • 第一次唤醒生成器必须使用next(生成器)生成器.send(None)
    • 在唤醒生成器时给生成器刚刚yield的位置设置一个值
    • 生成器.send(None)返回的是下一个yield的内容
  • return
    • 返回值不再是nextsend结果,也不是函数调用的结果
    • 对StopIteration的异常描述
    • 可有可无的

协程-yield

import time
"""
使用yield简单实现协程
"""

def work1():
    counter = 0
    while True:
        print("work1....执行中", counter)
        time.sleep(0.5)
        yield
        counter += 1


def work2():
    counter = 0
    while True:
        print("work2.....................执行中", counter)
        time.sleep(0.5)
        yield
        counter += 1

if __name__ == '__main__':
    gen1 = work1()
    gen2 = work2()

    while True:
        next(gen1)
        next(gen2)

协程-greenlet

from greenlet import greenlet
import time

def work1():
    while True:
        print("work1....")
        # 切换其他协程
        gl2.switch()
        time.sleep(0.5)


def work2():
    while True:
        print("work2...............")
        # 切换其他协程
        gl1.switch()
        time.sleep(0.5)

if __name__ == '__main__':
    # 创建两个greenlet对象
    gl1 = greenlet(work1)
    gl2 = greenlet(work2)

    gl1.switch()

[重点]协程-gevent

  • 使用gevent.sleep(10)
import gevent

def work1():
    for i in range(5):
        print("work1......", i)
        gevent.sleep(1)

def work2():
    for i in range(5):
        print("work2.............", i)
        gevent.sleep(1)


if __name__ == '__main__':
    # 指派任务
    g1 = gevent.spawn(work1)
    g2 = gevent.spawn(work2)

    gevent.sleep(10)
    # time.sleep(10)
    # g1.join()
    # g2.join()
  • 使用猴子补丁
import gevent
from gevent import monkey
monkey.patch_all()

import time
import threading

def work1():
    for i in range(5):
        print("work1......", i, gevent.getcurrent(), threading.current_thread())
        time.sleep(1)

def work2():
    for i in range(10):
        print("work2.............", i, gevent.getcurrent(), threading.current_thread())
        time.sleep(1)


if __name__ == '__main__':
    print("主线程:",gevent.getcurrent(), threading.current_thread())
    # 给gevent指派任务
    g1 = gevent.spawn(work1)
    g2 = gevent.spawn(work2)

    print("11111")
    g1.join()
    print("22222")
    g2.join()
    print("33333")

进程、线程、协程区别

  • 多进程:
    • 密集CPU任务,需要充分使用多核CPU资源(服务器,大量的并行计算)的时候,用多进程。
    • 缺陷:多个进程之间通信成本高,切换开销大。
  • 多线程:
    • 密集I/O任务(网络I/O,磁盘I/O,数据库I/O)使用多线程合适。
    • 缺陷:同一个时间切片只能运行一个线程,不能做到高并行,但是可以做到高并发。
  • 协程:
    • 当程序中存在大量不需要CPU的操作时(IO),适用于协程;
    • 缺陷:单线程执行,处理密集CPU和本地磁盘IO的时候,性能较低。处理网络I/O性能还是比较高.
  • 使用多进程+协程

案例:并发下载器

from gevent import monkey
monkey.patch_all()

from urllib import request

import gevent


def download_url(img_url, file_name):
    print("开始下载:",img_url, file_name)
    # 下载数据
    response = request.urlopen(img_url)

    # 准备文件,接收网络数据
    with open(file_name, "wb") as file:
        while True:
            data = response.read(4096)
            if data:
                file.write(data)
            else:
                break

    print("\33[42;1m 文件下载完毕 \033[0m{},文件名:{}".format(img_url, file_name))


if __name__ == '__main__':
    img_url1 = "http://img.mp.itc.cn/upload/20170716/8e1b835f198242caa85034f6391bc27f.jpg"
    img_url2 = "http://img.mp.sohu.com/upload/20170529/d988a3d940ce40fa98ebb7fd9d822fe2.png"
    img_url3 = "http://image.uczzd.cn/11867042470350090334.gif?id=0&from=export"

    g1 = gevent.spawn(download_url, img_url1, "1.gif")
    g2 = gevent.spawn(download_url, img_url2, "2.gif")
    g3 = gevent.spawn(download_url, img_url3, "3.gif")

    gevent.joinall([g1, g2, g3])
    # g1.join()
    # g2.join()
    # g3.join()

案例:协程版Web服务器

发布了40 篇原创文章 · 获赞 3 · 访问量 1397

猜你喜欢

转载自blog.csdn.net/hongge_smile/article/details/104242026