多任务:进程与线程

一、多任务

  通常,我们写的程序都是单任务的,也就是说一个函数或方法执行完成后,另一个函数或方法才能执行,如果想要它们同时执行就需要借助多任务。多任务,顾名思义就是同一时间内执行多个任务,它可以充分利用CPU资源,提高程序的执行效率,如现在的电脑安装的操作系统都是多任务操作系统,可以同时运行多个软件。

1.1 并发

  并发:一段时间内,操作系统交替执行多个任务,即任务数 > \gt > CPU核数。例如,对于单核CPU,操作系统轮流让多个任务交替执行:任务1执行0.01秒,任务2执行0.01秒,再切换到任务3,…,如此执行下去。由于CPU的执行速度非常快,这就导致我们感觉所有的任务都在同时执行一样。注意: 单核CPU是并发执行多任务。

1.2 并行

  并行:一段时间内,操作系统同时执行多个任务,即任务数 ≤ \le CPU核数。例如,对于多核CPU执行多任务,操作系统会给CPU的每一个核分配一个任务,多核真正一起执行多个任务。注意: 多核CPU是并行执行多任务,始终有多个任务一起执行。

二、进程

  进程,是操作系统进行资源分配和调度运行的最小单位,即计算机中一个正在运行的程序就是一个进程,如QQ和微信等。使用多进程可以大大提高程序的执行效率,一个程序运行后至少有一个进程,我们称之为主进程。创建以及切换一个进程比较消耗资源,效率较差,但是进程的健壮性高,可以利用多核资源,所以它适用于计算密集型任务。

2.1 引例: 尬歌尬舞

①需求: 编程实现一边唱歌,一边跳舞;

# 定义dance函数,负责跳舞;
def dance():
    for i in range(5):
        print("我正在跳舞!")

# 定义sing函数,负责唱歌;
def sing():
    for i in range(5):
        print("我正在唱歌!")

# 主程序入口
if __name__ == '__main__':
	# 按照程序自上而下的执行顺序,只有dance函数内部代码全部执行完毕,sing函数才会开始执行;
    dance()  # 调用dance函数
    sing()  # 调用sing函数

②程序执行结果: 先是5遍尬舞,然后是5遍尬歌,显然这与我们的一边唱歌一边跳舞相差甚远。不过,我们可以使用进程来解决这个问题;

2.2 进程执行无参多任务

  Python提供了multiprocessing多进程模块,该模块中的Process类可以实例化一个进程对象,这个对象可以理解为一个独立的进程;

①Process主要参数:

import multiprocessing
p1 = multiprocessing.Process(group, target, name, args=(), kwargs={})
"""
	group: 这个参数一般设置为None,不用管;
	target: 执行的目标任务名,一般为函数名;
	name: 进程名,可选;
	args: 以元组的方式给执行任务传参;
	kwargs: 以字典的方式给执行任务传参;
	daemon: 是否守护主进程,默认为None;
"""

②进程实现一边跳舞一边唱歌: 在主进程的基础上,分别创建两个独立的子进程,其中一个负责跳舞,另一个负责唱歌;

import multiprocessing
import os
import time

# 定义dance函数,负责跳舞;
def dance():
    sub_process = multiprocessing.current_process()
    print("【%s】正在执行跳舞任务,它的进程号为%s!" % (sub_process.name, sub_process.pid))
    for i in range(5):
        print("我正在跳舞!")
        # 由于操作系统(OS)执行速度太快,time模块中的sleep函数可以让程序休息执行时间(秒),这样就可以清楚观察到多任务的执行情况;
        time.sleep(0.2)

# 定义唱歌函数,负责唱歌;
def sing():
    sub_process = multiprocessing.current_process()
    print("【%s】正在执行唱歌任务,它的进程号为%s!" % (sub_process.name, sub_process.pid))
    for i in range(5):
        print("我正在唱歌!")
        time.sleep(0.2)
        
# 主程序入口
if __name__ == '__main__':
	# 通过multiprocessing模块中的current_process可以获取当前进程对象,它具有自己的方法和属性,其中进程名(name)和进程号(pid)是我们需要的;
    main_process = multiprocessing.current_process()
    print("【%s】已启动,它的进程号为%s,OS模块的进程号为%d!" % (main_process.name, main_process.pid, os.getpid()))
    # 创建子进程1,负责执行跳舞任务;
    p1 = multiprocessing.Process(target=dance, name="子进程1")
    # 创建子进程2,负责执行唱歌任务;
    p2 = multiprocessing.Process(target=sing, name="子进程2")
    # 启动子进程1和子进程2;
    p1.start()
    p2.start()

③程序执行结果: 上述程序中的主进程和子进程对象中的进程号(pid)与与计算机任务管理器中的进程号一致,这也就说明了进程在程序执行过程中拥有独立的内存单元;

2.3 进程执行有参多任务

①进程实现有参多任务: 上述代码中的唱歌和跳舞次数以及休息时长是一个定值,可否让用户自己设置这些值;

import multiprocessing
import time

# 跳舞函数;
def dance(iteration, sleep_time):
    sub_process = multiprocessing.current_process()
    for i in range(iteration):
        print("我是{},这是第{}次跳舞!".format(sub_process.name, i+1))
        time.sleep(sleep_time)
        print("我是{},跳完舞后已经休息了{}秒!".format(sub_process.name, sleep_time))

