Python-多线程-全局解释器锁 GIL-运行机制-全局变量-多进程调用


在算法一定的情况下,如何减少程序运行的时间,多线程是一种不错的办法。能够更高效的获取操作系统资源。

1.全局变量

全局变量:模块内、所有函数外、所有class外。
局部变量:函数内、class的方法(类方法、静态方法、实例方法)内,且变量前面没有修饰。
类变量:class内,不在class的任何方法内。
实例变量:class的方法内,使用self修饰。

1.1.全局变量与局部变量同名

如果在print_str函数中没有使用global关键字显式引用全局变量,在print_str函数中将优先使用局部变量,而不是全局变量。

firstValue = "Hello World"
def print_str():
    firstValue = "hi man"
    print(firstValue)  #注意:这里的firstValue调用的是局部变量firstValue,在方法中直接覆盖掉同名的全局变量firstValue

1.2.省略global关键字

当你的函数里只读取全局变量的值,此时可省略global,Python解释器明白你访问的是全局变量。

first = 100 #first是全局变量
def my_hello():
    print(first) #只是访问(读取)全局变量first的值,无需global修饰(加上global更规范)

1.3.不可省略global关键字

name = "员外"
def change_name():
    global name
    name = name + "非常牛"

上面的代码必须加上global修饰变量,说明先访问全局变量name的值,然后再相加,最后赋值全局变量,如果没有global修饰,Python解释器会无法找到name,因为此时Python解释器认为name为局部变量,应该先创建,一般会看到下午的Error,表示未创建局部变量。

my_name = "员外"
def baby():
    my_name = "四大才子"
    print(my_name) #注意:此处访问的是局部变量my_name,全局变量my_name并没有改变
baby()
print(my_name)

2.多线程

由于Python内一切皆对象,所以即使简单的全局数据,也不具备原子特性,需要加锁,保证数据读取正常。
由于引入了 全局解释器锁 GIL(global interpreter lock),以保证同一时间只有一个字节码在运行,这样就不会因为没用事先编译,而引发资源争夺和状态混乱的问题了。看似 “十全十美” ,但,这样做,就意味着**多线程执行时,会被 GIL 变为单线程,无法充分利用硬件资源。**希望通过多线程来提升运行效率,可能无法做到。ProcessPoolExecutor 并非万能的,它比较适合于 数据关联性低,且是 计算密集型 的场景。如果数据关联性强,就会出现进程间 “通信” 的情况,可能使好不容易换来的性能提升化为乌有。Python多线程类似于单核多线程,多线程有两个好处:CPU并行,IO并行,单核多线程相当于自断一臂。

1.I/O 密集型的任务,采用 Python 的多线程完全没用问题,可以大幅度提高执行效率。
2.对于计算密集型任务,要看数据依赖性是否低,如果低,采用 ProcessPoolExecutor 代替多线程处理,可以充分利用硬件资源。
3.如果数据依赖性高,可以考虑将关键的地方该用 C 来实现,一方面 C 本身比 Python 更快,另一方面,C 可以之间使用更底层的多线程机制,而完全不用担心受 GIL 的影响。
4.大部分情况下,对于只能用多线程处理的任务,不用太多考虑,直接利用 Python 的多线程机制,不用考虑太多。

3.多进程

如上文所言,如果任务之间的关联不大,可以使用多进程来实现硬件使用效率的提升。实现多进程的三种方式。

3.1.subprocess

如果单独调用一个外部进程程序,可以采用subprocess.Popen的方法,如下所示:

import os
import subprocess
import shutil
import uuid

# 使用命令,压缩、加密文件夹
def Compress7zFileby7z(srcFolder, dstFile7z, pw):
    loc_7z_fmt = r"{}\7-Zip\7z.exe"  # 7zip开源压缩工具的可执行文件路径
    rootpath = os.path.dirname(__file__);
    loc_7z = loc_7z_fmt.format(rootpath);
    archive_command_str = "";
    if pw == "":
        archive_command_str = "\"" + loc_7z + "\"" + " a " + "\"" + dstFile7z + "\"" + " " + "\"" + srcFolder.__str__() + "\""  # 编辑命令行
    else:
        archive_command_str = "\"" + loc_7z + "\"" + " a " + "\"" + dstFile7z + "\"" + " -p" + pw + " " + "\"" + srcFolder.__str__() + "\""  # 编辑命令行
    #print(archive_command_str);
    ex = subprocess.Popen(archive_command_str, stdout=subprocess.PIPE, shell=True);
    out, err = ex.communicate();
    status = ex.wait();

3.2.Process

process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。

Process(group=None, target=None, name=None, args=(), kwargs={})​
1 group——参数未使用,值始终为None
2 target——表示调用对象,即子进程要执行的任务
3 args——表示调用对象的位置参数元组,args=(1,2,‘egon’,)
4 kwargs——表示调用对象的字典,kwargs={‘name’:‘egon’,‘age’:18}
5 name——为子进程的名称

from multiprocessing import Process
import random
import time

def mail(name, age):
    count = random.random();
    print(f'给{age}岁的{name}发了一封邮件!延迟{count}秒');
    time.sleep(count);  # 模拟网络延迟
    """
    多个进程同时运行(注意,子进程的执行顺序不是根据启动顺序决定的)
    """

if __name__ == '__main__':
    info_list = [('小杨', 18), ('鲍勃', 20), ('艾伦', 55)]
    jo = []
    for info in info_list:
        obj = Process(target=mail, args=info);
        obj.start();
        jo.append(obj);

    # 将所有的子进程全部放入jo列表,在循环join所有子进程,就能等待所有子进程结束后在做操作
    for o in jo:
        o.join();#等待进程执行完毕

    # 所有的子进程结束的操作
    print('全部发送完毕')

