day23并发编程(下)

day23 并发编程(下)

课程目标:掌握多进程开发的相关知识点并初步认识协程。

今日概要:

  • 多进程开发
  • 进程之间数据共享
  • 进程锁
  • 进程池
  • 协程

1. 多进程开发

进程是计算机中资源分配的最小单元;一个进程中可以有多个线程,同一个进程中的线程共享资源;

进程与进程之间则是相互隔离。

Python中通过多进程可以利用CPU的多核优势,计算密集型操作适用于多进程。

1.1 进程介绍

# 示例:创建进程,还可以通过args传参
import multiprocessing

def task():
	pass

if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task)
    p1.start()
# 创建进程相关必须把代码放在 '__main__'代码块 里面,代码才能适用mac,linux,windows等多系统
import multiprocessing


def task(arg):
	pass

def run():
    # 创建进程
    p = multiprocessing.Process(target=task, args=('xxx',))
    p.start()

if __name__ == '__main__':
    run()

关于在Python中基于multiprocessiong模块操作的进程:

Depending on the platform, multiprocessing supports three ways to start a process. These start methods are

根据不同的平台,支持三种启动进程的方式。这些启动方法是:

  • fork,【“拷贝”父进程几乎所有资源】【支持文件对象/线程锁等传参】【unix】【任意位置开始】【快】

    The parent process uses os.fork() to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic.Available on Unix only. The default on Unix.

    译文:
                  
    # 父进程使用[os.fork()](https://docs.python.org/3/library/os.html#os.fork)来派生Python解释器。当子进程开始时,它实际上与父进程相同。父进程的所有资源都被子进程继承。注意,安全地分叉一个多线程进程是有问题的。仅在Unix上可用。Unix上的默认值。
    
  • spawn,【run参数传必备资源】【不支持文件对象/线程锁等传参】【unix、win】【main代码块开始】【慢】

    The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process object’s run() method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver.Available on Unix and Windows. The default on Windows and macOS.

    译文:
    #父进程启动一个新的python解释器进程。子进程将只继承运行进程对象的 [run()](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.run)方法所需的资源。特别是,父进程中不必要的文件描述符和句柄将不会被继承。与使用*fork*或* *forkserver*.Available *相比,使用这种方法启动进程的速度要慢得多。可在Unix和Windows上使用。Windows和macOS上的默认值。
    
  • forkserver,【run参数传必备资源】【不支持文件对象/线程锁等传参】【部分unix】【main代码块开始】

    When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to use os.fork(). No unnecessary resources are inherited.Available on Unix platforms which support passing file descriptors over Unix pipes.

    译文:
    # 当程序启动并选择*forkserver start * 方法时,服务器进程启动。从那时起,每当需要一个新进程时,父进程就会连接到服务器并请求它派生一个新进程。fork服务进程是单线程的,所以使用[ os.fork() ] (https://docs.python.org/3/library/os.html#os.fork) 是安全的。不继承不必要的资源。在支持通过Unix管道传递文件描述符的Unix平台上可用。
    

Changed in version 3.8: On macOS, the spawn start method is now the default. The fork start method should be considered unsafe as it can lead to crashes of the subprocess. See bpo-33725.

Changed in version 3.4: spawn added on all unix platforms, and forkserver added for some unix platforms. Child processes no longer inherit all of the parents inheritable handles on Windows.

On Unix using the spawn or forkserver start methods will also start a resource tracker process which tracks the unlinked named system resources (such as named semaphores or SharedMemory objects) created by processes of the program. When all processes have exited the resource tracker unlinks any remaining tracked object. Usually there should be none, but if a process was killed by a signal there may be some “leaked” resources. (Neither leaked semaphores nor shared memory segments will be automatically unlinked until the next reboot. This is problematic for both objects because the system allows only a limited number of named semaphores, and shared memory segments occupy some space in the main memory.)

译文:
#*在3.8版更改:*在macOS上,*spawn* start方法现在是默认的。*fork* start方法应该被认为是不安全的,因为它可能导致子进程崩溃。看到(bpo - 33725) (https://bugs.python.org/issue33725)。

#*在3.4版更改:* *spawn*添加到所有unix平台,*forkserver*添加到一些unix平台。在Windows上,子进程不再继承所有父进程的可继承句柄。

