Python 进程

多进程

因为线程无法调用多核,其相当于多个线程在单核下来回切换,所以有时候为了实现多核应用,比如线程不适合执行的计算操作密集的任务,就可以通过多进程来提高效率。当然要注意的是,虽然线程间可以互相通信,但是因为进程间不能互相访问,所以不同进程的线程间也是无法互相通信的,能通信的只有在同一个进程下的多个线程

创建进程

进程的语法几乎完全兼容线程语法,就修改了一些小地方,比如创建进程时是通过Process来创建,其他大部分操作都是相似的,还有就是在windows下创建进程的代码必须在if __name__ == '__main__':里编写,举例

import multiprocessing 
import time

def run():
   while True:
      print("This is process")
      time.sleep(2)

if __name__ == '__main__':
   multiprocessing.freeze_support()
   # 防止多进程启动报错
   t = multiprocessing.Process(target=run)    # 就这里有修改
   t.start()    # 新进程
创建进程机制
windows创建机制

子进程开辟新的内存后,内存里没有之前的数据,为了能够执行内容,会将父进程的文件重新import一遍,此时就有需要的内容了。但是在import的时候,如果父进程文件里的开启新进程代码没有在__main__下,那么就会又执行一遍,这样就会导致无限创建进程,所以windows下需要在if __name__ == "__main__"下使用

linux创建机制

直接把父进程的内存空间(值一样,但是地址是不一样的)复制一份,底层通过os.fork()完成,因此不需要将创建进程的代码放在__main__

linux下创建多进程示例
import os
import time
pid = os.fork()
# os.fork只能linux运行
print("create...", pid, ", parent:", os.getpid())
time.sleep(2)

# create... 93762 , parent: 93761
# create... 0 , parent: 93762

结果可以看出主进程和新创建的进程都执行了os.fork()之后的语句,原因是子进程会从创建进程的地方开始,将和主进程一样的数据、代码运行都拷贝一份并执行,然后这些拷贝的内容和主进程隔离开,所以os.fork()后面的内容会执行两次

区分父/子进程

os.fork()会返回创建的子进程的pid,因为子进程没有创建子进程,所以os.fork()的返回值为0,因此我们可以通过该方式来对父进程和子进程进行判断,举例:

import os
import time
print("main:", os.getpid())
pid = os.fork()
if pid == 0:
    print("child process:", os.getpid())
else:
    print("parent process", os.getpid(), "child process:", pid)
time.sleep(2)

# main: 93961
# parent process 93961 child process: 93962
# child process: 93962
进程资源回收

前面的示例中,都在最后加上了sleep语句,使得主进程尽量在子进程执行完毕后才结束,如果将主进程的sleep删掉,使得主进程提前结束,将可能会导致子进程的资源回收出现问题,举例:

import os
import time
print("main:", os.getpid())
pid = os.fork()
if pid == 0:
    print("child process:", os.getpid())
    time.sleep(1)
    print("child process end...")
else:
    print("parent process", os.getpid(), "child process:", pid)
    print("main end...")

# main: 93987
# parent process 93987 child process: 93988
# child process: 93988
# main end...
# root@ubuntu:~$ child process end...

结果会发现子进程没有正常结束退出,这是因为子进程需要父进程来回收资源,但这里父进程在子进程执行完之前就结束了,从而导致子进程资源无法被回收

多线程/多进程对比

CPU密集的操作,用多进程,因为能够真正使用到多个CPU;而IO密集的操作,使用多线程,因为CPU有很大一部分时间处于空闲,无须用到多核,而且进程的切换代价大于线程,因此此时多线程的结果甚至可能优于多进程

  • CPU密集操作示例:
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, wait

def fab(n):
    if n < 2:
        return 1
    return fab(n - 1) + fab(n - 2)

if __name__ == '__main__':
    start = time.time()
    [fab(i) for i in range(25, 34)]
    print("单线程耗时:{}".format(time.time() - start))
    thread_pool = ThreadPoolExecutor()
    start = time.time()
    fabs = [thread_pool.submit(fab, i) for i in range(25, 34)]
    wait(fabs)
    print("多线程耗时:{}".format(time.time() - start))
    process_pool = ProcessPoolExecutor()
    start = time.time()
    fabs = [process_pool.submit(fab, i) for i in range(25, 34)]
    wait(fabs)
    print("多进程耗时:{}".format(time.time() - start))

# 单线程耗时:4.424163579940796
# 线程耗时:4.396239280700684
# 进程耗时:2.3158042430877686

可以看出因为多进程使用到了更多的CPU参与运算,因此大大缩短了运行时间

  • IO密集操作示例:
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, wait

def iotask():
    time.sleep(1)

