python的多线程多进程与多协程

前言:
进程是资源单位,线程是执行单位,协程是线程中一个由代码切换的子任务,也叫微线程,
一个进程占用一片内存区域,自成一体,进程与进程之间变量等信息互相隔离,但是进程只是资源单位(个人理解),不能执行任务,及具体执行任务还是需要线程执行,所以一个进程中至少有一个或多个线程运行,同一个进程中的所有线程共用此进程中的内存资源,变量等信息互通。不同的进程,线程任务的切换都是依靠操作系统来进行切换执行,而协程则是依靠程序代码自己来进行切换执行的,协程会模拟操作系统的切换规则(即遇到io操作即切换)来自动切换任务,切换逻辑类似迭代器生成器,因为协程任务切换不依靠操作系统所以协程比进程线程消耗的计算机资源少的多

多进程

说明

python的多进程可以实现多个程序的并行,充分利用计算机的资源
Python中的多进程是通过multiprocessing包来实现的,和多线程的threading.Thread差不多,它可以利用multiprocessing.Process对象来创建一个进程对象。这个进程对象的方法和线程对象的方法差不多也有start(), run(), join()等方法,其中有一个方法不同Thread线程对象中的守护线程方法是setDeamon,而Process进程对象的守护进程是通过设置daemon属性来完成的

进程之间是相互独立的,每启动一个新的进程相当于把数据进行了一次克隆,子进程里的数据修改无法影响到主进程中的数据,不同子进程之间的数据也不能共享,这是多进程在使用中与多线程最明显的区别
多进程的实现
相关拓展接口
from multiprocessing import current_process
import os
获取当前进程的pid(可在主进程或者子进程中使用)
print(current_process().pid)
获取当前进程的pid
print(os.getpid())
获取当前进程父进程的的pid
print(os.getppid())

p = Process(target=child_main)
p.start()
杀死自己进程(自杀)本质是先向操作系统请示让操作系统杀死自己,所以有短暂延迟进程才会挂掉
p.terminate()
判断这个进程是否存在
p.is_alive()
对于某个子进程对象来说,如果要把它设置为主进程的守护进程,则在其start()之前设置其daemon 属性为True即可,默认为False, p.daemon = True
p.daemon = True
进程锁由于进程数据不互通,所以可在最开始创建锁对象,然后在进程的参数中传递进去,每次抢锁,完毕后解锁即可,这样可以保护一些数据的安全性,防止数据混乱
from multiprocessing import Lock
lock_obj = Lock()
抢锁
lock_obj.acquire()
释放锁
lock_obj.release()
进程执行相关流程梳理
1.正常情况下(子进程直接调用start()接口然后执行主进程代码),在这种情况下子进程在start()调用之后立马开始运行,第一种情况如果子进程先执行完,会立即死亡,然后主进程结束的时候会收回子进程占用的资源,然后主进程也死亡,第二种情况如果主进程先执行完毕不会立即死亡,而会等待所有子进程执行完毕后收回相关子进程占用的相关资源,然后在结束死亡,在此过程中主进程的代码逻辑虽然执行完毕了,但是主进程并没有死亡
2.(子进程直接调用start()接口,和join()接口,然后执行主进程代码),在这种情况下子进程在start()调用之后立马开始运行,主进程也开始执行,主进程执行到join()接口的时候,会在这里阻塞,等待调用join()接口的相关子进程执行完毕立即死亡后才会继续往下执行,主进程执行完毕后回收子进程的相关资源之后也死亡
3.守护进程情况(子进程执行p.daemon = True后调用start()接口,然后执行主进程代码),在这种情况下子进程在start()调用之后立马开始运行,主进程也开始执行,第一种情况如果主进程先于子进程之前执行完,则子进程不管执行完毕与否,立马死亡,然后主进程收回子进程的占用资源也死亡结束,第二种情况就是,子进程先结束,那么他会立即死亡,等主进程也结束的时候,回收他的占用资源后主进程也立即死亡,可以把Python的守护进程想象成帝王和妃子,帝王是主进程,妃子是子进程,帝王先死的话,妃子也会在帝王死的时候陪葬,而妃子先死的话,帝王活的好好的
总结:收尸原则,主进程会替它创建的所有非守护的子进程收尸,也就是纵然有时候主进程会先执行完毕,但是他不会立即死亡,他会等所有非守护进程执行完毕,给他们收完尸之后再死亡,而当有守护进程时,守护进程会在主进程执行完毕代码(而不是死亡)的时候就立即死亡。

