python--基础知识点--multiprocessing

手册:multiprocessing — 基于进程的并行
Python multiprocessing模块中常用函数和类的统计

一. 进程通信

(对于进程间通信此处理解不清,可能有误)
进程间通信方式:

  • 消息机制:Pipe、Queue
  • 共享内存:Value、Array
  • 共享文件:mmap模块
  • 同步原语:Lock、RLock、Event、Semaphore、Condition。

消息机制 (Pipe、Queue)

  • 效率高
  • 帮我们处理好锁问题。

共享文件

  • 1.效率低(共享数据基于文件,而文件是硬盘上的数据)
  • 2.需要自己加锁处理

同步原语(进程锁, 信号量 , 事件 )
这些有锁的功能的东西(能够阻塞)是全局的,加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。

多进程应该尽可能避免在进程间传递大量数据,越少越好。

二. Lock、RLock、Event、Semaphore、Condition、Pipe、Queue、JoinableQueue

  • multiprocess.Value()
  • multiprocess.Array()
  • multiprocessing.Pipe()
  • multiprocessing.Queue()
  • multiprocessing.JoinableQueue()
  • multiprocessing.Lock()
  • multiprocessing.RLock()
  • multiprocessing.Event()
  • multiprocessing.Semaphore()
  • multiprocessing.BoundedSemaphore()
  • multiprocessing.Condition()

multiprocessing的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition接口与threading接口相同。
threading中的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition详解

multiprocessing.Queue()的接口与用于多线程的queue.Queue()类似。queue.Queue()比 multiprocessing.Queue()多两个方法task_done和join。multiprocessing.JoinableQueue()接口与queue.Queue()接口相同。
queue模块详解

使用场景:Pipe、Queue只适用于多个进程都是源于同一个父进程的情况。如果多个进程不是源于同一个父进程,只能用共享内存,信号量等方式,但是这些方式对于复杂的数据结构,例如Queue,dict,list等,使用起来比较麻烦,不够灵活,这种情况使用Manager。

使用方法:

  • (multiprocessing的Value、Array、Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition) + (multiprocessing.Process())
  • (multiprocessing的Value、Array、Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition) +(concurrent.futures.ProcessPoolExecutor())
1. Lock
1.1 Lock示例
import multiprocessing


def sub(a, lock):
    print(f"sub_start;{a.value}")
    # lock.acquire() # 对于该程序来说在此处获取锁效率更高,避免多次进程间通信
    for i in range(100000):
        lock.acquire()
        b = a.value
        b -= 1
        a.value = b
        lock.release()
    # lock.release()
    print(f"sub_end;{a.value}")


def add(a, lock):
    print(f"add_start;{a.value}")
    for i in range(100000):
        lock.acquire()
        b = a.value
        b += 1
        a.value = b
        lock.release()
    print(f"add_end;{a.value}")


if __name__ == '__main__':
    a = multiprocessing.Value("i", 0)  # a 属于共享内存中的变量
    lock = multiprocessing.Lock()  # lock也属于共享内存中的变量
    tasks = [multiprocessing.Process(target=add, args=(a, lock), name="add"), multiprocessing.Process(target=sub, args=(a, lock), name="sub")]
    [task.start() for task in tasks]
    [task.join() for task in tasks]
    print(a.value)


"""
运行结果:
add_start;0
sub_start;5011
add_end;3511
sub_end;0
0

Process finished with exit code 0
"""
1.2 补充:multiprocessing.Value()

1.2.1 Value的构造函数:

Value的初始化非常简单,直接类似Value(‘d’, 0.0)即可,具体构造方法为:

multiprocessing.Value(typecode_or_type, *args[, lock])。

该方法返回从共享内存中分配的一个ctypes 对象,其中typecode_or_type定义了返回的类型。它要么是一个ctypes类型,要么是一个代表ctypes类型的code。比如c_bool和’b’是同样的,因为’b’是c_bool的code。