if __name__ == '__main__':
    start = time.time()
    [iotask() for i in range(5)]
    print("单线程耗时:{}".format(time.time() - start))
    thread_pool = ThreadPoolExecutor()
    start = time.time()
    iotasks = [thread_pool.submit(iotask) for i in range(5)]
    wait(iotasks)
    print("多线程耗时:{}".format(time.time() - start))
    process_pool = ProcessPoolExecutor()
    start = time.time()
    iotasks = [process_pool.submit(iotask) for i in range(5)]
    wait(iotasks)
    print("多进程耗时:{}".format(time.time() - start))

# 单线程耗时:5.00162410736084
# 多线程耗时:1.0023696422576904
# 多进程耗时:2.124408006668091

可以看出由于IO密集型操作下,CPU利用率不高,而进程的切换开销大,因此结果还不如多线程好

多线程/多进程运行区别
  • 多线程就是在同一进程下新开一个线程,数据共享
  • 多进程是新开一个不同于主进程的进程,数据不共享
相关API
getpid()/getppid()

获取进程ID和父进程ID,举例:

import os

print(os.getpid(), os.getppid())
# 获取进程ID、父进程ID

# 12440 11440
is_alive()

进程是否启动

terminate()

强制关闭进程

pid

进程id,需要在启动后才有,举例:

import time
import multiprocessing
def task():
    time.sleep(1)

if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task)
    print(p1.pid)
    p1.start()
    print(p1.pid)

# None
# 72236
ident

进程唯一标识

name

进程名

API示例
from multiprocessing import Process
import time

def fun():
    print("process start...")
    time.sleep(2)

if __name__ == "__main__":
    process = Process(target=fun)
    print(process.is_alive())
    # 进程是否启动
    process.start()
    print(process.pid, process.ident, process.name)
    # 进程id、进程唯一标识、进程名
    print(process.is_alive())
    time.sleep(0.1)
    process.terminate()
    # 强制关闭进程,需要等待一点时间
    print(process.is_alive())
    time.sleep(0.1)
    print(process.is_alive())

# False
# 12396 12396 Process-1
# True
# True
# False

因为启动的进程在另一个程序中,因此fun输出的内容当前终端无法看到

API应用示例

启动多个任务并监听,若任务中止,则重启任务,举例:

import time
import multiprocessing

def task1():
    print("start1")
    time.sleep(3)
    raise
    print("end1")

def task2():
    print("start2")
    time.sleep(5)
    print("end2")

def task3():
    print("start3")
    time.sleep(6)
    print("end3")

def task4():
    print("aaa")
    time.sleep(1000)

# 任务列表
tasks = {
    "task1": task1, "task2": task2, "task3":task3, "aaa": task4
}

# 监听任务列表
watch_tasks = {task: None for task in tasks}

def task_deco(task_name, task, *args, **kwargs):
    """监听装饰器
    """
    print(f"任务{task_name}启动...")
    try:
        task(*args, **kwargs)
    except Exception as e:
        print(f"任务{task_name}异常中止,中止原因:{e}")
    finally:
        print(f"任务{task_name}结束,准备重启...")

def start_process(task_name, task):
    """进程启动"""
    start = time.time()
    p = multiprocessing.Process(target=task_deco, args=(task_name, task))
    p.start()
    return p

def is_process(p):
    """进程判断
    """
    return hasattr(p, "is_alive") and hasattr(p, "terminate")

def handle_watch(p, task_name, task):
    """监听处理
    """
    if not p.is_alive():
        p = start_process(task_name, task)
        watch_tasks[task_name] = p

def handle_stop():
    """脚本停止处理
    """
    for p in watch_tasks.values():
        if is_process(p):
            p.terminate()

def loop():
    """监听循环
    """
    while True:
        time.sleep(1)
        for task, p in watch_tasks.items():
            if p is None:
                continue
            if is_process(p):
                handle_watch(p, task, tasks[task])

def init():
    """初始化启动所有任务进程
    """
    for task in tasks:
        p = start_process(task, tasks[task])
        watch_tasks[task] = p

def main():
    try:
        init()
        loop()
    except KeyboardInterrupt:
        handle_stop()

if __name__ == '__main__':
    main()

守护进程和守护线程

守护线程
  • 主线程会等待子线程结束后才会结束,而守护线程随着主线程的结束而结束
  • 主线程一旦结束了进程就会结束,从而回收所有的进程资源(线程也是资源)
  • 如果主线程结束后还有其他子线程在运行,则守护线程也守护
守护进程
  • 守护进程会随着主进程的结束而结束
  • 如果主进程结束后还有其他子进程在运行,则守护进程不守护

原因:

  • 守护进程需要主进程来回收资源
  • 守护线程是随着进程的结束而结束