方法1.函数传参实现多进程

from multiprocessing import Process
import os


def child_main(name):
    print('I am', name, 'process id:', os.getpid())
    print('parent process:', os.getppid())


if __name__ == '__main__':
    print('main process id:', os.getpid())
    p_list = []
    for i in range(3):
        # target进程函数名, args参数进程函数的需要的参数
        p = Process(target=child_main, args=('bob--{}'.format(i),))
        p_list.append(p)

	# 对于某个子进程对象来说,如果要把它设置为主进程的守护进程,则在其start()之前设置其daemon 属性为True即可,默认为False, p.daemon = True
	# p.daemon = True
    # 开启子进程
    temp = [p.start() for p in p_list]

    # join()接口的作用是,调用后会等待上面start运行起来的子进程执行完毕再执行join()这行代码之后主进程下面的代码
    temp = [p.join() for p in p_list]


    # main process id: 9320
    # I am bob--0 process id: 12732
    # parent process: 9320
    # I am bob--1 process id: 3592
    # parent process: 9320
    # I am bob--2 process id: 4624
    # parent process: 9320

	# Process函数的参数
    # group 应该始终是 None ;它仅用于兼容性考虑
    # target 传入一个可调用对象。它默认为 None,这里传入的是子进程的运行函数。
    # name 是进程名称,仅仅具有标识作用,并不会改变操作系统中的进程名称。
    # args 是目标调用的参数元组,也就是target调用函数的参数
    # kwargs 是目标调用的关键字参数字典,也是target调用函数的参数
    # daemon 将进程 daemon 标志设置为True或False 如果是True则标记为守护进程,当主进程执行完毕时此子进程也立马终结

方法2.继承方式创建多进程

from multiprocessing import  Process

class MyProcess(Process): #继承Process类
    def __init__(self,name):
        super(MyProcess,self).__init__()
        self.name = name

    def run(self):
        print('测试%s多进程' % self.name)

if __name__ == '__main__':
    process_list = []
    for i in range(5):  #开启5个子进程执行fun1函数
        p = MyProcess('Python-子进程{}'.format(i)) #实例化进程对象
        	# 对于某个子进程对象来说,如果要把它设置为主进程的守护进程,则在其start()之前设置其daemon 属性为True即可,默认为False, p.daemon = True
	# p.daemon = True
        p.start()
        process_list.append(p)

    for i in process_list:
    # join()接口的作用是,调用后会等待上面start运行起来的子进程执行完毕再执行join()这行代码之后主进程下面的代码
        p.join()

    print('结束测试')

    # 测试Python - 子进程0多进程
    # 测试Python - 子进程1多进程
    # 测试Python - 子进程2多进程
    # 测试Python - 子进程3多进程
    # 测试Python - 子进程4多进程
    # 结束测试

Queue队列主进程获取子进程的数据(通信)

# 主进程获取子进程的数据
from multiprocessing import Process, Queue

def fun1(q,i):
    print('子进程%s 开始put数据' %i)
    # put()给队列放对象,先进先出
    q.put('我是%s 通过Queue通信' %i)