# 唱歌函数;
def sing(iteration, sleep_time):
    sub_process = multiprocessing.current_process()
    for i in range(iteration):
        print("我是{},这是第{}次唱歌!".format(sub_process.name, i+1))
        time.sleep(sleep_time)
        print("我是{},唱完歌后已经休息了{}秒!".format(sub_process.name, sleep_time))

# 主程序入口;
if __name__ == '__main__':
    # 子进程1使用【元组传参】:元组()内的实参类型、个数和顺序一定要与dance函数中一致,否则会报错;
    p1 = multiprocessing.Process(target=dance, name="子进程1", args=(4, 1))

    # 子进程2使用【字典传参】:字典{}内的key名称和个数一定要与函数sing中一致,而key-value的传入顺序可以与函数中定义不一;
    p2 = multiprocessing.Process(target=sing, name="子进程2", kwargs={"iteration": 5, "sleep_time": 2})
    # 启动子进程1和子进程2;
    p1.start()
    p2.start()

②程序执行结果: Python中可以使用元组字典的形式为执行多任务的进程传入参数;

2.4 多进程执行流程源码分析

①示例代码: 创建一个子进程执行函数coding,该函数主要记录编码次数且每次输出结果后休息1秒;

import multiprocessing
import time

def coding(iteration):
    for i in range(iteration):
        print(f"这是第{i+1}遍coding!")
        time.sleep(1)
        
# 主程序入口
if __name__ == '__main__':
	# 实例化一个进程,它主要负责执行函数coding;
    coding_process = multiprocessing.Process(target=coding, name="子进程", args=(5,))
    coding_process.start()

②代码解析: Python内部是如何处理我们上述实例化一个子进程执行函数coding的过程或行为?

  • 进程创建Process(): 我们创建进程时传给Process()的各个参数,如target、name等都保存在它的父类BaseProcess()中;
coding.process = multiprocessing.Process(target=coding, name="子进程", args=(5,))
  • 进程开启start():
# 启动进程;
coding_process.start()
  • start()方法内部会调用底层的C语言代码开启子进程,并且会在开启的子进程中自动执行run()方法;
  • run()方法会调用保存在父类BaseProcess中的self._target,并且将参数args和kwargs传给它,所以target实际运行在run()方法中;

③注意: 在Python中,函数或方法并无进程之分,它只是一个任务,它运行在哪个进程就属于哪个进程;

import multiprocessing
import time

def coding(iteration):
    cur_process = multiprocessing.current_process()
    for i in range(iteration):
        print(f"我是【{cur_process.name}】,这是第{i+1}遍coding!")
        time.sleep(1)
  • 错误调用: 使用主进程和子进程执行函数的时候,如果先让主进程执行,此时子进程还未创建,代码自上至下顺序执行;
if __name__ == '__main__':
	# 利用主进程执行coding函数(此时子进程还未创建,最终执行效果就是调用两遍coding,创建子进程与否无任何意义)
    coding(5) 
    
	# 创建子进程执行coding函数;
    p1 = multiprocessing.Process(target=coding, name="子进程", args=(5,))
    p1.start()
  • 正确调用: 使用主进程与子进程执行函数时,应该先启动子进程,然后再在其后面调用主进程代码,最终就是主进程和子进程交替执行;
if __name__ == '__main__':
	# 创建子进程执行coding函数;
    p1 = multiprocessing.Process(target=coding, name="子进程", args=(5,))
    p1.start()
    
    # 利用主进程执行coding函数()
    coding(5) 

2.5 多进程面向对象实现

  从2.4多进程执行流程源码分析可以知道,我们通过Process()类创建一个进程对象时传入的参数本质上是存储在该类的父类BaseProcess()中,并且调用进程对象的start()方法底层也是在调用其父类的run()方法。那么,我们是否可以将一个进程任务以面向对象的形式进行封装,外部创建进程对象后就可以直接执行任务。

import time
import multiprocessing
from multiprocessing import current_process

# ------------------------------ 使用面向对象封装多进程任务 ------------------------------
class SubProcess(multiprocessing.Process):  # SubProcess继承自Process,而Process又继承自BaseProcess,这是一个多层继承结构;

    # 子类重写init方法,程序执行时优先调用子类而不是父类的init方法;
    def __init__(self, num):
        # 调用父类的init方法进行资源初始化,本质上【Ctrl+单击】就可以发现其实SubProcess是直接继承的BaseProcess的init方法;
        super(SubProcess, self).__init__()
        # 初始化Process()时的参数全部
        self.num = num
        # 开启进程:进程对象实例化之后就执行任务;
        self.start()

    # 定义进程需要执行的任务;
    def do_something(self):
        for i in range(self.num):
            print("第%d个数字为:" % (i + 1), i)
            time.sleep(0.3)

    # 重写父类BaseProces()的run()方法,进程对象调用start()方法后会执行它里面的代码;
    def run(self):
        sub_process = current_process()
        print("run()方法中子进程【{}】的进程号为:{}".format(sub_process.name, sub_process.pid))
        self.do_something()

# ------------------------------ 定义一个任务,使用非面向对象执行 ------------------------------
def coding(string):
    sub_pro = current_process()
    print("子进程【{}】的进程号为:{}".format(sub_pro.name, sub_pro.pid))
    for idx, ele in enumerate(string):
        print("string中的第{}个字符为:{}".format(idx+1, ele))
        time.sleep(0.4)