# 在Unix上使用* *或* forkserver start methods* 开始方法也将启动*资源追踪*流程跟踪链接命名系统资源(如名称的信号或(“SharedMemory”)(https://docs.python.org/3/library/multiprocessing.shared_memory.html) multiprocessing.shared_memory.SharedMemory对象)创建的过程程序。当所有进程都退出后,资源跟踪器将解除任何剩余被跟踪对象的链接。通常应该没有,但是如果一个进程被一个信号终止,可能会有一些“泄漏”的资源。泄漏信号量和共享内存段都不会被自动解除链接,直到下次重新引导。这对于两个对象都是有问题的,因为系统只允许有限数量的命名信号量,并且共享内存段在主内存中占用一些空间。)

通过如下方式设置模式:

import multiprocessing
multiprocessing.set_start_method("spawn")

官方文档:https://docs.python.org/3/library/multiprocessing.html

  • fork示例:fork会拷贝父进程中的值,Windows下不支持fork模式,启动进程快

    # 示例1:子进程中空列表追加的123不会影响主进程中的name
    import multiprocessing
    import time
    
    def task():
        print(name)
        name.append(123)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        name = []
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
        time.sleep(2)
        print(name)  # []
    
    # 示例2:子进程会拷贝主进程所有资源,子进程在拷贝时,列表中已经有数据了,所以print(name)输出[123]
    import multiprocessing
    import time
    def task():
        print(name) # [123]
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        name = []
        name.append(123)
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
    # 示例3:子进程在拷贝时,列表为空,所以输出还是空列表
    import multiprocessing
    import time
    def task():
        print(name)  # []
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        name = []
        # 创建子进程时,name=[],所以输出为一个空列表
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
        name.append(123)
    
  • spawn 模式 ,不会去拷贝主进程的值,但是需要手动去传一下必备的参数。支持Unix,win,Mac,启动进程速度慢

    import multiprocessing
    import time
    def task(data):
        print(data) # 拷贝的是主进程的,所有输出为 []
        data.append(999)
    
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")  # fork、spawn、forkserver
        name = []
    	# 创建子进程去执行task
        p1 = multiprocessing.Process(target=task, args=(name,))
        p1.start()
    
        time.sleep(2)
        print(name)  # []
    
    """
    结果为:
    []
    []
    """
    
  • forkserver 模式:和spawn模式一样通过run参数传必备参数;win不支持

    import multiprocessing
    import time
    
    def task(data):
        print(data)
        data.append(999)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("forkserver")  # fork、spawn、forkserver
        name = []
    
        p1 = multiprocessing.Process(target=task,args=(name,))
        p1.start()
    
        time.sleep(2)
        print(name)
    

fork 模式几乎可以拷贝父进程所有资源, 特殊对象(文件对象和锁),可以通过拷贝和传参去共享主进程的资源。而spawn 和 forkserver 不支持文件对象和线程锁等传参。

  • 案例1:

    # 特殊的对象可以直接拷贝
    import multiprocessing
    
    
    def task():
        print(file_object)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        
        name = []
        file_object = open('x1.txt', mode='a+', encoding='utf-8')
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
    
    # 特殊的对象还可以通过参数的方式去传递
    import multiprocessing
    
    def task(fb):
        print(fb)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        
        name = []
        file_object = open('x1.txt', mode='a+', encoding='utf-8')
    
        p1 = multiprocessing.Process(target=task,args=(file_object,))
        p1.start()
    

    锁对象也可以通过拷贝和传参的方式去共享主进程的资源

    #  拷贝
    import multiprocessing
    import threading
    
    def task(fb):
        print(fb,lock)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
    
        name = []
        file_object = open('x1.txt', mode='a+', encoding='utf-8')
        lock = threading.RLock()
    
        p1 = multiprocessing.Process(target=task, args=(file_object,))
        p1.start()
    
    # 传参
    
进程模式案例分析
import multiprocessing


def task():
    print(name)
    file_object.write("dave\n")
    file_object.flush()


if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    
    name = []
    file_object = open('x1.txt', mode='a+', encoding='utf-8')
    file_object.write("白居易\n") # 写到了内存中,还没写入到文件中
	
    # 主进程等待子进程执行完毕后,主进程终止时会把内存中的数据最终也会刷到硬盘中
    p1 = multiprocessing.Process(target=task)
    p1.start()

# 文件中内容为:
"""
白居易
dave
白居易
"""

例2:

import multiprocessing


def task():
    print(name)
    file_object.write("dave\n")
    file_object.flush()


if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    
    name = []
    file_object = open('x1.txt', mode='a+', encoding='utf-8')
    file_object.write("白居易\n")
    file_object.flush() # 刷新到硬盘中
# 主进程执行到这里时,已经把内容刷到硬盘中了,此时文件对象中是没有值的
    p1 = multiprocessing.Process(target=task)
    p1.start()

# 结果
"""
白居易
dave
"""

例3:

import multiprocessing
import threading
import time

def func():
    print("来了")
    with lock:
        print(666)
        time.sleep(1)

def task():
    # 拷贝的锁也是被申请走的状态
    # 被谁申请走了? 被子进程中的主线程申请走了
    for i in range(10):
        t = threading.Thread(target=func)
        t.start()
    time.sleep(2)
    lock.release()


if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    name = []
    lock = threading.RLock()
    lock.acquire()
    # print(lock)
    # lock.acquire() # 申请锁
    # print(lock)
    # lock.release()
    # print(lock)
    # lock.acquire()  # 申请锁
    # print(lock)

    p1 = multiprocessing.Process(target=task)
    p1.start()

1.2 常见功能

进程的常见方法:

  • p.start(),当前进程准备就绪,等待被CPU调度(工作单元其实是进程中的线程)。

  • p.join(),等待当前进程的任务执行完毕后再向下继续执行。

    import time
    import multiprocessing
    from multiprocessing import Process
    
    def task(arg):
        time.sleep(2)
        print("执行中...")
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")
        p = Process(target=task, args=('xxx',))
        p.start()
        p.join()
     
        print("继续执行...")
    
  • p.daemon = 布尔值,守护进程(必须放在start之前)

    • p.daemon =True,设置为守护进程,主进程执行完毕后,子进程也自动关闭。
    • p.daemon =False,设置为非守护进程,主进程等待子进程,子进程执行完毕后,主进程才结束。
    import time
    import multiprocessing
    from multiprocessing import Process
    
    
    def task(args):
        time.sleep(2)
        print("执行中...")
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")
        p = Process(target=task, args=('xxx',))
        p.daemon = True
        p.start()
    
        print("继续执行...")
    
  • 进程名称的设置,获取及获取进程pid

    import os
    import time
    import threading
    import multiprocessing
    
    
    def func():
        time.sleep(3)
    
    
    def task(arg):
        for i in range(10):
            t = threading.Thread(target=func)
            t.start()
        print(os.getpid(), os.getppid()) # ppid父进程
        print("线程个数", len(threading.enumerate())) # 获取进程个数
        time.sleep(2)
        print("当前进程的名称:", multiprocessing.current_process().name) 
    
    
    if __name__ == '__main__':
        print(os.getpid())
        multiprocessing.set_start_method("spawn")
        p = multiprocessing.Process(target=task, args=('xxx',))
        p.name = "哈哈哈哈"
        p.start()
    
        print("继续执行...")
    """
    扩展:
    threading.enumerate(): 返回一个包含正在运行的线程的list。
    
    正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    
    返回一个list类型,通过这个list类型我们就可以得知,目前线程数量的多少。
     """
    
  • 自定义进程类,直接将线程需要做的事写到run方法中。

    import multiprocessing
    
    
    class MyProcess(multiprocessing.Process):
        def run(self):
            print('执行此进程', self._args)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")
        p = MyProcess(args=('xxx',))
        p.start()
        print("继续执行...")
    
    
  • CPU个数,程序一般创建多少个进程?(利用CPU多核优势)。

    # 获取COU个数
    import multiprocessing
    multiprocessing.cpu_count()
    
    import multiprocessing
    
    if __name__ == '__main__':
        count = multiprocessing.cpu_count()
        for i in range(count - 1):
            p = multiprocessing.Process(target=xxxx)
            p.start()
    

2.进程间数据的共享

进程是资源分配的最小单元,每个进程中都维护自己独立的数据,默认不共享。

import multiprocessing


def task(data):
    data.append(666)


if __name__ == '__main__':
    data_list = []
    p = multiprocessing.Process(target=task, args=(data_list,))
    p.start()
    p.join()

    print("主进程:", data_list) # []

如果想要让他们之间进行共享,则可以借助一些特殊的东西来实现。

2.1 共享

Shared memory

