Python网络与并发编程 13 线程或进程通信

线程或进程通信

在同一个进程中,该进程下的所有数据资源都会被该进程下的线程共享。

为了保证数据安全性,我们在多个线程进行数据交互时必须使用一种线程安全的容器来承载交互信息。

如,常见的Redis数据库、MQ等消息中间件是非常好的选择。

当然,多进程也是如此。

但是在实际的测试环境中,我们需要更加方便的一些工具来测试代码是否准确,这时候就会用到下面介绍的工具了。

多线程通信

queue

queue模块对于多线程通信来讲是十分明智的选择,它使用简单开箱即用,最关键的一点是它是Python的内置模块,故不用经历任何额外的下载、安装过程。

官方文档

queue本身是线程安全的,它其实就是管道 + 锁的组合,它提供了3种队列供用户使用:

  • queue.Queue:先进先出队列
  • queue.LifoQueue:后进先出队列
  • quque.PriorityQueue:优先级队列

以下是queue模块各个队列中提供的常见方法:

方法描述
Queue.qsize()返回当前队列的大小
Queue.empty()判断当前队列是否为空
Queue.full()判断当前队列是否已满
Queue.put(item, block=True, timeout=None)将item放入队列中,如果block为True,则队列已满时进行阻塞,timeout为阻塞超时时间,超过该时间后抛出Full的异常
Queue.put_nowait(item)相当于Queue.put(item, block=False, timeout=None),将item放入队列时一旦队列已满就立即抛出Full的异常
Queue.get(block=True, timeout=None)从队列中取出项目,如果block为True,则队列为空时进行阻塞,timeout为阻塞超时时间,超过该时间后抛出Empty的异常
Queue.get_nowait()相当于Queue.get(block=False, timeout=None),从队列中取出项目时一旦队列为空就立即抛出Empty的异常
Queue.join()阻塞队列,此时队列中不可取出任何数据
Queue.task_done()通知取消阻塞队列,此时队列中可取出数据

先进先出队列

以下是先进先出队列的简单使用:

import queue
q = queue.Queue()  # 可指定maxsize参数,定义当前队列的容量

q.put(1)
q.put(2)
q.put(3)

print(q.get())
print(q.get())
print(q.get())

# 1
# 2
# 3

后进先出队列

以下是后进先出队列的简单使用:

import queue
q = queue.LifoQueue()  # 可指定maxsize参数,定义当前队列的容量

q.put(1)
q.put(2)
q.put(3)

print(q.get())
print(q.get())
print(q.get())

# 3
# 2
# 1

优先级队列

以下是优先级队列的简单使用,出队时优先级较小的先出队:

import queue
q = queue.PriorityQueue()  # 可指定maxsize参数,定义当前队列的容量

q.put([10, "A"])  # [优先级, 数据项]
q.put([50, "B"])
q.put([30, "C"])

print(q.get())
print(q.get())
print(q.get())

# [10, 'A']
# [30, 'C']
# [50, 'B']

队列阻塞

下面是队列阻塞方法Queue.join()和Queue.task_done()的示例:

import threading
import queue


def putTask(article):
    name = "Ken"
    # 放入玫瑰
    q.put(article)
    print("%s put %s" % (name, article))
    # 通知对面可以取了
    q.task_done()


def getTask():
    name = "Jack"
    # 如果先启动该线程,则会阻塞进行等待对吗的task_done()进行通知
    q.join()
    # 取出玫瑰
    print("%s get %s" % (name, q.get()))


if __name__ == "__main__":
    q = queue.Queue()
    gT = threading.Thread(target=getTask)
    pT = threading.Thread(target=putTask, args=("rose", ))
    gT.start()
    pT.start()

# Ken put rose
# Jack get rose

内部原理图示

由于多线程都在一个进程中,故queue这个队列是共享的,任意该进程下的线程都能自由的对其进行数据项的读取。

如下图所示:

image-20210703153216964

多进程通信

multiprocessing.Queue

多进程通信时不可使用普通的queue模块所提供的队列,而必须多进程模块multiprocessing所提供的Queue。

这个进程队列是没有提供task_done()方法与join()方法的,如果你想使用这2个方法,则可以导入multiprocessing.JoinableQueue这个队列。

以下是multiprocessing.Queue()所提供的方法:

方法描述
Queue.qsize()返回当前队列的大小
Queue.empty()判断当前队列是否为空
Queue.full()判断当前队列是否已满
Queue.put(item, block=True, timeout=None)将item放入队列中,如果block为True,则队列已满时进行阻塞,timeout为阻塞超时时间,超过该时间后抛出Full的异常
Queue.put_nowait(item)相当于Queue.put(item, block=False, timeout=None),将item放入队列时一旦队列已满就立即抛出Full的异常
Queue.get(block=True, timeout=None)从队列中取出项目,如果block为True,则队列为空时进行阻塞,timeout为阻塞超时时间,超过该时间后抛出Empty的异常
Queue.get_nowait()相当于Queue.get(block=False, timeout=None),从队列中取出项目时一旦队列为空就立即抛出Empty的异常
Queue.close()关闭队列,该队列将变得不可put()
Queue.join_thread()等待后台线程。这个方法仅在调用了 close()方法之后可用。这会阻塞当前进程,直到后台线程退出,确保所有缓冲区中的数据都被写入管道中
Queue.cancel_join_thread()防止 join_thread() 方法阻塞当前进程。具体而言,这防止进程退出时自动等待后台线程退出。详见 join_thread() 方法