# 主程序入口;
if __name__ == '__main__':
    # 打印主进程信息;
    main_process = current_process()
    print("主进程【%s】的进程号为:%d".center(50, "*") % (main_process.name, main_process.pid))

    # 使用面向对象执行多进程任务;
    SubProcess(5)

    # 使用非面向对象执行多任务;
    sub_process = multiprocessing.Process(target=coding, name="SubProcess-2", kwargs={"string": "Hello"})
    sub_process.start()

2.6 进程间的执行顺序

2.6.1 主进程与子进程的结束顺序

# 公共代码,减少冗余;
import multiprocessing
import time

# 定义一个函数,内部打印0-7,每打印依次休息0.2秒;
def working():
    for i in range(8):
        print("工作中...")
        time.sleep(0.2)

①主进程和子进程的执行顺序: 为保证子进程能够正常运行,主进程会等所有的子进程执行完毕后才会销毁;

# 主程序入口;
if __name__ == '__main__':
	# 创建子进程,负责输出0-7;
    sub_process = multiprocessing.Process(target=working, name="子进程")
    sub_process.start()
    # 主进程休息0.8秒后执行;
    time.sleep(0.8)
    print("主进程执行完毕...")
  • 程序执行结果: 可以看到,主进程执行完成后并没有直接退出,而是在等子进程全部执行完毕;

②设置守护主进程: 主进程退出子进程销毁,不让主进程等待子进程执行完毕;

if __name__ == '__main__':
	# 守护主进程: 可在创建进程对象时传入daemon=True 或 创建完成后通过进程对象.daemon=True实现;
    sub_process = multiprocessing.Process(target=working, name="子进程", daemon=True)
    # sub_process.daemon = True
    sub_process.start()
    time.sleep(0.8)
    print("主进程执行完毕...")
  • 程序执行结果: 设置守护主进程后,主进程执行完成后直接销毁,不再等子进程全部执行完毕;

③手动销毁子进程: 主进程退出之前,让子进程直接销毁,终止执行;

if __name__ == '__main__':
    sub_process = multiprocessing.Process(target=working, name="子进程")
    sub_process.start()
    time.sleep(0.8)
    # 手动杀死子进程;
    sub_process.terminate()
    print("主进程执行完毕...")
  • 程序执行结果: 通过调用进程对象的terminate()方法,可以在主进程退出前手动杀死子进程;

④主进程堵塞: 进程对象中有一个join()方法,它可以保证在子进程没有执行完成前,主进程不会退出;

if __name__ == '__main__':
    sub_process = multiprocessing.Process(target=working, name="子进程")
    sub_process.start()
    # 调用进程对象join()方法即可保证子进程执行完成后,主进程才销毁,也就是将并行工作转为串行工作;
    sub_process.join()  # 当然,我们也可以为time.sleep()设置一个足够长的休息时间,直至子进程执行完成;
    print("子进程执行完成...")
    print("主进程执行完毕...")
  • 程序执行结果: 可以看到,调用进程对象的join()方法后,子进程全部执行完成后,主进程才会退出;

⑤进程知识扩充

  • 僵尸进程: 父进程正在运行,子进程结束,但是操作系统并不会立即将其清除,目的是父进程可以访问子进程信息;
  • 孤儿进程: 一个父进程已经死亡,然而它的子进程还在执行,这个时候操作系统会来接管这些子进程;

2.6.2 子进程间的执行顺序

import multiprocessing
from multiprocessing import current_process
import time

# 定义一个函数:获取不同进程进来后的信息;
def say_hello():
    sub_process = current_process()
    print("子进程【%s】向您问好: Hello!" % sub_process.name)
    time.sleep(0.5)

①多进程无序执行任务: 默认情况下,多个子进程之间执行任务是无序的,不随进程创建的快慢而改变;

# 主程序入口;
if __name__ == '__main__':
    # 创建8个子进程;
    for i in range(8):
        sub_pro = multiprocessing.Process(target=say_hello)
        sub_pro.start()

②多进程有序执行任务: 调用进程对象的join()方法就可以保证多个子进程之间有序执行任务;

# 主程序入口;
if __name__ == '__main__':
    # 创建8个子进程;
    for i in range(8):
        sub_pro = multiprocessing.Process(target=say_hello)
        sub_pro.start()
        sub_pro.join()

2.7 进程间不共享全局变量

  创建子进程会对主进程的资源进行拷贝,也就是说子进程是主进程的一个副本,就像是一对双胞胎。之所以进程间不能共享全局变量,是因为它们操作的是各自进程中的全局变量,只不过不同进程中的全局变量名字一样而已。

①示例代码: 分别创建两个子进程,它们同时执行对全局变量进行加1操作;

import multiprocessing
# 全局变量num;
num = 100

def num_sum1():
    sub_process = multiprocessing.current_process()
    # Python中的函数可以分隔变量的作用域,它不能直接操作全局变量,需要借助global关键字;
    global num
    num += 1
    print("我是【%s】,修改num后的值为:" % sub_process.name, num)

def num_sum2():
    sub_process = multiprocessing.current_process()
    global num
    num += 1
    print("我是【%s】,修改num后的值为:" % sub_process.name, num)
    
# 主程序入口;
if __name__ == '__main__':
	# 分别创建两个子进程,它们分别执行对全局变量加1操作;
    sum1 = multiprocessing.Process(target=num_sum1, name="子进程1")
    sum2 = multiprocessing.Process(target=num_sum2, name="子进程2")
    sum1.start()
    sum2.start()
  • 程序执行结果: 进程之间相互独立,它们各自保存(拷贝)一份全局变量;