Data can be stored in a shared memory map using Value or Array. For example, the following code

# 译文:数据可以使用[' Value ']或[' Array ']存储在共享内存映射中。例如,下面的代码
    'c': ctypes.c_char,  'u': ctypes.c_wchar,
    'b': ctypes.c_byte,  'B': ctypes.c_ubyte, # 代表 0-255区间的单个数
    'h': ctypes.c_short, 'H': ctypes.c_ushort,
    'i': ctypes.c_int,   'I': ctypes.c_uint,  (其u表示无符号)
    'l': ctypes.c_long,  'L': ctypes.c_ulong, 
    'f': ctypes.c_float, 'd': ctypes.c_double # float底层存储是32位,double是基于64位存储

short,int,long不同区间的数

案例1:

from multiprocessing import Process, Value, Array #  Array代表数组

# 子进程拿到主进程定义的数据后可以通过value来修改
def func(n, m1, m2):
    n.value = 888
    m1.value = 'a'.encode('utf-8')
    m2.value = "武"


if __name__ == '__main__':
    # i代表整型,初始化了一个值为666
    num = Value('i', 666)
    # c代表一个字符,单个符号
    v1 = Value('c')
    # u代表单个字符且可以为中文字符
    v2 = Value('u')
	
    # 定义好了当做参数传递给另外一个进程
    p = Process(target=func, args=(num, v1, v2))
    p.start()
    p.join()
    
# 即实现了进程之间的共享
    print(num.value)  # 888
    print(v1.value)  # a
    print(v2.value)  # 武

案例2:

from multiprocessing import Process, Value, Array


def f(data_array):
    # 在子进程中把数组的第一个元素换成666
    data_array[0] = 666


if __name__ == '__main__':
    arr = Array('i', [11, 22, 33, 44]) # 数组:元素类型必须是int;无法再放其他类型;且数据只能为4
# 把数组放到子进程中
    p = Process(target=f, args=(arr,))
    p.start()
    p.join()

    print(arr[:]) #  [666, 22, 33, 44]
# 也能实现数据的共享

Server process

A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

#译文:由' manager() '返回的管理器对象控制一个持有Python对象的服务器进程,并允许其他进程使用代理来操作它们。
from multiprocessing import Process, Manager # 基于Manger可以将列表,字典等数据实现进程直接的共享

def f(d, l):
    d[1] = '1'
    d['2'] = 2
    d[0.25] = None
    l.append(666)

if __name__ == '__main__':
    with Manager() as manager:
        # 定义了一个特殊的字典和列表,可以用于实现进程之间的共享
        d = manager.dict()
        l = manager.list()

        p = Process(target=f, args=(d, l))
        p.start()
        p.join()

        print(d)
        print(l)

2.2 交换

multiprocessing supports two types of communication channel between processes

# 译文:[' multiprocessing ']支持两种进程间的通信通道

Queues 队列

基于队列实现,可以将队列看成一个管道,左边一个进程,右边一个进程。这种结构对于队列而言,是先进先出

请添加图片描述

The Queue class is a near clone of queue.Queue. For example

# 译文:[' Queue ']类几乎是[' Queue .Queue ']的克隆。例如
import multiprocessing


def task(q):
    for i in range(10):
        q.put(i)


if __name__ == '__main__':
    # 主进程中创建了一个队列
    queue = multiprocessing.Queue()
    
    # 将队列放到子进程中去
    p = multiprocessing.Process(target=task, args=(queue,))
    p.start()
    p.join()

    print("主进程")
    # 通过.get去队列中取值,也可以通过put去子进程中去放数据
    print(queue.get())
    print(queue.get())
    print(queue.get())
    print(queue.get())
    print(queue.get())
# get 取数据 put 放数据

Pipes 管道

可以看是一个双向的管道结构,A和B都可以通过Pipes来取数据或发数据,A发送数据B能取到,反之依然。

请添加图片描述

The Pipe() function returns a pair of connection objects connected by a pipe which by default is duplex (two-way). For example:

# 译文:[' Pipe() ']函数返回一对由管道连接的连接对象,默认情况下是双工(双向)。例如:
import time
import multiprocessing


def task(conn):
    time.sleep(1)
    conn.send([111, 22, 33, 44])
    data = conn.recv() # 阻塞
    print("子进程接收:", data)
    time.sleep(2)