属性

obj.daemon:默认值为False,如果设为True,代表obj为后台运行的守护进程,当obj的父进程终止时,obj也随之终止,并且设定为True后,obj不能创建自己的新进程,必须在obj.start()之前设置。
obj.name:进程的名称。
obj.pid:进程的pid。
obj.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)。
obj.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)。

方法

obj.start():启动进程,并调用该子进程中的obj.run()
obj.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
obj.terminate():强制终止进程obj,不会进行任何清理操作,如果obj创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果obj还保存了一个锁那么也将不会被释放,进而导致死锁
obj.is_alive():如果obj仍然运行,返回True
obj.join([timeout]):主线程等待obj终止(强调:是主线程处于等的状态,而obj是处于运行的状态)。timeout是可选的超时时间,需要强调的是,obj.join只能join住start开启的进程,而不能join住run开启的进程

注意:

由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。使用if name == ‘main’:,这个if语句中的语句将不会在导入时被调用。
进程之间是不共享全局变量的。

3.2.进程池

当需要创建的⼦进程数量不多时, 可以直接利⽤multiprocessing.Process动态生成多个进程, 但如果要创建很多进程时,⼿动创建的话⼯作量会非常大,此时就可以⽤到multiprocessing模块提供的Pool去创建一个进程池。
multiprocessing.Pool常⽤函数:

apply_async(func, args, kwds):使⽤⾮阻塞⽅式调⽤func(任务并⾏执⾏),args为传递给func的参数列表,kwds为传递给func的关键字参数列表
apply(func, args, kwds):使⽤阻塞⽅式调⽤func,必须等待上⼀个进程执行完任务后才能执⾏下⼀个进程,了解即可,几乎不用
close():关闭Pool,使其不再接受新的任务 terminate():不管任务是否完成,⽴即终⽌
join():主进程阻塞,等待⼦进程的退出,必须在close或terminate之后使⽤

进程间共享数据-multiprocessing.Manager
Python中进程间共享数据,处理基本的queue,pipe和value+array外,还提供了更高层次的封装。使用multiprocessing.Manager可以简单地使用这些高级接口。Manager()返回的manager对象控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问。从而达到多进程间数据通信且安全。Manager支持的类型有list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue, Value和Array。

使用范例
初始化Pool时,可以指定⼀个最⼤进程数,当有新的任务提交到Pool中时,如果进程池还没有满,那么就会创建⼀个新的进程⽤来执⾏该任务,但如果进程池已满(池中的进程数已经达到指定的最⼤值),那么该任务就会等待,直到池中有进程结束才会创建新的进程来执⾏。

import multiprocessing
from multiprocessing import Pool
import time

def work(i,return_dict):
    print("work'{}'执行中......".format(i), multiprocessing.current_process().name, multiprocessing.current_process().pid);
    time.sleep(0.2);
    print("work'{}'执行完毕......".format(i));
    return_dict[str(i)] = multiprocessing.current_process().pid;

if __name__ == '__main__':
    manager = multiprocessing.Manager();
    return_dict = manager.dict();

    # 创建进程池
    # Pool(3) 表示创建容量为3个进程的进程池
    pool = Pool(5);
    for i in range(10):
        # 利⽤进程池同步执⾏work任务,进程池中的进程会等待上⼀个进程执行完任务后才能执⾏下⼀个进程
        # pool.apply(work, (i, ))
        # 使⽤异步⽅式执⾏work任务
        pool.apply_async(work, (i, return_dict));

    # 进程池关闭之后不再接受新的请求
    pool.close();
    # 等待po中所有子进程结束,必须放在close()后面, 如果使⽤异步⽅式执⾏work任务,主进程不再等待⼦进程执⾏完毕再退出!
    pool.join();

    # 最后的结果是多个进程返回值的集合
    keys=return_dict.keys();
    values=return_dict.values();
    print(keys);
    print(values);

总结
进程池比单个进程创建的好处在于,多了进程数量的管理,多个进程等待结束等进程集合管理功能模块。

3.3.ProcessPoolExecutor

ProcessPoolExecutor是concurrent.futures里面的一个多进程解决方案,对多进程进行了一些便利的封装。使用submit异步调用,异步调用: 提交/调用一个任务,不在原地等着,直接执行下一行代码。

# encoding=utf-8
from datetime import datetime
import time
import random
from concurrent.futures import ProcessPoolExecutor, wait, as_completed

def work(msg):
    def run():
        print('[Child-{}][{}]'.format(msg, datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')))

    # 模拟一个耗时任务
    time.sleep(random.randint(1, 5));
    run();
    return msg;

if __name__ == '__main__':
    # 进程池大小
    pool_size = 2
    # 进程池
    pool = ProcessPoolExecutor(pool_size)
    # 添加任务, 假设我们要添加6个任务,由于进程池大小为2,每次能只有2个任务并行执行,其他任务排队
    tasks = [];
    for i in range(6):
        # print(pool.submit(job3, i).result());#同步调用
        task = pool.submit(work, i);  # 异步调用
        tasks.append(task);

    # 等待任务执行完, 也可以设置一个timeout时间
    for task in as_completed(tasks):  # 完成一个返回一个
        print(task.result());

    wait(tasks)  # 所有对象完成
    for task in tasks:
        print(task.result());

    # 结束
    print('main process done');

4.作者答疑

如有疑问,敬请留言。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值