②扩展练习: 分别创建两个进程,一个负责向全局变量写数据,另一个负责读取全局变量中的数据;

import multiprocessing
import time
my_list = []

# 向全局变量my_list中写入数据;
def num_sum1():
    sub_process = multiprocessing.current_process()
    for i in range(3):
        my_list.append(i)
        print("Add:", i)
    print("我是【%s】, 我正在写数据:" % sub_process.name, my_list)

# 读取全局变量my_list中的数据;
def num_sum2():
    sub_process = multiprocessing.current_process()
    print("我是【%s】, 我正在读数据:" % sub_process.name, my_list)

if __name__ == '__main__':
    sum1 = multiprocessing.Process(target=num_sum1, name="子进程1")
    sum2 = multiprocessing.Process(target=num_sum2, name="子进程2")
    # 启动子进程;
    sum1.start()
    time.sleep(1)
    sum2.start()
  • 程序执行结果: 写数据和读数据操作的是各自的my_list,所以它们不能资源共享;

2.8 进程间通信

  实际项目开发过程中,有时单进程无法满足我们的需求,这时就需要创建多个进程去执行不同的任务,但是进程间又不能共享全局变量,如何在它们需要沟通协作的时候实现相互通信。Python中,进程间通信(Inter-Process Communication, IPC)主要有两种方式:共享内存和消息传递。共享内存,多个进程间共享一块内存区域,它们在此区域进行数据读写,从而实现进程间通信,这块内存区域支持multiprocessing.Manager中的list、dictArray等数据类型。消息传递方式的实现主要通过队列和管道。

2.8.1 基于Queue队列

  这种方式主要是借助队列Queue先进先出的特点,实现不同子进程之间的通信,其中Put()方法是从队尾添加元素,Get()是从对头获取元素,如下所示:

①Queue常用功能: 创建队列时可设置最大长度(maxsize),默认为0表示不限长度;

  • 队列不限最大长度: 可以不断地添加元素以及获取元素;
from multiprocessing import Queue  # 从multuiprocessing模块中导入Queue;
queue = Queue()

for i in range(5):
    queue.put(i)

for i in range(5):
    print(f"从队列中获取第{i+1}个元素为:%d" % queue.get())
  • 队列限制最大长度: 添加元素数量如果大于maxsize,则put()方法会堵塞;获取元素时,如果队列为空,get()方法也会堵塞;
from multiprocessing import Queue

queue = Queue(5)  # 实例化一个队列对象,设置它的最大长度为5;
# 循环向队列中添加元素;
for i in range(7):
    if not queue.full():  # full()方法可以判断队列是否已经达到最大长度;
        print(f"向队列中添加第{i+1}个元素为:", i)
        queue.put(i)  # 队列已满,程序就会堵塞,等待数据从队列中拿走;
        # put_nowait():如果队列已满,则直接报错,不会堵塞;
    else:
        print("队列已满,第【{}】个元素已不能继续添加,程序处于堵塞状态!".format(i))

# 循环从队列中获取元素;
for i in range(7):
    if not queue.empty():  # empty()可以用来判断队列是否为空;
        print("从队列中获取第{}个元素!".format(i+1), queue.get())  # get_nowait():如果队列已空,不会堵塞,直接报错;
    else:
        print("队列为空,第【{}】次已不能再从队列中获取元素!".format(i))

②进程通信1: 分别创建3个生产商和1个消费者生产和购买面包,其中每个生产商生产5个面包,消费者总共需要10个面包(生产者消费者模型);

import time
from multiprocessing import Process, Queue, current_process
num = 5

# 不同生产商生产面包;
def producer(x):
    sub_process = current_process()
    for i in range(num):
        time.sleep(0.42)
        print("{}--->{}号面包!".format(sub_process.name, i+1))
        x.put(f"{sub_process.name}{i+1}号面包!")  #


# 同一个消费者购买面包;
def consumer(x):
    for i in range(num*2):  # 设置该消费者需要的面包数;
        time.sleep(0.2)
        print('消费者---->{}'.format(x.get()))

# 主程序入口;
if __name__ == '__main__':
    q = Queue()  # 创建一个队列对象,不限制最大长度;
    # 实例化三个生产商对象:它们都同时生产面包: 总共生产15个面包;
    p1 = Process(target=producer, name="生产商1", args=(q,))
    p2 = Process(target=producer, name="生产商2", args=(q,))
    p3 = Process(target=producer, name="生产商3", args=(q,))
    p1.start()
    p2.start()
    p3.start()
    # 创建一个消费者对象: 总共买10个面包!
    c2 = Process(target=consumer, args=(q,))
    c2.start()

③进程通信2: 分别定义两个进程,一个负责文件写入,一个负责文件读取;

import time
from multiprocessing import Process, Queue, current_process

# 定义一个写入函数;
def writer(q):
    sub_process = current_process()
    for i in range(12):  # 我们这里故意将写入次数大于队列的最大长度;
        # 每次添加之前先判断队列是否已满,如果已满则直接退出,避免程序堵塞;
        if q.full():
            print("队列已满".center(30, "-"))
            break
        print("【{}】写入了{}!".format(sub_process.name, i))
        q.put(i)
        time.sleep(0.3)

# 定义一个读取函数;
def reader(q):
    sub_process = current_process()
    # 循环读取,直至队列为空;
    while True:
        # 每次从队列中读取数据之前,先判断队列是否为空;
        if q.empty():
            print("队列已空".center(30, "-"))
            break
        print("%s拿到:" % sub_process.name, q.get())