进程结束步骤

其他子线程结束 -> 主线程结束 -> 主进程结束 -> 回收整个进程中的资源包括守护线程

相关知识点
  • 进程是资源分配单位
  • 子进程都需要父进程来回收资源
  • 线程是进程中的资源
  • 所有的线程都会随着进程的结束而被回收资源

进程通信

进程队列

由于子进程在创建以后,其和父进程的内存是独立分开的,因此父进程的数据是无法和子进程进行共享的,举例:

import time
import multiprocessing
def task(li):
    li.append(1)

if __name__ == '__main__':
    li = []
    multiprocessing.Process(target=task, args=(li, )).start()
    time.sleep(1)
    print(li)

# []

可以看到列表li并没有被共享使用,但multiprocessing提供了一个Queue,其支持进程间的通信,举例:

import time
import multiprocessing
def task(queue):
    queue.put(1)

if __name__ == '__main__':
    queue = multiprocessing.Queue()
    multiprocessing.Process(target=task, args=(queue, )).start()
    time.sleep(1)
    print(queue.get())

# 1

注:
要注意的是继承于multiprocessing中的Queue才可以传入子进程,如果是继承于queueQueue是不能传进去的,比如下面这样虽然看起来看起来二和前面的原理差不多,但因为队列的本质不同,举例:

import multiprocessing
import queue

def run(q):
   print(q.get())

if __name__ == '__main__':
   q = queue.Queue()
   q.put("I'm in main process&thread")
   t2 = multiprocessing.Process(target=run, args=(q,))
   t2.start()
管道

multiprocessing下提供了Pipe对象,其返回管道的两端,而得到其中一端的进程允许与管道另一端的进程间互相通信,举例:

import time
import multiprocessing
def task1(pipe):
    pipe.send("aaa")
    print(pipe.recv())

def task2(pipe):
    print(pipe.recv())
    pipe.send("bbb")

if __name__ == '__main__':
    p1, p2 = multiprocessing.Pipe()
    # 新建个管道对象,返回管道两端,实际都一样,传入哪一端,就用另一端与之通信即可
    multiprocessing.Process(target=task1, args=(p1, )).start()
    multiprocessing.Process(target=task2, args=(p2, )).start()

# aaa
# bbb

Pipe虽然只能允许两个进程间互相通信,但其有性能高的优势(Queue虽然能允许多个进程通信,但由于内部加了很多锁来控制,因此效率相对较低)

进程共享

前面的队列和管道还无法做到真正的进程之间共享数据,所以像如果想多进程共同修改同一份数据还是做不到的,这里就有一个Manager()对象可以实现,其支持listdictLockRLockEventQueueArray等内容的共享,即在新建一个Manager对象后,调用该对象的上面那些数据结构,然后跟前面的队列和管道一样传入子进程就可以了

示例

(实现多进程间共同修改同一个列表和字典)

import multiprocessing
import os

def run(l, d, i):
   l.append(os.getpid())        #各个子进程将自己进程id添加入列表
   d[i] = os.getpid()           #...添加入字典
   # print(l)
   # print(d)

if __name__ == '__main__':
   manager = multiprocessing.Manager()      #新建一个Manager对象
   share_list = manager.list(["id"])            #生成可进程间共享的列表
   share_dict = manager.dict()              #...的字典
   control_list = []    #存放进程组
   for each in range(5):
      t = multiprocessing.Process(target=run, args=(share_list, share_dict, each))
      #将可共享的数据传入子进程
      t.start()
      control_list.append(t)
   for each in control_list:
      each.join()       #各进程不能同时修改,所以需等待前个进程完成操作
   print(share_list)
   print(share_dict)

结果:
['id', 8716, 8468, 5472, 6452, 6032]
{0: 5472, 1: 8716, 2: 8468, 3: 6452, 4: 6032}

进程锁

照理来说,进程互相独立,是不会影响到对方的,但是像有些地方却还是共用的,比如屏幕输出,如果不设置锁的话,那么可能第一个用print还没输出完就到下一个输出,结果几个进程的输出夹杂在一起,所以就需要进程锁来控制。进程锁和线程锁几乎一样,不过是继承于multiprocessing的,然后也是用Lock()生成,用acquire()release()来实现,但是导入的时候要from multiprocessing import Lock,而且锁要在程序最开始就定义好,即在函数之前就要定义好,举例:

import multiprocessing
from multiprocessing import Lock        #另外导入锁

lock = Lock()   #先生成锁

def abc(i):
   lock.acquire()
   print(i)
   lock.release()

if __name__ == '__main__':
   t_s = [multiprocessing.Process(target=abc, args=(i,)) for i in range(100)]
   #链表推导式创建包含一堆进程列表
   for t in t_s:
      t.start() #依次执行进程