if __name__ == '__main__':
	# 创建进程队列,先进先出
    q = Queue()

    process_list = []
    for i in range(3):
        p = Process(target=fun1,args=(q,i,))  #注意args里面要把q对象传给我们要执行的方法,这样子进程才能和主进程用Queue来通信
        p.start()
        process_list.append(p)

    for i in process_list:
        p.join()

    print('主进程获取Queue数据')
    # get()拿队列中的对象,先进先出
    print(q.get())
    print(q.get())
    print(q.get())
    print('结束测试')
from multiprocessing import Process, Queue


def fun1(q,i):
    print('子进程%s 开始put数据' %i)
    q.put('我是%s 通过Queue通信' %i)

if __name__ == '__main__':
	# 创建进程队列,先进先出
    q = Queue()

    process_list = []
    for i in range(3):
        p = Process(target=fun1,args=(q,i,))  #注意args里面要把q对象传给我们要执行的方法,这样子进程才能和主进程用Queue来通信
        p.start()
        process_list.append(p)

    for i in process_list:
        p.join()

    print('主进程获取Queue数据')
    print(q.get())
    print(q.get())
    print(q.get())
    print('结束测试')

    # 子进程0
    # 开始put数据
    # 子进程1
    # 开始put数据
    # 子进程2
    # 开始put数据
    # 主进程获取Queue数据
    # 我是0
    # 通过Queue通信
    # 我是1
    # 通过Queue通信
    # 我是2
    # 通过Queue通信
    # 结束测试
    # 子进程0
    # 开始put数据
    # 子进程1
    # 开始put数据
    # 子进程2
    # 开始put数据
    # 主进程获取Queue数据
    # 我是0
    # 通过Queue通信
    # 我是1
    # 通过Queue通信
    # 我是2
    # 通过Queue通信
    # 结束测试

管道互发消息通信

from multiprocessing import Process, Pipe
def fun1(conn):
    print('子进程发送消息:')
    conn.send('你好主进程')
    print('子进程接受消息:')
    print(conn.recv())
    conn.close()

if __name__ == '__main__':
    conn1, conn2 = Pipe() #关键点,pipe实例化生成一个双向管
    p = Process(target=fun1, args=(conn2,)) #conn2传给子进程
    p.start()
    print('主进程接受消息:')
    print(conn1.recv())
    print('主进程发送消息:')
    conn1.send("你好子进程")
    p.join()
    print('结束测试')

    # 主进程接受消息:
    # 子进程发送消息:
    # 子进程接受消息:
    # 你好主进程
    # 主进程发送消息:
    # 你好子进程
    # 结束测试

Manager实现主进程子进程实现数据共享(修改同一份数据)


### 主进程子进程实现数据共享
from multiprocessing import Process, Manager

def fun1(dic,lis,index):

    dic[index] = 'a'
    dic['2'] = 'b'
    lis.append(index)    #[0,1,2,3,4,0,1,2,3,4,5,6,7,8,9]
    #print(l)

if __name__ == '__main__':
    with Manager() as manager:
        dic = manager.dict()#注意字典的声明方式,不能直接通过{}来定义
        l = manager.list(range(5))#[0,1,2,3,4]

        process_list = []
        for i in range(10):
            p = Process(target=fun1, args=(dic,l,i))
            p.start()
            process_list.append(p)

        for res in process_list:
            res.join()
        print(dic)
        print(l)

        # {1: 'a', '2': 'b', 0: 'a', 2: 'a', 3: 'a', 4: 'a', 5: 'a', 7: 'a', 6: 'a', 8: 'a', 9: 'a'}
        # [0, 1, 2, 3, 4, 1, 0, 2, 3, 4, 5, 7, 6, 8, 9]

进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。就是固定有几个进程可以使用。
from  multiprocessing import Process, Pool
import os, time, random