# 主程序入口;
if __name__ == '__main__':
    # 创建消息队列: 用于进程之间的消息共享;
    queue = Queue(10)

    # 创建子进程1负责执行写入任务;
    p1 = Process(target=writer, name="写入进程", args=(queue,))
    p1.start()
    p1.join()  # 先让写入进程执行完成后,再执行读取进程(写入进程休眠时间内,读取进程如果执行empty则直接退出!);
    # 创建子进程2负责执行读取任务;
    p2 = Process(target=reader, name="读取进程", kwargs={"q": queue})
    p2.start()

2.8.2 基于Pipe管道

  Pipe()方法返回两个参数:conn1conn2,它们分别代表一个管道的两端。此外,该方法有一个参数duplex,默认为True,表示管道是双工模式,即conn1conn2都具备收发功能;如果该参数为False,则conn1只负责接收数据,而conn2只负责发送消息,发送和接收消息的方法分别为send()recv()

①Pipe管道单工模式: conn1负责发送消息,conn2负责接收消息,Pipe参数duplexFalse;

import multiprocessing
import time

# 接收进程;
def process_receiver(pipe):
    while True:
        data = pipe.recv()
        print("pipe -> recv: \33[42;1m 接收 \033[0m", data)

# 发送进程;
def process_sender(pipe):
    for i in range(5):
        print("send -> pipe: \33[41;1m 发送 \033[0m", i)
        pipe.send(i)
        time.sleep(0.2)

# 主程序入口;
if __name__ == '__main__':
    pipe = multiprocessing.Pipe(duplex=False)  # 单工模式;

    p_sender = multiprocessing.Process(target=process_sender, args=(pipe[1],))  # conn2负责接收;
    p_receiver = multiprocessing.Process(target=process_receiver, args=(pipe[0],))  # conn1负责发送;

    p_sender.start()
    p_receiver.start()

②Pipe管道双工模式:

import multiprocessing
import time

def process_receiver(pipe):
    while True:
        data = pipe.recv()
        print("pipe -> recv: \33[42;1m 接收 \033[0m", data)
        pipe.send("报告:{}".format(data))

def process_sender(pipe):
    for i in range(5):
        print("send -> pipe: \33[41;1m 发送 \033[0m", i)
        pipe.send(i)
        resp = pipe.recv()
        print("收到回复-> {}".format(resp))
        time.sleep(0.3)

# 主程序入口;
if __name__ == '__main__':
    # duplex双工模式,默认为True
    pipe = multiprocessing.Pipe(duplex=True)
    p_receiver = multiprocessing.Process(target=process_receiver, args=(pipe[0],))
    p_sender = multiprocessing.Process(target=process_sender, args=(pipe[1],))

    p_receiver.start()
    p_sender.start()

2.8.3 基于文件

需求: 分别创建两个进程实现以下功能(可以操作同一个文件,其中进程2向文件中写数据,进程1读到数据后停止循环);

  1. 进程1中输出1到10的数据,每隔一秒钟输出一个数据;
  2. 进程2中输出10到20的数据,每隔一秒钟输出一个数据;
  3. 进程2输出到16的时候要通知进程1停下来;
import time
from multiprocessing import Process

# 进程1;
def process1():
    try:
        f = open("info.txt", mode="r")
    except:
        f = open("info.txt", mode="w+")
    for ele in range(1, 11):
        content = f.read()
        if content == "1":
            print("进程1停止循环!")
            f.close()
            return
        print(ele)
        time.sleep(1)

# 进程2;
def process2():
    with open("info.txt", mode="w") as f:
        for i in range(10, 21):
            if i == 16:
                f.write("1")
                f.close()
            print(i)
            time.sleep(1)

# 主程序入口;
if __name__ == '__main__':
    # 分别创建两个进程,执行上面的任务;
    p1 = Process(target=process1, name="进程1")
    p2 = Process(target=process2, name="进程2")
    p1.start()
    p2.start()

2.9 进程池

  实际开发过程中,当需要创建的子进程数量不多时,我们可以直接利用multiprocessing模块中的Process类来动态生成多个进程。但是,如果有成百上千个任务需要执行的时候,难道我们也要为每一个任务创建一个进程吗?显然,这是不切合实际的想法,一方面,进程的创建和销毁需要消耗时间;另一方面,即使我们可以创建成百上千个进程,操作系统不会也不能同时执行它们,如从反而会影响程序的执行效率。这种情况下,我们就需要引入进程池这个概念了。顾名思义,进程池就是先定义一个池子,程序设计者可以在里面放入固定数量的进程:如果任务数小于池中的进程数,操作系统随机选取与任务数相等的进程数去处理它们,任务完成后这些进程并不会关闭而是放回池子中继续等待任务;如果任务数大于池中的进程数,超出进程数并且还没有处理的任务需要等待,直至有空闲进程才能继续执行。
  对应于Python中,我们可以利用multiprocessing模块中的进程池Pool来实现。初始化Pool时可以指定最大进程数(processes):当有新的请求提交到Pool时,如果进程池还没满就会创建一个进程来执行该请求;如果进程池中的进程数已经达到指定的最大值,那么该请求就会等待,直到进程池中有进程结束,才会用之前的进程来执行新任务。在工作模式上,Pool支持阻塞非阻塞两种方式调用进程,其中非阻塞方式(apply_async(func[, *args[, **kwargs]]))会使用所有的空闲子进程执行func,而阻塞方式(apply(func[, *args[, **kwargs]]))则会等待上一个进程结束才会开启下一个进程。

