Python中的多任务(线程、进程、协程)

1. 多任务的概念

多任务简单地说,就是操作系统可以同时运行多个任务。分为并行和并发两种。

1.1 并行

指的是任务数小于等于CPU核数,即任务真的是一起执行的

1.2 并发

指的是任务数多于CPU核数,通过操作系统的各种任务调度算法,实现用多个任务一起执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

1.3 网络的概念

网络就是辅助双方或多方,连接在一起的辅助工具

1.4 网络的作用

网络能够实现数据的相互传送,能够通信,互相传递信息等。

1.5 ip地址

ip地址就是在网络中标记一台计算机的地址,比如192.168.1.1;在本地局域网上是唯一的。

  1. 在这么多网络IP中,国际规定有一部分IP地址是用于我们的局域网使用,也就

是属于私网IP,不在公网中使用的,它们的范围是:

10.0.0.0~10.255.255.255

172.16.0.0~172.31.255.255

192.168.0.0~192.168.255.255
复制代码
  1. 注意

IP地址127.0.0.1~127.255.255.255用于回路测试,

如:127.0.0.1可以代表本机IP地址,用http://127.0.0.1就可以测试本机中配置的Web服务器。

1.6 端口(port)

端口号就是电脑上某个程序对应的地址,用来标记某个程序,端口号只有整数,范围是从0到65535

  • 知名端口:知名端口是众所周知的端口号,范围从0到1023,都已被占用
  • 动态端口:动态端口的范围是从1024到65535,之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。

1.5 socket

socket是一种进程之间的通信方式,简称套接字。它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的

例如我们每天浏览网页、QQ 聊天、收发 email 等等。

  • 创建socket
import socket
socket.socket(AddressFamily, Type)
复制代码

说明:

函数 socket.socket 创建一个 socket,该函数带有两个参数:

Address Family:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)

2. 线程

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用

2.1 多线程执行

#coding=utf-8
import threading
import time

def saySorry():
    print("我能吃饭了吗?")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=saySorry)
        t.start() #启动线程,即让线程开始执行
复制代码
  • 使用的是多线程并发的操作,当调用start()时,才会真正的创建线程,并且开始执行
  • 主线程结束后,子进程立即结束

2.2 线程执行代码的封装

python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。

2.3 线程的执行顺序

多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞,到sleep结束后,线程进入就绪状态,等待调度。而线程调度将自行选择一个线程执行。

2.4 多线程共享全局变量

  • 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
  • 缺点就是,线程是对全局变量随意更改可能造成多线程之间对全局变量的混乱
  • 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确

2.5 互斥锁

当多个线程几乎同时修改某个共享数据的时候,需要进行同步控制。

某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()
复制代码
  • 如果这个锁之前没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

2.6 死锁

在线程键共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

可以添加超时时间等,解决死锁

案例:多线程版udp聊天器

import socket
import threading


def send_msg(udp_socket):
    """获取键盘数据,并将其发送给对方"""
    while True:
        # 1. 从键盘输入数据
        msg = input("\n请输入要发送的数据:")
        # 2. 输入对方的ip地址
        dest_ip = input("\n请输入对方的ip地址:")
        # 3. 输入对方的port
        dest_port = int(input("\n请输入对方的port:"))
        # 4. 发送数据
        udp_socket.sendto(msg.encode("utf-8"), (dest_ip, dest_port))


def recv_msg(udp_socket):
    """接收数据并显示"""
    while True:
        # 1. 接收数据
        recv_msg = udp_socket.recvfrom(1024)
        # 2. 解码
        recv_ip = recv_msg[1]
        recv_msg = recv_msg[0].decode("utf-8")
        # 3. 显示接收到的数据
        print(">>>%s:%s" % (str(recv_ip), recv_msg))


def main():
    # 1. 创建套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 2. 绑定本地信息
    udp_socket.bind(("", 7890))

    # 3. 创建一个子线程用来接收数据
    t = threading.Thread(target=recv_msg, args=(udp_socket,))
    t.start()
    # 4. 让主线程用来检测键盘数据并且发送
    send_msg(udp_socket)

if __name__ == "__main__":
    main()
复制代码

3. 进程

multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time

def run_proc():
    """子进程要执行的代码"""
    print('子进程运行中,pid=%d...' % os.getpid())  # os.getpid获取当前进程的进程号
    print('子进程将要结束...')

if __name__ == '__main__':
    print('父进程pid: %d' % os.getpid())  # os.getpid获取当前进程的进程号
    p = Process(target=run_proc)
    p.start()
复制代码

3.1 process语法结构如下:

Process([group [, target [, name [, args [, kwargs]]]]])

  • target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码

  • args:给target指定的函数传递的参数,以元组的方式传递

  • kwargs:给target指定的函数传递命名参数

  • name:给进程设定一个名字,可以不设定

  • group:指定进程组,大多数情况下用不到 Process创建的实例对象的常用方法:

  • start():启动子进程实例(创建子进程)

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

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

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

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

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

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

进程之间不共享全局变量

3.2 进程与线程的区别

  • 进程是系统进行资源分配和调度的一个独立单位
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本不拥有系统资源,只拥有一点在运行中必不可少的资源,但是它可与同属一个进程的其它线程共享进程所拥有的全部资源
  • 一个程序至少有一个进程,一个进程至少有一个线程
  • 线程的划分尺度小于进程,使得多线程序的并发性高。
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  • 线程不能独立执行,必须依存在进程中
  • 可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
  • 线程和进程在使用上各有优缺点:线程执行开销小。但不利于资源的管理和保护,而进程正相反。

3. 进程池Pool

当需要创建的子进程数量巨大时,就可以用到multiprocessing模块提供的Pool方法

  • apply_async(func[,args[,kwds]]):使用非阻塞方式调用func,(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表
  • close():关闭Pool,使其不再接收新的任务
  • terminate():不管任务是否完成,立即终止
  • join():主进程阻塞,等待子进程的退出,必须在close或terminate之后使用
  • 在程序执行过程中,关闭进程池,则程序会立即停止,不会再继续执行后续语句。

4. Queue

进程是通过导入Queue,用Queue实现多进程之间的数据传递,Queue本身是一个消息队列程序。

  • Queue.qsize():返回当前队列包含的消息数量
  • Queue.empty():如果队列为空,返回True,反之False
  • Queue.full():如果队列满了,返回True,反之False
  • Queue.get([block[,timeout]]):获取队列中的一条消息,然后将其从队列中移除,block默认值为True
  • Queue.put_nowait(item):相当Queue.put(item,False)

5. 迭代器

5.1 可迭代对象

  1. 我们把可以通过for...in...这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable)**
  2. 可迭代对象的本质就是可以向我们提供一个这样的中间"人"即迭代器帮助我们对其进行迭代遍历使用。
  3. 可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据,也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。

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

  • 可以使用isinstance()判断一个对象是否是iterable对象
  • 生成器是一类特殊的迭代器

5.3 创建生成器的方法

  1. 只要把一个列表生成式的[]改成()
  2. 只要在def中有yield关键字的就称为生成器,使用了yield关键字的函数不再是函数,而是生成器

5.4 唤醒

除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒来执行,使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据

6. gevent

6.1 geven使用

import gevent
def f(n):
    for i in range(n):
        print(gevent.getcurrent(),i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
复制代码

运行结果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4
复制代码

3个greenlet是依次运行而不是交替运行,可以导入time,添加延时切换执行

6.2 线程、进程、协程对比

  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源很大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据CPU核数不一样可能是并行的,但是协程是在一个线程中所以是并发

7. 阻塞和非阻塞

  1. Python中遇到recv、accept、等语句时,是默认阻塞的,即如果不设置一些条件时,程序会一直等待下去。

  2. 而非阻塞就是通过设置条件,把阻塞变为非阻塞,所以转变语法需要在阻塞之前设置

     例:server_socket.setblocking(False)
    复制代码

8. Http协议

8.1 浏览器向服务器发送http请求

请求包括:请求头、请求体、请求行

8.2 服务器向浏览器返回HTTP响应

响应包括:响应头、响应行、响应体

9 长连接和短连接

9.1 短连接

短连接就是建立连接、接收数据、关闭连接,每次只获取1次数据

9.2 长连接

长连接就是建立连接后,多次请求,直到没有请求后关闭的连接

代码最后有close操作的其实都是短连接,长连接不能在连接中强制调用close

10. 三次握手,四次挥手

TCP通信的整个过程

三次握手:建立连接时,客户端向服务器发送连接请求,服务器向客户端回应请求的同时向客户端发送连接请求,客户端回应请求,服务器收到时,三次握手成功,双方连接成功。

四次挥手:客户端调用close时,向服务器发送请求,服务器回应请求同时解堵塞,调用自己close后,再次向客户端发送close请求,此时客户端方会等待两个最大报文时间,等待接收服务器的请求(等待是为了避免断网,断电等特殊情况),收到服务器的请求后,向服务器回应请求,服务器收到请求后关闭,4次挥手成功,双方关闭连接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值