ctypes是Python的一个外部函数库,它提供了和C语言兼任的数据类型,可以调用DLLs或者共享库的函数,能被用作在python中包裹这些库。

*args是传递给ctypes的构造参数

1.2.2 Value的使用

对于共享整数或者单个字符,初始化比较简单,参照下图映射关系即可。
python--基础知识点--multiprocessing_第1张图片
注意,如果我们使用的code在上表不存在,则会抛出:

size = ctypes.sizeof(type_)
TypeError: this type has no size

如果共享的是字符串,则在上表是找不到映射关系的,就是没有code可用。所以我们需要使用原始的ctype类型

ctype类型可从下表查阅
python--基础知识点--multiprocessing_第2张图片
1.2.3 示例

from multiprocessing import Value
from ctypes import *


a = Value("i", 2)
print(a.value)
print(a)
a = Value(c_int, 2)
print(a.value)
print(a)
a = Value("f", 3.14)
print(a.value)
print(a)
a = Value(c_float, 3.14)
print(a.value)
print(a)
a = Value('c', b'a')
print(a.value.decode("ascii"))
print(a)
a = Value(c_char, b'a')
print(a.value.decode("ascii"))
print(a)
a = Value(c_char_p, bytes("中国", "utf-8"))
print(a.value.decode("utf-8"))
print(a)


"""
运行结果:
2

2

3.140000104904175

3.140000104904175

a

a

中国


Process finished with exit code 0
"""

参考博客

2. RLock
import multiprocessing


def fibonacci(num, total, lock):
    if num == 2 or num == 1:
        return 1
    else:
        lock.acquire()
        print("lock已为total上锁")
        total.value = fibonacci(num - 1, total, lock) + fibonacci(num - 2, total, lock)
        lock.release()
        print("lock已为total释放锁")
        return total.value


if __name__ == '__main__':
    total = multiprocessing.Value("i", 0)
    lock = multiprocessing.RLock()
    task = multiprocessing.Process(target=fibonacci, args=(6, total, lock))
    task.start()
    task.join()
    print(total.value)


"""
运行结果:
lock已为total上锁
lock已为total上锁
lock已为total上锁
lock已为total上锁
lock已为total释放锁
lock已为total释放锁
lock已为total上锁
lock已为total释放锁
lock已为total释放锁
lock已为total上锁
lock已为total上锁
lock已为total释放锁
lock已为total释放锁
lock已为total释放锁
8

Process finished with exit code 0
"""
3. Event
import multiprocessing
import time


def test0(event):
    event.clear()
    print(f"{multiprocessing.current_process().name}_start========")
    time.sleep(1)
    print(f"{multiprocessing.current_process().name}_end========")
    event.set()


def test1(event):
    event.wait()
    print(f"{multiprocessing.current_process().name}_end========")


if __name__ == '__main__':
    event = multiprocessing.Event()
    task0 = multiprocessing.Process(target=test0, args=(event,), name="Pro_test0")
    task1 = multiprocessing.Process(target=test1, args=(event,), name="Pro_test0")

    task0.start()
    task1.start()

    task0.join()
    task1.join()


"""
运行结果:
Pro_test0_start========
Pro_test0_end========
Pro_test0_end========

Process finished with exit code 0
"""
4. Semaphore
import multiprocessing
import time
import random


def db_connect(num, semaphore):
    print(f"thread =={num}== is preparing to connect to db.")
    semaphore.acquire()
    time.sleep(int(random.random()*10)/10)  # 模拟连接db的过程
    print(f"connection =={num}== already complete.")
    semaphore.release()


if __name__ == '__main__':
    semaphore = multiprocessing.Semaphore(value=2)
    tasks = [multiprocessing.Process(target=db_connect, args=(num, semaphore)) for num in range(7)]
    [task.start() for task in tasks]
    [task.join() for task in tasks]
    semaphore.release()  # Semaphore对象释放多次不会抛出异常