进程池

因为进程的资源消耗较大,所以可以通过进程池来控制一次可以开启的进程的次数(和线程里的信号量相似),使用进程池可以使用multiprocessing.Pool对象或者concurrent.futures下的ProcessPoolExecutor对象

ProcessPoolExecutor

和线程池的用法几乎一致

Pool

通过Pool()来生成一个进程池,此时创建进程则通过Pool提供的apply或者apply_async方法创建,前者代表同步执行(即串行运行),后者代表异步执行(并发执行),里面有两个常用参数:funcargs对应Processtargetargs,通过close关闭,join等待(记住这里是先closejoin),而且在异步执行时必须要有closejoin,否则程序会直接关闭,举例:

import time
import multiprocessing
def task(n):
    time.sleep(1)
    return n

if __name__ == '__main__':
    pool = multiprocessing.Pool(multiprocessing.cpu_count())
    # 进程数与当前设备的CPU数相等更能充分利用CPU
    res = pool.apply_async(task, args=(1,))
    pool.close()
    # join之前需要close,从而不再接收新的任务
    pool.join()
    print(res.get())
同步进程示例

(串行执行进程池)

import multiprocessing
import os
import time

def run(n):
   print("the message is %d\t" % n)
   time.sleep(1)
   print("%d is end" % n)

if __name__ == '__main__':  #别忘了进程必须有这个
   pool = multiprocessing.Pool(5)  #运行同时存在5个进程
   for each in range(10):
      pool.apply(func=run, args=(each,))        #串行执行进程池
      #pool.apply_async(func=run, args=(each,)) #异步执行
   pool.close()     #异步时必须有close和join,并且close要在join前
   pool.join()

apply_async里还有一个回调参数callback,意思是当进程运行结束后会将返回值返回给回调参数里的函数,然后执行该函数,但这个函数不是在前面的子进程里执行的,而是在父进程里执行的

异步进程示例

(异步执行进程,并执行回调函数)

def run(n):
   print("the message is %d\t" % n)
   time.sleep(1)
   print("%d is end" % n)
   return os.getpid()       #将进程号返回给回调函数

def run_callback(arg):  #参数是进程里返回的
   print("%s I'm the callback, my process is %s" % (arg, os.getpid()))
   #会发现回调函数是在父进程执行的

if __name__ == '__main__':  #别忘了进程必须有这个
   pool = multiprocessing.Pool(5)  #运行同时存在5个进程
   for each in range(10): 
      pool.apply_async(func=run, args=(each,), callback=run_callback)  #回调函数run_callback
   pool.close()
   pool.join()
相关API
  • map:迭代创建进程,其使用方法十分简单,首先用Pool()方法创建一个进程池,然后用map()方法传入函数和迭代器,从而迭代创建执行函数的进程,举例:
import multiprocessing
import time
import os

def main(i):
    time.sleep(1)
    print(i, os.getpid())

if __name__ == "__main__":
    pool = multiprocessing.Pool(5)
    pool.map(main, [i for i in range(10)])

# 1 37908
# 6 37908
# 0 37228
# 5 37228
# 2 40604
# 7 40604
# 3 39996
# 8 39996
# 4 40688
# 9 40688
监听进程完成

可以使用Pool实例对象下的imap方法,和线程池的map方法差不多,举例:

import time
import multiprocessing
def task(n):
    time.sleep(n % 2)
    return n

if __name__ == '__main__':
    pool = multiprocessing.Pool(multiprocessing.cpu_count())
    for res in pool.imap(task, [i for i in range(5)]):
        print(res)

# 0
# 1
# 2
# 3
# 4

也可以使用imap_unordered方法,类似于as_completed,谁先完成谁返回,举例:

import time
import multiprocessing
def task(n):
    time.sleep(n % 2)
    return n

if __name__ == '__main__':
    pool = multiprocessing.Pool(multiprocessing.cpu_count())
    for res in pool.imap_unordered(task, [i for i in range(5)]):
        print(res)

# 0
# 2
# 4
# 1
# 3
进程池通信

而对于进程池Pool,即使使用multiprocessing里的Queue也无法互相进行通信,此时需要使用multiprocessing.Manager对象下的Queue,举例:

import time
import multiprocessing
def task(queue):
    queue.put(1)

if __name__ == '__main__':
    queue = multiprocessing.Manager().Queue()
    # 使用Manager对象下的Queue
    pool = multiprocessing.Pool(multiprocessing.cpu_count())
    pool.apply(task, (queue, ))
    time.sleep(1)
    print(queue.get())

# 1

你可能感兴趣的:(Python 进程)