import time, random, os
from multiprocessing import Pool
# 定义一个函数:统计每一个进程的执行时间;
def worker(msg):
    start = time.time()
    print("任务【%s】开始执行,进程号为:【%d】" % (msg, os.getpid()))
    time.sleep(random.random()*2)
    end = time.time()
    print("任务{0}执行完毕,耗时{1:.2f}秒!".format(msg, (end-start)))

# 主程序入口;
if __name__ == "__main__":
	# 1.初始化Pool,设置维持执行的最大进程总数processes=3;
    pool = Pool(processes=3)
    for i in range(10):
        """
        # 2.每一轮循环调用空闲子进程执行任务;
        pool.apply_async(worker, (i,))
        # pool.apply(worker, (i,))
    # 3.关闭进程池,关闭后pool不会再接收新的请求;
    pool.close()
    # 4.主进程堵塞,主进程等待pool中的所有子进程执行结束后才能退出,必须位于close()或terminate()之后,否则会报错;
    pool.join()

三、线程

  线程是程序执行的最小单位,一个进程至少有一个线程来负责执行程序,一般我们称其为主线程,线程一般不拥有资源,但是可以与同一进程内的其它线程共享全局变量,所以它更适合于I/O密集型任务。Python中,我们可以借助threading模块中的Thread()类创建子线程对象。

Process()参数详解:

from threading import Thread
# 创建一个线程对象;
sub_process = Thread(group, target, name, args, kwargs, daemon)
"""
	group: 这个参数一般设置为None,不用管;
	target: 执行的目标任务名,一般为函数名;
	name: 进程名,可选;
	args: 以元组的方式给执行任务传参;
	kwargs: 以字典的方式给执行任务传参;
	daemon: 是否守护主进程,默认为None;