if __name__ == '__main__':
    # 创建一个multiprocessing.Pipe,可以将两个线程看成管道的两端
    parent_conn, child_conn = multiprocessing.Pipe()
	
    # 将child交给子进程
    p = multiprocessing.Process(target=task, args=(child_conn,))
    p.start()
    
    info = parent_conn.recv() # 阻塞
    print("主进程接收:", info)
    parent_conn.send(666)

上述都是Python内部提供的进程之间数据共享和交换的机制,作为了解即可,在项目开发中很少使用,后期项目中一般会借助第三方的来做资源的共享,例如:MySQL、redis等。

请添加图片描述

3. 进程锁

如果多个进程抢占式去做某些操作时候,为了防止操作出问题,可以通过进程锁来避免。

import time
from multiprocessing import Process, Value, Array


def func(n, ):
    n.value = n.value + 1


if __name__ == '__main__':
    num = Value('i', 0)

    for i in range(20):
        p = Process(target=func, args=(num,))
        p.start()

    time.sleep(3)
    print(num.value)
import time
from multiprocessing import Process, Manager


def f(d, ):
    d[1] += 1


if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
        d[1] = 0
        for i in range(20):
            p = Process(target=f, args=(d,))
            p.start()
        time.sleep(3)
        print(d)
import time
import multiprocessing


def task():
    # 假设文件中保存的内容就是一个值:10
    with open('f1.txt', mode='r', encoding='utf-8') as f:
        current_num = int(f.read())

    print("排队抢票了")
    time.sleep(1)

    current_num -= 1
    with open('f1.txt', mode='w', encoding='utf-8') as f:
        f.write(str(current_num))


if __name__ == '__main__':
    for i in range(20):
        p = multiprocessing.Process(target=task)
        p.start()

以上案例都是多进程在操作同一个数据就会出现数据混乱。多个进程之间有数据共享时会用到进程锁,如果用Pipes与Queue去做多进程数据共享时不会出现数据混乱,因为结构是排队一个一个取数据的。

很显然,多进程在操作时就会出问题,此时就需要锁来介入:

import time
import multiprocessing


def task(lock):
    print("开始")
    lock.acquire()
    # 假设文件中保存的内容就是一个值:10
    with open('f1.txt', mode='r', encoding='utf-8') as f:
        current_num = int(f.read())

    print("排队抢票了")
    time.sleep(0.5)
    current_num -= 1

    with open('f1.txt', mode='w', encoding='utf-8') as f:
        f.write(str(current_num))
    lock.release()


if __name__ == '__main__':
    multiprocessing.set_start_method("spawn")
    
    lock = multiprocessing.RLock() # 进程锁,支持spawn;线程锁不支持当做参数拷贝,而进程锁支持。
    
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(lock,))
        p.start()

    # spawn模式,需要特殊处理。
    time.sleep(7)


import time
import multiprocessing
import os


def task(lock):
    print("开始")
    lock.acquire()
    # 假设文件中保存的内容就是一个值:10
    with open('f1.txt', mode='r', encoding='utf-8') as f:
        current_num = int(f.read())

    print(os.getpid(), "排队抢票了")
    time.sleep(0.5)
    current_num -= 1

    with open('f1.txt', mode='w', encoding='utf-8') as f:
        f.write(str(current_num))
    lock.release()


if __name__ == '__main__':
    multiprocessing.set_start_method("spawn")
    lock = multiprocessing.RLock()

    process_list = []
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(lock,))
        p.start()
        process_list.append(p)

    # spawn模式,需要特殊处理。
    for item in process_list:
        item.join()
import time
import multiprocessing

def task(lock):
    print("开始")
    lock.acquire()
    # 假设文件中保存的内容就是一个值:10
    with open('f1.txt', mode='r', encoding='utf-8') as f:
        current_num = int(f.read())

    print("排队抢票了")
    time.sleep(1)
    current_num -= 1

    with open('f1.txt', mode='w', encoding='utf-8') as f:
        f.write(str(current_num))
    lock.release()


if __name__ == '__main__':
    multiprocessing.set_start_method('fork')
    lock = multiprocessing.RLock()
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(lock,))
        p.start()

4. 进程池

示例一:

import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def task(num):
    print("执行", num)
    time.sleep(2)


if __name__ == '__main__':
    # 修改模式
    pool = ProcessPoolExecutor(4) # 创建一个进程池,里面有四个进程
    for i in range(10):
        pool.submit(task, i) # 支持传参
	print(1)
	print(2)