"""
运行结果:
thread ==0== is preparing to connect to db.
thread ==1== is preparing to connect to db.
connection ==0== already complete.
thread ==2== is preparing to connect to db.
connection ==1== already complete.
connection ==2== already complete.
thread ==4== is preparing to connect to db.
thread ==3== is preparing to connect to db.
connection ==3== already complete.
thread ==5== is preparing to connect to db.
thread ==6== is preparing to connect to db.
connection ==4== already complete.
connection ==6== already complete.
connection ==5== already complete.

Process finished with exit code 0
"""
5. Condition
import multiprocessing
import time


def task_1(condition_obj):
    proc_name = multiprocessing.current_process().name
    print('开始 %s' % proc_name)
    with condition_obj:
        print('%s运行结束,开始运行task_2' % proc_name)
        condition_obj.notify_all()


def task_2(condition_obj):
    proc_name = multiprocessing.current_process().name
    print('开始 %s' % proc_name)
    with condition_obj:
        condition_obj.wait()
        print('task_2 %s 运行结束' % proc_name)


if __name__ == '__main__':
    condition_obj = multiprocessing.Condition()

    s1 = multiprocessing.Process(name='s1', target=task_1, args=(condition_obj,))

    s2_clients = [multiprocessing.Process(name='task_2[{}]'.format(i), target=task_2, args=(condition_obj,),) for i in range(1, 3)]

    for c in s2_clients:
        c.start()
        time.sleep(1)
    s1.start()

    s1.join()
    for c in s2_clients:
        c.join()


"""
运行结果:
开始 task_2[1]
开始 task_2[2]
开始 s1
s1运行结束,开始运行task_2
task_2 task_2[2] 运行结束
task_2 task_2[1] 运行结束

Process finished with exit code 0
"""
6. 消息机制Pipe

Pipe()函数返回两个对象 conn1 和 conn2 ,这两个对象表示管道的两端。

Pipe()函数有一个可选参数 duplex,参数 duplex 的默认值为 True,表示该管道是双向的,即两个对象都可以发送和接收消息。如果把参数 duplex 设置为 False ,表示该管道是单向的,即 conn1 只能用于接收消息,conn2 只能用于发送消息。

# 双向管道
import multiprocessing


def test0(conn):
    conn.send("test1,我是test0")
    print(conn.recv())


def test1(conn):
    print(conn.recv())
    conn.send("test0,你好")


if __name__ == '__main__':
    conn0, conn1 = multiprocessing.Pipe(duplex=True)
    task0 = multiprocessing.Process(target=test0, args=(conn0,))
    task1 = multiprocessing.Process(target=test1, args=(conn1,))

    task0.start()
    task1.start()

    task0.join()
    task1.join()


"""
运行结果:
test1,我是test0
test0,你好

Process finished with exit code 0
"""
# 单向管道
import multiprocessing


def test0(conn):
    # conn.send("test1,我是test0")
    print(conn.recv())


def test1(conn):
    # print(conn.recv())
    conn.send("test0,你好")


if __name__ == '__main__':
    conn0, conn1 = multiprocessing.Pipe(duplex=False)  # conn0接收消息,conn1发送消息
    task0 = multiprocessing.Process(target=test0, args=(conn0,))
    task1 = multiprocessing.Process(target=test1, args=(conn1,))

    task0.start()
    task1.start()

    task0.join()
    task1.join()


"""
运行结果:
test0,你好

Process finished with exit code 0

"""
7. 消息机制Queue

原型:Queue([maxsize])

参数介绍

  • maxsize是队列中允许最大项数,省略则无大小限制。

当一个队列为空的时候如果再用get取则会堵塞,所以取队列的时候一般是用到get_nowait()方法,这种方法在向一个空队列取值的时候会抛一个Empty异常,所以更常用的方法是先判断一个队列是否为空,如果不为空则取值。

队列Queue的实例对象queue中常用的方法

  • queue.qsize()返回队列的大小
  • queue.empty()如果队列为空,返回True,反之False
  • queue.full()如果队列满了,返回True,反之False
  • queue.get([block[, timeout]]) 获取队列,timeout等待时间
  • queue.get_nowait()相当Queue.get(False) ,非阻塞 Queue.put(item) 写入队列,timeout等待时间
  • queue.put_nowait(item) 相当Queue.put(item, False)