个人还是推荐使用multiprocessing.JoinableQueue这个队列,因为它比multiprocessing.Queue强大一点。

需要注意的是,不管是multiprocessing.Queue还是multiprocessing.JoinableQueue,它们都是先进先出队列。

队列阻塞

以下是使用multiprocessing.JoinableQueue实现的队列阻塞:

import multiprocessing
import queue


def putTask(article):
    name = "Ken"
    # 放入玫瑰
    q.put(article)
    print("%s put %s" % (name, article))
    # 通知对面可以取了
    q.task_done()


def getTask():
    name = "Jack"
    # 如果先启动该线程,则会阻塞进行等待对吗的task_done()进行通知
    q.join()
    # 取出玫瑰
    print("%s get %s" % (name, q.get()))


if __name__ == "__main__":
    q = multiprocessing.JoinableQueue()
    gT = multiprocessing.Process(target=getTask)
    pT = multiprocessing.Process(target=putTask, args=("rose", ))
    gT.start()
    pT.start()

# Ken put rose
# Jack get rose

内部原理

为什么线程队列queue.Queue不能做到进程间数据共享呢?

这是因为进程队列multiprocessing.Queue会采取一种映射的方式来同步数据,所以说进程队列的资源消耗比线程队列要庞大很多。

由于一个进程下的所有线程中的信息是共享的,所以线程队列根本不需要映射关系。

进程队列只是告诉你可以这样使用它达到进程间的数据共享,但是并不推荐你滥用它。

image-20210703153457159

multiprocessing.Pipe

除开使用进程队列来实现进程间的通信,multiprocessing还提供了Pipe管道来进行通信。

他的资源消耗较少并且使用便捷,但是唯一的缺点便是只支持点对点

Pipe有点类似socket通信。但是比socket通信更加简单,它不需要将字符串转换成字节后再进行发送,先来看一个实例:

import multiprocessing
from multiprocessing.connection import Pipe


def putTask(article):
    name = "Ken"
    # 发送玫瑰
    conn1.send(article)
    print("%s send %s" % (name, article))


def getTask():
    name = "Jack"
    # 接收玫瑰
    article = conn2.recv()
    print("%s receive %s" % (name, article))


if __name__ == "__main__":
    conn1, conn2 = multiprocessing.Pipe()  # 实例化2个电话

    gT = multiprocessing.Process(target=getTask)
    pT = multiprocessing.Process(target=putTask, args=("rose", ))
    gT.start()
    pT.start()

# Ken send rose
# Jack receive rose

Pipe()会去创建一个双向链接通道,如下所示:

image-20210703154142651

multiprocessing.Manager

除了进程队列multiprocessing.Queue,管道Pipe之外,multiprocessing还提供了Manager作为共享变量来提供多进程数据交互。

但是这种方式是不应该被直接使用的,因为它相较于进程队列Queue是数据不安全的。当多个进程同时修改一个共享变量势必导致结果出现问题,所以要想使用共享变量还得使用multiprocessin提供的进程锁才行。

  • Manager类是数据不安全的
  • Mangaer类支持的类型非常多,如:value, Array, List, Dict, Queue(进程池通信专用),Lock等。
  • Mangaer实现了上下文管理器,可使用with语句创建多个对象

下面这个例子是使用multiprocessing.Manager来实现进程数据共享:

import multiprocessing
from multiprocessing import Manager


def task_1():
    dic["task_1"] = "A"


def task_2():
    dic["task_2"] = "B"


if __name__ == "__main__":
    with Manager() as m:
        dic = m.dict()

        subProcessIns01 = multiprocessing.Process(target=task_1)
        subProcessIns02 = multiprocessing.Process(target=task_2)

        subProcessIns01.start()
        subProcessIns02.start()

        subProcessIns01.join()
        subProcessIns02.join()

        print(dic)

# {'task_1': 'A', 'task_2': 'B'}

可以看见使用multiprocessing.Manager所提供的数据类用来数据交互展示很方便,但是要操纵数据则需要考虑数据安全问题:

import multiprocessing
from multiprocessing import Manager


def task_1():
    for i in range(1000):
        dic["number"] -= 1


def task_2():
    for i in range(1000):
        dic["number"] += 1


if __name__ == "__main__":
    with Manager() as m:
        dic = m.dict()
        dic["number"] = 0
        
        subProcessIns01 = multiprocessing.Process(target=task_1)
        subProcessIns02 = multiprocessing.Process(target=task_2)

        subProcessIns01.start()
        subProcessIns02.start()

        subProcessIns01.join()
        subProcessIns02.join()

        print(dic)

# 结果三次采集
# {'number': -13}
# {'number': 5}
# {'number': 8}

我们可以使用进程锁,来保证数据一致性:

import multiprocessing
from multiprocessing import Manager


def task_1():
    with lock:
        for i in range(1000):
            dic["number"] -= 1


def task_2():
    with lock:
        for i in range(1000):
            dic["number"] += 1


if __name__ == "__main__":
    lock = multiprocessing.RLock()
    
    with Manager() as m:
        dic = m.dict()
        dic["number"] = 0
        
        subProcessIns01 = multiprocessing.Process(target=task_1)
        subProcessIns02 = multiprocessing.Process(target=task_2)

        subProcessIns01.start()
        subProcessIns02.start()

        subProcessIns01.join()
        subProcessIns02.join()

        print(dic)

# 结果三次采集
# {'number': 0}
# {'number': 0}
# {'number': 0}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值