示例二:等待进程池中的任务都执行完毕后,再继续往后执行。

import time
from concurrent.futures import ProcessPoolExecutor


def task(num):
    print("执行", num)
    time.sleep(2)


if __name__ == '__main__':

    pool = ProcessPoolExecutor(4)
    for i in range(10):
        pool.submit(task, i)
	# 等待进程池中的任务都执行完毕后,再继续往后执行。
    pool.shutdown(True)
    print(1)

示例三:当进程池中的某个进程执行完任务后再去执行别的任务。

import time
from concurrent.futures import ProcessPoolExecutor
import multiprocessing

def task(num):
    print("执行", num)
    time.sleep(2)
    return num

# 回调函数
def done(res):
    print(multiprocessing.current_process())
    time.sleep(1)
    print(res.result())
    time.sleep(1)


if __name__ == '__main__':

    pool = ProcessPoolExecutor(4)
    for i in range(50):
        fur = pool.submit(task, i) # 交给进程池
        fur.add_done_callback(done) # done的调用由主进程处理(与线程池不同,当任务执行完之后,在执行回调函数done是都是由子线程完成)
        
    print(multiprocessing.current_process())
    pool.shutdown(True)

注意:如果在进程池中要使用进程锁,则需要基于Manager中的Lock和RLock来实现。

import time
import multiprocessing
from concurrent.futures.process import ProcessPoolExecutor


def task(lock):
    print("开始")
    # lock.acquire()
    # lock.relase()
    with lock:
        # 假设文件中保存的内容就是一个值:10
        with open('f1.txt', mode='r', encoding='utf-8') as f:
            current_num = int(f.read())

        print("排队抢票了")
        time.sleep(1)
        current_num -= 1

        with open('f1.txt', mode='w', encoding='utf-8') as f:
            f.write(str(current_num))


if __name__ == '__main__':
    pool = ProcessPoolExecutor()
    # lock_object = multiprocessing.RLock() # 不能使用
    manager = multiprocessing.Manager()
    lock_object = manager.RLock() # Lock互斥锁,RLock递归锁
    for i in range(10):
        pool.submit(task, lock_object)

案例:计算每天用户访问情况。

请添加图片描述

  • 示例1

    import os
    import time
    from concurrent.futures import ProcessPoolExecutor
    from multiprocessing import Manager
    
    
    def task(file_name, count_dict):
        ip_set = set()
        total_count = 0
        ip_count = 0
        file_path = os.path.join("files", file_name)
        file_object = open(file_path, mode='r', encoding='utf-8')
        for line in file_object:
            if not line.strip():
                continue
            user_ip = line.split(" - -", maxsplit=1)[0].split(",")[0]
            total_count += 1
            if user_ip in ip_set:
                continue
            ip_count += 1
            ip_set.add(user_ip)
        count_dict[file_name] = {"total": total_count, 'ip': ip_count}
        time.sleep(1)
    
    
    def run():
        # 根据目录读取文件并初始化字典
        """
            1.读取目录下所有的文件,每个进程处理一个文件。
        """
    
        pool = ProcessPoolExecutor(4)
        with Manager() as manager:
            """
            count_dict={
            	"20210322.log":{"total":10000,'ip':800},
            }
            """
            count_dict = manager.dict()
            
            for file_name in os.listdir("files"):
                pool.submit(task, file_name, count_dict)
                
                
            pool.shutdown(True)
            for k, v in count_dict.items():
                print(k, v)
    
    
    if __name__ == '__main__':
        run()
    
  • 示例2

    import os
    import time
    from concurrent.futures import ProcessPoolExecutor
    
    
    def task(file_name):
        ip_set = set()
        total_count = 0
        ip_count = 0
        file_path = os.path.join("files", file_name)
        file_object = open(file_path, mode='r', encoding='utf-8')
        for line in file_object:
            if not line.strip():
                continue
            user_ip = line.split(" - -", maxsplit=1)[0].split(",")[0]
            total_count += 1
            if user_ip in ip_set:
                continue
            ip_count += 1
            ip_set.add(user_ip)
        time.sleep(1)
        return {"total": total_count, 'ip': ip_count}
    
    
    def outer(info, file_name):
        def done(res, *args, **kwargs):
            info[file_name] = res.result()
    
        return done
    
    
    def run():
        # 根据目录读取文件并初始化字典
        """
            1.读取目录下所有的文件,每个进程处理一个文件。
        """
        info = {}
        
        pool = ProcessPoolExecutor(4)
    
        for file_name in os.listdir("files"):
            fur = pool.submit(task, file_name)
            fur.add_done_callback(  outer(info, file_name)  ) # 回调函数:主进程
    
        pool.shutdown(True)
        for k, v in info.items():
            print(k, v)
    
    
    if __name__ == '__main__':
        run()
    
    