import multiprocessing
import time


def test0(queue, num):
    for i in range(num*3):
        queue.put(f"{num}: {i}")


def test1(queue):
    flag = 0
    while True:
        if not queue.empty():
            print(queue.get())
            flag = 0
        else:
            time.sleep(0.1)
            flag += 1
            if flag == 2:
                break


if __name__ == '__main__':
    queue = multiprocessing.Queue()
    task0 = [multiprocessing.Process(target=test0, args=(queue, num)) for num in range(1, 3)]
    task1 = multiprocessing.Process(target=test1, args=(queue,))

    [task.start() for task in task0]
    task1.start()

    [task.join() for task in task0]
    task1.join()


"""
运行结果:
1: 0
1: 1
1: 2
1: 3
1: 4
2: 0
2: 1
2: 2
2: 3
2: 4
2: 5
2: 6
2: 7
2: 8
2: 9

Process finished with exit code 0
"""
8. 消息机制JoinableQueue

原型:JoinableQueue([maxsize])

  • 这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

参数介绍

  • maxsize是队列中允许最大项数,省略则无大小限制。

当一个队列为空的时候如果再用get取则会堵塞,所以取队列的时候一般是用到get_nowait()方法,这种方法在向一个空队列取值的时候会抛一个Empty异常,所以更常用的方法是先判断一个队列是否为空,如果不为空则取值。

JoinableQueue的实例queue队列中常用的方法:

  • queue.qsize() 返回队列的大小
  • queue.empty() 如果队列为空,返回True,反之False
  • queue.full() 如果队列满了,返回True,反之False
  • queue.get([block[, timeout]]) 获取队列,timeout等待时间
  • queue.get_nowait() 相当Queue.get(False) ,非阻塞 Queue.put(item) 写入队列,timeout等待时间
  • queue.put_nowait(item) 相当Queue.put(item, False)
  • JoinableQueue的实例queue除了与Queue对象相同的方法之外还具有:
    • queue.task_done():使用者使用此方法发出信号,表示queue.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
    • queue.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用queue.task_done()方法为止
import multiprocessing
import time


def test0(queue, num):
    for i in range(num*3):
        queue.put(f"{num}: {i}")


def test1(queue):
    flag = 0
    while True:
        if not queue.empty():
            print(queue.get())
            queue.task_done()
            flag = 0
        else:
            time.sleep(0.1)
            flag += 1
            if flag == 2:
                break


if __name__ == '__main__':
    queue = multiprocessing.JoinableQueue(maxsize=1)
    task0 = [multiprocessing.Process(target=test0, args=(queue, num)) for num in range(1, 3)]
    task1 = multiprocessing.Process(target=test1, args=(queue,))

    [task.start() for task in task0]
    task1.start()

    # [task.join() for task in task0]
    # task1.join()
    queue.join()


"""
运行结果:
1: 0
1: 1
2: 0
1: 2
2: 1
2: 2
2: 3
2: 4
2: 5

Process finished with exit code 0
"""

三. Pool、Manager

1. 进程池Pool

Pool类可以提供指定数量的进程供用户调用,当有新的请求提交到Pool中时,如果池还没有满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求

下面介绍一下multiprocessing 模块下的Pool类下的几个方法:

apply(func[, args=()[, kwds={}]])

  • 该函数用于传递不定参数,主进程会被阻塞直到函数执行结束

apply_async(func[, args=()[, kwds={}[, callback=None]]])

  • 与apply用法一致,但它是非阻塞的且支持结果返回后进行回调

map(func, iterable[, chunksize=None])

  • Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到结果返回
    注意:虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程

map_async(func, iterable[, chunksize[, callback]])

  • 与map用法一致,但是它是非阻塞的

close()

  • 关闭进程池(pool),使其不再接受新的任务