"""

2.1 多线程执行无参任务

①需求: 分别创建两个子线程,一个用于执行唱歌任务,另一个用于执行跳舞任务,要求它们同时进行;

import time
import threading
from threading import current_thread
from multiprocessing import current_process

# 定义一个唱歌任务;
def sing():
    sub_thread = current_thread()
    print("线程【%s】正在执行唱歌任务!" % sub_thread.name)
    for i in range(5):
        print("这是第%d遍唱歌!" % (i+1))
        time.sleep(0.3)

# 定义一个跳舞任务;
def dance():
    sub_thread = current_thread()
    print("线程【%s】正在执行跳舞任务!" % sub_thread.name)
    for i in range(5):
        print("这是第%d遍跳舞!" % (i + 1))
        time.sleep(0.2)

# 主程序入口;
if __name__ == '__main__':
    # 分别打印主进程和主线程的信息;
    main_process = current_process()
    print("主进程【{}】的进程号为:{}".format(main_process.name, main_process.pid))
    main_thread = current_thread()
    print("主线程【%s】已启动..." % main_thread.name)

    # 分别创建子线程1执行唱歌任务,子线程2执行跳舞任务,建议多进程和多线程使用关键字参数传参;
    sing_thread = threading.Thread(target=sing)
    dance_thread = threading.Thread(target=dance)
    sing_thread.start()
    dance_thread.start()

②执行结果: 可以看到,一个程序启动后默认会有一个主进程,主进程之中又包含一个主线程用于启动程序,进程申请资源,线程利用资源;

2.2 多线程执行有参任务

需求: 定义两个子线程,一个做波比跳,一个做俯卧撑,要求做两个波比跳之后再做一个俯卧撑,它们交替执行;

import time
import threading
from threading import current_thread

# 定义一个波比跳任务;
def bobby_jump(num):
    sub_thread = current_thread()
    print("线程【%s】开始执行波比跳任务!" % sub_thread.name)
    for i in range(num):
        print("这是第%d遍波比跳!" % (i+1))
        time.sleep(0.2)

# 定义一个俯卧撑任务;
def push_up(num):
    sub_thread = current_thread()
    print("线程【%s】开始执行俯卧撑任务!" % sub_thread.name)
    for i in range(num):
        print("这是第%d遍俯卧撑!" % (i + 1))
        time.sleep(0.42)

# 主程序入口;
if __name__ == '__main__':
    # 分别创建子线程1执行唱歌任务,子线程2执行跳舞任务;
    jump_thread = threading.Thread(target=bobby_jump, args=(10,))
    push_thread = threading.Thread(target=push_up, kwargs={"num": 5})
    jump_thread.start()
    push_thread.start()

2.3 多线程面向对象实现

需求: 分别定义两个子线程,其中一个子线程执行coding任务,需要使用面向对象封装任务,另一个执行music任务,不需要封装;

import time
import threading

# 面向对象线程执行任务封装;
class SubThread(threading.Thread):

    # 所有传入Thread()中的参数,我们都可以在Init()方法中进行接收然后使用;
    def __init__(self, num):
        # 调用父类BaseProcess()的init方法进行资源初始化;
        super(SubThread, self).__init__()
        self.num = num
        # 启动线程;
        self.start()  # 调用run()方法 --> 调用具体任务coding();

    def coding(self):
        for i in range(self.num):
            print("第{}个元素为:{}".format(i+1, i))
            time.sleep(0.12)

    def run(self):
        sub_thread = threading.current_thread()
        print("run()方法中的子线程【%s】开始执行coding任务..." % sub_thread.name)
        self.coding()

# 听音乐
def music():
    sub_thread = threading.current_thread()
    print("子线程【%s】开始执行music任务..." % sub_thread.name)
    for i in range(5):
        print("夫妻双双把家还...")
        time.sleep(0.2)

# 主程序入口;
if __name__ == '__main__':
    # 主线程启动程序;
    main_thread = threading.current_thread()
    print("主程序【%s】启动程序..." % main_thread.name)

    # 1.以面向对象方式启动线程执行编码任务;
    SubThread(10)

    # 2.以非面向对象方式启动线程执行唱歌任务;
    sub_thr = threading.Thread(target=music)
    sub_thr.start()

2.4 线程间的执行顺序

  我们知道,进程之间执行任务是无序的,并且默认情况下主进程会等所有的子进程执行完成后才会退出。那么,子线程间执行任务是有序还是无序,主线程执行完成后是否会等子线程?

2.4.1 主线程与子线程的结束顺序

import time
import threading

# 定义一个任务;
def get_thread_info():
    sub_thread = threading.current_thread()
    print("子线程【%s】开始执行任务..." % sub_thread.name)
    for i in range(5):
        print("元素为:", i)
        time.sleep(0.4)

①主线程与子线程执行顺序: 默认情况下,主线程会等子线程执行完成后才会退出;

if __name__ == '__main__':
    # 主线程启动程序;
    main_thread = threading.current_thread()
    print("主线程【%s】启动..." % main_thread.name)

    # 创建1个子线程执行get_thread_info任务;
    thread = threading.Thread(target=get_thread_info)
    thread.start()

    # 主程序休息1秒钟后退出;
    time.sleep(1)
    print("主线程执行完毕...")

②守护主线程(daemon): 主线程执行完成后,不需要等待子线程执行完成即可结束;

if __name__ == '__main__':
    # 主线程启动程序;
    main_thread = threading.current_thread()
    print("主线程【%s】启动..." % main_thread.name)

    # 创建线程对象时传入daemon=True或创建线程对象后设置setDaemon(True)都可以守护主线程;
    thread = threading.Thread(target=get_thread_info, daemon=True)  
    # thread.setDaemon(True)
    thread.start()

    # 主程序休息1秒钟后退出;
    time.sleep(1)
    print("主线程执行完毕...")

③主线程堵塞: 线程中有一个join()方法,它可以保证在子线程没有执行完成前,主线程不会退出;

if __name__ == '__main__':
    # 主线程启动程序;
    main_thread = threading.current_thread()
    print("主线程【%s】启动..." % main_thread.name)

    # 创建1个子线程执行get_thread_info任务;
    thread = threading.Thread(target=get_thread_info)
    thread.start()
    # 主线程堵塞:等待子线程执行完成所有任务后才会退出;
    thread.join()
    print("主线程执行完毕...")

2.4.2 子线程间的执行顺序

import time
import threading

# 定义一个任务: 获取线程信息;
def get_thread_info():
    time.sleep(0.5)
    sub_thr = threading.current_thread()
    print(sub_thr)

①子线程无序执行任务

if __name__ == '__main__':
    # 分别定义8个子线程执行get_thread_info任务: 看看线程之间执行是否有序;
    for i in range(8):
        sub_thread = threading.Thread(target=get_thread_info)
        sub_thread.start()

②子线程有序执行任务

if __name__ == '__main__':
    # 分别定义8个子线程执行get_thread_info任务: 看看线程之间执行是否有序;
    for i in range(8):
        sub_thread = threading.Thread(target=get_thread_info)
        sub_thread.start()
        sub_thread.join()  # join()方法可以保证上一个线程执行完成后,下一个线程才会开始;

2.5 线程间共享全局变量

需求: 定义一个全局变量,分别创建两个子线程对它的值进行加1操作,观察它会发生什么样的变化;

import threading
# 全局变量num: 创建两个线程修改它;
num = 100

def num_sum1():
    sub_thread = threading.current_thread()
    # Python中的函数可以分隔变量的作用域,它不能直接操作全局变量,需要借助global关键字;
    global num
    num += 1
    print("我是【%s】,修改num后的值为:" % sub_thread.name, num)

def num_sum2():
    sub_thread = threading.current_thread()
    global num
    num += 1
    print("我是【%s】,修改num后的值为:" % sub_thread.name, num)

# 主程序入口;
if __name__ == '__main__':
    print("全局变量num的初始值为:", num)
    # 分别创建两个子进程,它们分别执行对全局变量加1操作;
    sum1 = threading.Thread(target=num_sum1, name="子线程1")
    sum2 = threading.Thread(target=num_sum2, name="子线程2")
    sum1.start()
    sum2.start()

2.6 线程间资源竞争问题

  线程间资源竞争的原因是:当一个线程修改数据的同时,另一个线程在上一个线程还没有修改完成数据的时候也同时修改了同一数据,这样就会造成数据错误。因此,多个线程之间修改同一个全局变量的时候需要进行信息共享或同步控制。

import threading
import time
# 全局变量ticket;
ticket = 10

# 定义一个卖票函数;
def sale_ticket():
    # Python中函数可以分割变量的作用域;
    while True:
        if ticket > 0:
            time.sleep(0.2)
            global ticket
            ticket -= 1
            print('【{}】卖出一张票,还剩【{}】张!'.format(threading.current_thread().name, ticket))
        else:
            print("票卖完了...")
            break

# 主程序入口;
if __name__ == '__main__':
    # 分别创建两个子进程,它们分别执行对全局变量加1操作;
    threading1 = threading.Thread(target=sale_ticket, name="子线程1")
    threading2 = threading.Thread(target=sale_ticket, name="子线程2")
    threading1.start()
    threading2.start()

2.6.1 互斥锁

  互斥锁就是当有多个线程同时修改一个共享数据时,可以保证多个线程之间安全竞争资源,从而保证数据的正确性。互斥锁本质上就是为公共资源引入了一个状态:锁定(Lock)或非锁定(Unlock),类似于公共厕所上的锁一样,它可以确保每次只有一个线程操作数据。具体来说就是,当某个线程要更改共享数据时,必须先将其锁定,此时资源的状态为Lock,其它线程不能更改,直到该线程释放(Release)资源,将它的状态变为Unlock,其它线程才能再次锁定该资源。虽然互斥锁成功解决了线程间的资源竞争问题,但是伴随而来的是执行效率的降低,这也就暗示我们实际工作中对于非核心但可同时执行的任务使用多线程执行,而对于核心但只能串行解决的任务则使用这种方式。

import threading
import time
# 定义全局变量ticket;
ticket = 10

# 定义一个卖票函数;
def sale_ticket():
    # 2.线程进来加锁(需要等待释放下一个线程才能进来)
    switch.acquire()
    while True:
        if ticket > 0:
            time.sleep(0.2)
            global ticket
            ticket -= 1
            print('【{}】卖出一张票,还剩【{}】张!'.format(threading.current_thread().name, ticket))
        else:
            print("票卖完了...")
            break
    # 3.线程离开将锁释放;
    switch.release()

# 主程序入口;
if __name__ == '__main__':
    # 1.先创建一把锁;
    switch = threading.Lock()
    # 分别创建两个子进程,它们分别执行对全局变量加1操作;
    threading1 = threading.Thread(target=sale_ticket, name="子线程1")
    threading2 = threading.Thread(target=sale_ticket, name="子线程2")
    threading1.start()
    threading2.start()

2.6.2 死锁

  某个线程加锁后没有释放锁便可能造成死锁,而产生死锁的原因主要有: 互斥条件: 多个条件使用同一个资源,资源不能共享,同时只能满足一个线程使用; 循环等待条件: 若干个线程或进程形成环形链,每个都占用对方申请的下一个资源; 请求与保持条件: 进程或线程以获得一些资源,但因请求其它资源被阻塞时,对以获得资源不放;
需求: 分别创建两个任务和一把锁,它们同时修改一个全局变量,每一个任务都只加锁,不释放锁,看看会发生什么?

import threading
# 全局变量
g_num = 0

# 对g_num进行加操作
def sum_num1():
    sub_thread1 = threading.current_thread()
    print("【%s】进来加锁了..." % sub_thread1.name)
    # 上锁
    mutex.acquire()
    for i in range(10000):
        global g_num
        g_num += 1
    print("【%s】离开了没有释放锁..." % sub_thread1.name)

# 对g_num进行加操作
def sum_num2():
    sub_thread2 = threading.current_thread()
    print("【%s】进来了,锁没有释放进不去..." % sub_thread2.name)
    # 上锁
    mutex.acquire()
    for i in range(10000):
        global g_num
        g_num += 1
    print("g_num2:", g_num)
    print("【%s】离开了没有释放锁..." % sub_thread2.name)

# 主程序入口;
if __name__ == '__main__':
    # 创建锁
    mutex = threading.Lock()
    # 创建子线程
    sum1_thread = threading.Thread(target=sum_num1)
    sum2_thread = threading.Thread(target=sum_num2)

    # 启动线程
    sum1_thread.start()
    sum2_thread.start()

2.6.3 全局解释器锁(GIL)

  Python有很多种解释器,比如CPython、JPython和PyPy等,但是市场占有率(99.9%)最高的就是基于C语言编写的CPython,它的内部规定了全局解释器锁(Global Interpreter Lock,GIL)。无论我们的电脑有多少个CPU,Python在执行程序时,同一时刻只允许一个线程运行。Python的线程虽然是真正的线程,但解释器执行代码时会有一个GIL锁:任何Python线程执行前必须先获得GIL锁,然后每执行100条字节码,解释器就会自动释放GIL锁,让别的线程有机会进入,而这一特性在PyPy和JPython中是没有的。

2.8 线程池

  线程池就是创建若干个可执行的线程放入一个池子(容器)中,当有任务需要处理时,会提交到线程池中的任务队列,处理完任务后的线程并不会销毁,而是放回池子中等待下一个任务,Python中的线程池对象需要借助concurrent.futures中的ThreadPoolExecutor()类来创建。

import threading
import time
from concurrent.futures import ThreadPoolExecutor

# 定义一个函数:记录线程池中的不同线程信息;
def say_hello():
    sub_thread = threading.current_thread()
    print("子线程【%s】向您问好: Hello!" % sub_thread.name)
    time.sleep(0.5)

# 主程序入口;
if __name__ == '__main__':
    main_thread = threading.current_thread()
    print("主线程【%s】已开启..." % main_thread.name)
    # 创建一个容量最多为3的线程池: max_workers=3;
    executor = ThreadPoolExecutor(max_workers=3)
    for i in range(9):
        executor.submit(say_hello)  # 任务和参数;
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值