def fun1(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    pool = Pool(5) #创建一个5个进程的进程池

    for i in range(10):
    	# 非阻塞式调用,调用apply_async接口后直接执行里面的函数,且同时代码往下走,apply_async()一般配合close(),join()接口使用
        pool.apply_async(func=fun1, args=(i,))
        # 阻塞式接口,调用apply()后代码不会往下走,只会在进程池和池子外面的所有任务都执行完毕后往下走执行主进程后面的代码
        # pool.apply(func=fun1, args=(i,))
        
	# 锁定进程池
    pool.close()
    
    # 等待所有子进程执行完毕
    pool.join()
    print('结束测试')


# Run task 0 (37476)...
# Run task 1 (4044)...
# Task 0 runs 0.03 seconds.
# Run task 2 (37476)...
# Run task 3 (17252)...
# Run task 4 (16448)...
# Run task 5 (24804)...
# Task 2 runs 0.27 seconds.
# Run task 6 (37476)...
# Task 1 runs 0.58 seconds.
# Run task 7 (4044)...
# Task 3 runs 0.98 seconds.
# Run task 8 (17252)...
# Task 5 runs 1.13 seconds.
# Run task 9 (24804)...
# Task 6 runs 1.46 seconds.
# Task 4 runs 2.73 seconds.
# Task 8 runs 2.18 seconds.
# Task 7 runs 2.93 seconds.
# Task 9 runs 2.93 seconds.
# 结束测试

问题记录

在windows中的程序建议将
from multiprocessing import freeze_support
freeze_support()
放到程序的最开始,我在之前碰到过脚本中没有加freeze_support(),以py脚本状态运行代码没有任何问题,但是将脚本打包成exe工具(pyinstaller打包)之后,导致多进程动作失效,且会报一个什么fork啥的错误(虽然会报错但是除了多进程的动作失效外没有其他的异常),加了freeze_support()之后解决了此问题
网上有说是因为 对于pyinstaller 是不支持含有多进程的python打包,打包完毕后,不会执行子进程的内容,而是会一直创建进程,导致崩溃

解决方法:
multiprocessing.freeze_support()
这句话一定要放在main下的第一行,如下所示

if __name__ == '__main__':
    from  multiprocessing import Process,Pool
    from  multiprocessing import freeze_support
    freeze_support()
    
    import os, time, random
    
    def fun1(name):
        print('Run task %s (%s)...' % (name, os.getpid()))
        start = time.time()
        time.sleep(random.random() * 3)
        end = time.time()
        print('Task %s runs %0.2f seconds.' % (name, (end - start)))
    
    if __name__=='__main__':
        pool = Pool(5) #创建一个5个进程的进程池
    
        for i in range(10):
            pool.apply_async(func=fun1, args=(i,))
    
        pool.close()
        pool.join()
        print('结束测试')

============================================================

线程

说明

python的多线程是假多线程,本质是交叉串行,并不是严格意义上的并行,或者可以这样说,不管怎么来python的多线程在同一时间有且只有一个线程在执行(举个例子,n个人抢一个座位,但是座位就这一个,不管怎么抢同一时间只有一个人在座位上,可能前一秒是a在座位上座,后一秒b就给抢去了)
多线程个线程的数据是共享的
接口拓展
事件
from threading import Event
子线程外部创建对象
event = Event()
子线程1内部发送信号
event.set()
子线程2内部等待且接收信号(也可以多个其他线程同时等这个信号)接收到信号可以往下走
event.wait()

线程锁由于线程数据互通,所以可在最开始创建锁对象,可以在程的参数中传递进去,也可以直接不管和变量寻找规则一样在线程函数内部自己寻找,每次抢锁,完毕后解锁即可,这样可以保护一些数据的安全性,防止数据混乱,注意如果在全局申明了一个变量然后线程内部需要改变这个变量的话需要在线程内部先申明 global 变量名 然后在修改,锁的话因为不牵扯变量修改所以不需要global ,直接抢锁解锁即可
from threading import Lock
lock_obj = Lock()
抢锁
lock_obj.acquire()
释放锁
lock_obj.release()

执行逻辑:
1.没有守护线程,主线程的代码如果先于子线程的代码之前执行完毕的话,主线程会等待所有子线程执行完毕死亡之后在收回这些子线程的资源然后再死亡
2.守护线程,主线程死亡之后守护子线程也会立即死亡,但是主线程执行完代码后如果有未死亡的非守护线程,他会等待这些非守护线程执行完毕,替他们收尸之后再死亡,这个时候如果有守护线程还在运行,守护线程就会立即死亡
总结:收尸原则,主线程会替它创建的所有非守护的子线程收尸,也就是纵然有时候主线程会先执行完毕,但是他不会立即死亡,他会等所有非守护线程执行完毕,给他们收完尸之后再死亡,这块和进程是一样的,而与守护进程不同的是,守护线程会在主线程死亡的时候立即死亡而不是执行完代码。你也可以理解为守护线程是守护的当前进程的最后一个执行完毕的非守护线程,当最后一个执行完毕的非守护线程执行完毕时,如果守护线程还没运行完则他会立即终止死亡,因为但凡有一个非守护线程还在运行,主线程就不会死亡*

多线程大体上有两种实现方式

1.继承实现多线程

threading模块
建立一个类然后继承这个类threading.Thread

import threading

# 创建动作类继承threading.Thread
class MyThread(threading.Thread):
    def __init__(self, name):
        threading.Thread.__init__(self, name=name)

    # 重写run方法,此时run方法里面的动作就是此线程会执行的动作
    def run(self):
        print('thread {} is running'.format(self.name))


temp_list = ['thread1', 'thread2']
t1 = MyThread('thread1')
t2 = MyThread('thread2')


temp1 = [MyThread(t) for t in temp_list]
temp2 = [s.start() for s in temp1]
temp3 = [j.join() for j in temp1]

2.线程类传参实现多线程

import threading
from threading import current_thread, active_count
import time

def worker(a, b):
    while True:
        print('Worker thread running  {} {}'.format(a, b))
        time.sleep(1)
        # 打印当前子线程的名字
        print(current_thread().name)


a = 123
b = "hahaha"
# 创建守护线程1
t = threading.Thread(target=worker, args=(a, b, ), daemon=True)
# 2守护线程也可以这样设置
daemon=True
# 启动线程
t.start()
# join()调用之后程序会等到此线程结束后再执行t.join()后面主线程的代码
t.join()

# 打印当进程中前活跃的线程总数量
print(active_count())
# 主线程执行一些操作
print('Main thread running')
time.sleep(5)
print('Main thread finished')

协程

说明

python的协程是比线程更小的单位,协程是一种轻量级的线程,可以在一个线程中实现并发。相比进程线程消耗的系统资源更少,原因是协程可以理解为是一个函数,通过函数内部的机制进行任务的调度切换,而不是通过操作系统来进行调度。与进程线程不同的是,协程的切换可以人为在代码层面进行设置切换,切换的整个过程不需要依赖操作系统,所以协程的切换不会消耗那么多系统资源
与多线程和多进程不同,协程是可以由程序员自己控制的。在协程中,程序员可以手动挂起和恢复协程的执行。因此,协程可以更好地支持异步编程,以及在I/O密集型应用中更高效地利用计算机资源
由于工作涉及到协程,所有稍加研究,但是协程这块研究不是很深之后有新的理解再加入
协程的实现,有多种方法,我所知道的一种是使用第三方协程库gevent 直接python -m pip install gevent安装

import gevent

# gevent协程实现与线程类似
g_list = []
task_info_list = ["协程1", "协程2", "协程3", "协程4", "协程5", "协程6"]

def g_test_fun(a, b, c, d):
    print(a)
    # 在gevent实现的协程里面在执行io等不依赖cpu的操作的时候,
    # 此模块会自动切换到别的协程里面去,这一点类似线程,而gevent协程模块多了一个有意思的接口gevent.sleep(),
    # 这个接口本质上是会模拟不依赖cpu的io等操作,然后促使当前主程序句柄切换到别的协程执行任务
    gevent.sleep(1)
    print(b)
    gevent.sleep(1)
    print(c)
    
for i in task_info_list:
    # 类似线程的方法,传递第一个参数为函数名,剩下的为函数依次的参数
    g = gevent.spawn(g_fun_name, "{} 协程函数的第一个参数".format(i), "{} 协程函数的第二个参数".format(i) ,"{} 协程函数的第三个参数".format(i),"{} 协程函数的第四个参数".format(i))
    g_list.append(g)

# 类似线程的join()函数,不过这里传递的的所有协程列表, 等待所有传递进去的协程完成后再执行后面的代码
gevent.joinall(g_list)


# 协程1 协程函数的第一个参数
# 协程2 协程函数的第一个参数
# 协程3 协程函数的第一个参数
# 协程4 协程函数的第一个参数
# 协程5 协程函数的第一个参数
# 协程6 协程函数的第一个参数
# 协程1 协程函数的第二个参数
# 协程2 协程函数的第二个参数
# 协程3 协程函数的第二个参数
# 协程4 协程函数的第二个参数
# 协程5 协程函数的第二个参数
# 协程6 协程函数的第二个参数
# 协程1 协程函数的第三个参数
# 协程2 协程函数的第三个参数
# 协程3 协程函数的第三个参数
# 协程4 协程函数的第三个参数
# 协程5 协程函数的第三个参数
# 协程6 协程函数的第三个参数

猴子补丁

from gevent import monkey
在程序的最开始就直接执行monkey.patch_all()将会给协程打补丁,作用是可以将协程函数中的高耗时,阻塞类代码在执行时自动修改改为非阻塞代码,然后有助于灵活切换不至于一直阻塞在一个协程中

import gevent
from gevent import monkey
monkey.patch_all()

# gevent协程实现与线程类似
g_list = []
task_info_list = ["协程1", "协程2", "协程3", "协程4", "协程5", "协程6"]


def g_test_fun(a, b, c, d):
    print(a)
    # 其他方式实现的协程不太清楚,起码在gevent实现的协程里面在执行io等不依赖cpu的操作的时候,
    # 也会自动切换到别的协程里面去这点类似线程,而gevent协程模块多了一个有意思的接口gevent.sleep(),
    # 这个接口本质上是会模拟不依赖cpu的io等操作,然后促使当前主程序句柄切换到别的协程执行任务
    gevent.sleep(1)
    print(b)
    gevent.sleep(1)
    print(c)


for i in task_info_list:
    # 类似线程的方法,传递第一个参数为函数名,剩下的为函数依次的参数
    g = gevent.spawn(g_fun_name, "{} 协程函数的第一个参数".format(i), "{} 协程函数的第二个参数".format(i), "{} 协程函数的第三个参数".format(i),
                     "{} 协程函数的第四个参数".format(i))
    g_list.append(g)

# 类似线程的join()函数,不过这里传递的的所有协程列表, 等待所有传递进去的协程完成后再执行后面的代码
gevent.joinall(g_list)


# 协程1 协程函数的第一个参数
# 协程2 协程函数的第一个参数
# 协程3 协程函数的第一个参数
# 协程4 协程函数的第一个参数
# 协程5 协程函数的第一个参数
# 协程6 协程函数的第一个参数
# 协程1 协程函数的第二个参数
# 协程2 协程函数的第二个参数
# 协程3 协程函数的第二个参数
# 协程4 协程函数的第二个参数
# 协程5 协程函数的第二个参数
# 协程6 协程函数的第二个参数
# 协程1 协程函数的第三个参数
# 协程2 协程函数的第三个参数
# 协程3 协程函数的第三个参数
# 协程4 协程函数的第三个参数
# 协程5 协程函数的第三个参数
# 协程6 协程函数的第三个参数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值