terminal()

  • 结束工作进程,不再处理未处理的任务

join()

  • 主进程阻塞等待子进程的退出, join方法要在close或terminate之后使用
2. 服务器进程管理器对象moprocessing.Manager()
2.1 概念
  • Manager() 返回的管理器对象控制一个服务器进程,该进程保存Python对象并允许其他进程使用代理操作它们。
  • 服务器进程管理器比使用共享内存对象更灵活,因为它支持Python支持的所有数据类型。
  • 单个管理器可以通过网络由不同计算机上的进程共享。但是,它们比使用共享内存
  • 源于不同父进程的子进程之间可以通过Manager()对象通信。

原理:先启动一个ManagerServer进程,这个进程是阻塞的,它监听一个socket,然后其他进程(ManagerClient)通过socket来连接到ManagerServer,实现通信。

2.2 Manager() 返回的管理器支持类型
  • Manager().dict()
  • Manager().Array()
  • Manager().Namespace()

  • Manager().Value()
  • Manager().list()
  • Manager().Queue()
  • Manager().Lock()
  • Manager().RLock()
  • Manager().Event()
  • Manager().Semaphore()
  • Manager().BoundedSemaphore()
  • Manager().Condition()

Manager()的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition接口与threading接口相同。
threading中的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition详解

Manager()的Queue、Value、Array的接口与multiprocess的Queue、Value、Array相同。

因此此处只谈接口list、dict、Namespace接口。

使用场景:

  • 源于不同父进程的子进程之间可以通过Manager()对象通信。
  • 不同主机间的进程通信。
  • 同一父进程创建的子进程间通信,不建议使用,效率较低。

使用方法:

  • Manager() + Pool()
  • Manager() + multiprocessing.Process(),Manager() + concurrent.futures.ProcessPoolExecutor(),不建议使用,因Manager()底层走的是socket,效率较低。
2.3 示例

2.3.1 list、dict

# list
import multiprocessing


def add(temp_list, lock):
    print(f"++++add_{multiprocessing.current_process().ident}_start:{temp_list[0]}++++")
    for i in range(10000):
        lock.acquire()
        temp_list[0] += 1
        lock.release()
    print(f"++++add_{multiprocessing.current_process().ident}_end:{temp_list[0]}++++")


def sub(temp_list, lock):
    print(f"++++sub_{multiprocessing.current_process().ident}_start:{temp_list[0]}++++")
    for i in range(10000):
        lock.acquire()
        temp_list[0] -= 1
        lock.release()
    print(f"++++sub_{multiprocessing.current_process().ident}_end:{temp_list[0]}++++")


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    temp_list = manager.list([0])
    lock = manager.Lock()

    pool = multiprocessing.Pool(2)
    for num in range(2):
        pool.apply_async(add, args=(temp_list, lock))

    for num in range(2):
        pool.apply_async(sub, args=(temp_list, lock))

    pool.close()
    pool.join()
    print(temp_list)


"""
运行结果:
++++add_14680_start:0++++
++++add_9740_start:20++++
++++add_14680_end:19983++++
++++sub_14680_start:19991++++
++++add_9740_end:19992++++
++++sub_9740_start:19982++++
++++sub_14680_end:15++++
++++sub_9740_end:0++++
[0]

Process finished with exit code 0
"""
# dict
import multiprocessing


def add(temp_dict, lock):
    print(f"++++add_{multiprocessing.current_process().ident}_start:{temp_dict['test']}++++")
    for i in range(10000):
        lock.acquire()
        temp_dict['test'] += 1
        lock.release()
    print(f"++++add_{multiprocessing.current_process().ident}_end:{temp_dict['test']}++++")