5. 协程

暂时以了解为主。

计算机中提供了:线程、进程 用于实现并发编程(真实存在)。

协程(Coroutine),是程序员通过代码写出来的一个东西(非真实存在)。

协程也可以被称为微线程,是一种用户态内的上下文切换技术。
简而言之,其实就是通过一个线程实现代码块相互切换执行(来回跳着执行)。

例如:

def func1():
    print(1)
    ...
    print(2)
    
def func2():
    print(3)
    ...
    print(4)
    
func1()
func2()

上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4

但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4

在Python中有多种方式可以实现协程,例如:

  • greenlet

    pip install greenlet
    
    from greenlet import greenlet
    
    def func1():
        print(1)        # 第1步:输出 1
        gr2.switch()    # 第3步:切换到 func2 函数
        print(2)        # 第6步:输出 2
        gr2.switch()    # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行
        
    def func2():
        print(3)        # 第4步:输出 3
        gr1.switch()    # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
        print(4)        # 第8步:输出 4
        
    gr1 = greenlet(func1)
    gr2 = greenlet(func2)
    
    gr1.switch() # 第1步:去执行 func1 函数
    
  • yield

    def func1():
        yield 1
        yield from func2()
        yield 2
        
    def func2():
        yield 3
        yield 4
        
    f1 = func1()
    for item in f1:
        print(item)
    

虽然上述两种都实现了协程,但这种编写代码的方式没啥意义。

这种来回切换执行,可能反倒让程序的执行速度更慢了(相比较于串行)。

协程如何才能更有意义呢?

不要让用户手动去切换,而是遇到IO操作时能自动切换。

Python在3.4之后推出了asyncio模块 + Python3.5推出async;async语法 ,内部基于协程并且遇到IO请求自动化切换。

import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
    
async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
    
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

爬虫示例:

"""
需要先安装:pip3 install aiohttp
"""

import aiohttp
import asyncio

async def fetch(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        content = await response.content.read()
        file_name = url.rsplit('_')[-1]
        with open(file_name, mode='wb') as file_object:
            file_object.write(content)
            
async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
            'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
            'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
        ]
        tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
        await asyncio.wait(tasks)
if __name__ == '__main__':
    asyncio.run(main())

# 当第一章图片下载请求等待时,会自动切换到下载第二张,第二张下载等待时会自动切换到下载第三张并会自动切换到有响应的下载进程。

通过上述内容发现,在处理IO请求时,协程通过一个线程就可以实现并发的操作。

协程、线程、进程的区别?

线程,是计算机中可以被cpu调度的最小单元。[cpu中工作的最小单元]
进程,是计算机资源分配的最小单元(进程为线程提供资源)。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

由于CPython中GIL的存在:
	- 线程,适用于IO密集型操作。
    - 进程,适用于计算密集型操作。

协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。

#协程不适合做计算和处理,只有一个线程效率较低,一般只让协程去发网络io请求,拿到数据后将数据扔到数据库,文件或队列中,再让其他的程序去处理,这时协程的优势便体现出来了。


所以,在处理IO操作时,协程比线程更加节省开销(协程的开发难度大一些)。

现在很多Python中的框架都在支持协程,比如:FastAPI、Tornado、Sanic、Django 3、aiohttp等,企业开发使用的也越来越多(目前不是特别多)。

有兴趣想要研究的同学可以参考如下的文章和专题视频:

  • 文章

    https://pythonav.com/wiki/detail/6/91/
    https://zhuanlan.zhihu.com/p/137057192
    
  • 视频

    https://www.bilibili.com/video/BV1NA411g7yf
    

总结

  1. 了解的进程的几种模式
  2. 掌握进程和进程池的常见操作
  3. 进程之间数据共享
  4. 进程锁
  5. 协程、进程、线程的区别(概念)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值