def sub(temp_dict, lock):
    print(f"++++sub_{multiprocessing.current_process().ident}_start:{temp_dict['test']}++++")
    for i in range(10000):
        lock.acquire()
        temp_dict['test'] -= 1
        lock.release()
    print(f"++++sub_{multiprocessing.current_process().ident}_end:{temp_dict['test']}++++")


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    temp_dict = manager.dict({'test': 0})
    lock = manager.Lock()

    pool = multiprocessing.Pool(2)
    for num in range(2):
        pool.apply_async(add, args=(temp_dict, lock))

    for num in range(2):
        pool.apply_async(sub, args=(temp_dict, lock))

    pool.close()
    pool.join()
    print(temp_dict['test'])


"""
运行结果:
++++add_11360_start:0++++
++++add_476_start:17++++
++++add_476_end:19990++++
++++sub_476_start:19998++++
++++add_11360_end:19999++++
++++sub_11360_start:19991++++
++++sub_476_end:13++++
++++sub_11360_end:0++++
0

Process finished with exit code 0
"""

补充list、dict
Manager处理list、dict等可变数据类型时,需要注意一个陷阱,即Manager对象无法监测到它引用的可变对象值的修改,需要通过触发__setitem__方法来让它获得通知。

而触发__setitem__方法比较直接的办法就是增加一个中间变量,如同在C语言中交换两个变量的值一样:
int a=1;int b=2;int tmp=a;a=b;b=tmp;

示例:

# Manager对象无法监测到它引用的可变对象值的修改
import multiprocessing


def test(idx, test_dict, lock):
    lock.acquire()
    test_dict['test'][idx] = idx
    lock.release()


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    temp_dict = manager.dict()
    lock = manager.Lock()
    temp_dict['test'] = {}

    pool = multiprocessing.Pool(4)
    for i in range(10):
        pool.apply_async(test, args=(i, temp_dict, lock))

    pool.close()
    pool.join()
    print(temp_dict)


"""
运行结果:
{'test': {}}

Process finished with exit code 0
"""
# 通过触发__setitem__方法来让Manager对象获得通知
import multiprocessing


def test(idx, test_dict, lock):
    lock.acquire()
    row = test_dict['test']
    row[idx] = idx
    test_dict['test'] =row
    lock.release()


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    temp_dict = manager.dict()
    lock = manager.Lock()
    temp_dict['test'] = {}

    pool = multiprocessing.Pool(4)
    for i in range(10):
        pool.apply_async(test, args=(i, temp_dict, lock))

    pool.close()
    pool.join()
    print(temp_dict)


"""
运行结果:
{'test': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 6: 6, 7: 7, 5: 5, 8: 8, 9: 9}}

Process finished with exit code 0
"""

2.3.2 Namespace
Namespace对象没有公共的方法,但是有可写的属性。Namespace支持python中所有的基本数据类型,但与dict、list相同Manager对象无法监测到它引用的可变对象值的修改,需要通过触发__setitem__方法来让它获得通知。

当使用manager返回的namespace的proxy的时候,_属性值属于proxy,跟原来的namespace没有关系。

import multiprocessing


def f(ns):
    ns.x *= 10
    ns.y *= 10
    # ns.l[0] *= 10  # Manager对象无法监测到它引用的可变对象值的修改,需要通过触发__setitem__方法来让它获得通知。
    l = ns.l
    l[0] *= 10
    ns.l = l
    if 'testkey' in ns.d:
        v = ns.d['testkey'] * 10
        ns.d = {'testkey':  v}


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    ns = manager.Namespace()
    ns.x = 1
    ns.y = 2
    ns._z = 5  # this is an attribute of the proxy
    ns.d = {'testkey': 3}
    ns.l = [4]
    print('before', ns)
    p = multiprocessing.Process(target=f, args=(ns,))
    p.start()
    p.join()
    print('after', ns)



"""
运行结果:
before Namespace(d={'testkey': 3}, l=[4], x=1, y=2)
after Namespace(d={'testkey': 30}, l=[40], x=10, y=20)

Process finished with exit code 0
"""





[参考博客]
Python进程池multiprocessing.Pool的用法
python 多进程共享全局变量之Manager()详解
python 关于multiprocessing中在Namespace的实例下保存dict/list的疑问

你可能感兴趣的:(python,#,基础知识点)