python-多线程/多进程

多线程

多进程       

       最近同事接手我维护的打包项目时, 使用了Edit Configurations来配置运行参数, 测试的时候直接点击运行按钮。最开始我接手时每次修改代码都要push到git仓库, 然后到打包机脚本目录下pull最新代码, 再使用job打包验证。每次都是要经历这么痛苦的过程, 后来我优化了下, 将打包机上的命令复制保存起来, 每次修改脚本后就将保存的命令在pycharm控制台执行来验证修改效果。 同事这个方法省去了打包命令的拷贝过程, 在主打提效的部门这当然算一个提效点, 周末在家我便想着打包机一直这么慢, 插件工程打包的过程能否像java一样使用多线程 + CountDownLatch的方式来实现呢?

        因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。多线程的并发在Python中就是一个美丽的梦。

        GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

线程执行过程中,计算时间分为两部分:

  • CPU计算
  • 占用CPU

不需要CPU计算,不占用CPU,等待IO返回,比如recv(), accept(), sleep()等操作,具体操作就是比如访问cache、RPC调用下游service、访问DB,等需要网络调用的操作。那么如果计算时间占50%, 等待时间50%,那么为了利用率达到最高,可以开2个线程; 假如工作时间是2秒, CPU计算完1秒后,线程等待IO的时候需要1秒,此时CPU空闲了,这时就可以切换到另外一个线程,让CPU工作1秒后,线程等待IO需要1秒,此时CPU又可以切回去,第一个线程这时刚好完成了1秒的IO等待,可以让CPU继续工作,就这样循环的在两个线程之前切换操作。

那么如果计算时间占20%, 等待时间80%,那么为了利用率达到最高,可以开5个线程:
可以想象成完成任务需要5秒,CPU占用1秒,等待时间4秒,CPU在线程等待时,可以同时再激活4个线程,这样就把CPU和IO等待时间,最大化的重叠起来

抽象一下,计算线程数设置的公式就是:
N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
由于有GIL的影响,python只能使用到1个核,所以这里设置N=1。

线程同步问题

多线程实现同步有四种方式:

锁机制,信号量,条件判断和同步队列。

下面我主要关注两种同步机制:锁机制和同步队列。

锁机制

from time import sleep
import threading
from threading import Thread, Lock

class Account(object):

    def __init__(self):
        self._balance = 0
        # self._lock = Lock()

    def deposit(self, money):
        # 先获取锁才能执行后续的代码
        # self._lock.acquire()
        try:
            threadLock.acquire()
            new_balance = self._balance + money
            sleep(0.01)
            self._balance = new_balance
            threadLock.release()
        finally:
            pass
            # 在finally中执行释放锁的操作保证正常异常锁都能释放
            # self._lock.release()

    @property
    def balance(self):
        return self._balance

class AddMoneyThread(threading.Thread):

    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)

def main():
    account = Account()
    threads = []
    for _ in range(100):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        print(t.name)
        t.start()
    for t in threads:
        t.join()
    print('账户余额为: ¥%d元' % account.balance)

if __name__ == '__main__':
    # 锁机制
    # threading的Lock类,用该类的acquire函数进行加锁,用realease函数进行解锁
    threadLock = threading.Lock()
    main()

同步队列

python2.x中提供的Queue, Python3.x中提供的是queue

Python的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步

def func(msg):
    build_commands = 'cd %s && pwd && chmod +x gradlew && ./gradlew clean assemble%s' % (
        msg, "Debug")
    fetch_process()
    code = subprocess.call(build_commands, shell=True)
    fetch_process()
    return code

def test_thread(projects):
    with ThreadPoolExecutor(4) as executor:
        for project in projects:
            executor.submit(func, project)
        # with ThreadPoolExecutor(3) as execut:
        #     execut.map(func, project)
import queue
import threading
import time

class WorkManager(object):
    def __init__(self, work_num=50, thread_num=2):
        self.work_queue = queue.Queue()
        self.threads = []
        self.__init_work_queue(work_num)
        self.__init_thread_pool(thread_num)
    
    """
       初始化线程
    """
    def __init_thread_pool(self, thread_num):
        for i in range(thread_num):
            self.threads.append(Work(self.work_queue))
    
    """
       初始化工作队列
    """
    def __init_work_queue(self, jobs_num):
        for i in range(jobs_num):
            self.add_job(do_job, i)
    
    """
       添加一项工作入队
    """
    def add_job(self, func, *args):
        self.work_queue.put((func, list(args)))  # 任务入队,Queue内部实现了同步机制
    
    """
       等待所有线程运行完毕
    """
    def wait_allcomplete(self):
        for item in self.threads:
            if item.isAlive():
                item.join()

class Work(threading.Thread):
    def __init__(self, work_queue):
        threading.Thread.__init__(self)
        self.work_queue = work_queue
        self.start()
    
    def run(self):
        # 死循环,从而让创建的线程在一定条件下关闭退出
        while True:
            try:
                do, args = self.work_queue.get(block=False)  # 任务异步出队,Queue内部实现了同步机制
                do(args)
                self.work_queue.task_done()  # 通知系统任务完成
            except:
                break
            
            # 具体要做的任务

def do_job(args):
    time.sleep(0.1)  # 模拟处理时间
    print(threading.current_thread())
    print(list(args))

if __name__ == '__main__':
    start = time.time()
    work_manager = WorkManager(30, 5)
    work_manager.wait_allcomplete()
    end = time.time()
    print("cost all time: %s" % (end - start))

多进程

import multiprocessing
import subprocess
import time

MAX = 100000000

def func_params(mydict, index):
    mydict["index" + str(index)] = "aaaaaa"  # 子进程改变dict, 主进程跟着改变

def test_multiprocessing():
    """多进程值共享"""
    with multiprocessing.Manager() as MG:  # 重命名
        my_dict = MG.dict()  # 主进程与子进程共享字典
        for i in [1, 2, 3]:
            p = multiprocessing.Process(target=func_params, args=(my_dict, i))
            p.start()
            p.join()   
        print(my_dict)

def func_return(msg):
    """多进程返回值"""
    print("func." + msg)
    ret_code = subprocess.call('adb devices', shell=True)
    print(ret_code)
    return "\ndone " + msg

def func(msg):
    for i in range(0, int(MAX / 4)):
        print(msg)
        pass

def test():
    pool = multiprocessing.Pool()
    result = []
    for i in range(0, 4):
        msg = "hello %d " % i
        """for循环MAX次耗时 分而治之思想"""
        result.append(pool.apply_async(func, (msg,)))
        """返回值"""
        # result.append(pool.apply_async(func_return(), (msg,)))
    pool.close()
    pool.join()
    
    # for res in result:
    #     print(res.get())
    # print("Sub-process(es) done.")

def test_single():
    """多进程对照组"""
    for i in range(0, MAX):
        pass

if __name__ == "__main__":
    # test_multiprocessing()
    start_time = time.time()
    """比较for循环MAX次耗时"""
    test_single()
    # test()
    elapse_time = time.time() - start_time
    '''对比发现使用多进程比单进程耗时减半'''
    print(elapse_time)

       通过python执行shell脚本

os.system( )
该函数的语法为:

os.system(cmd)
 
参数cmd: 要执行的命令
该函数返回命令执行结果的返回值,system()函数在执行过程中进行了以下三步操作: 
1.fork一个子进程; 

2.在子进程中调用exec函数去执行命令; 

3.在父进程中调用wait(阻塞)去等待子进程结束。 对于fork失败,system()函数返回-1。 

由于使用该函数经常会莫名其妙地出现错误,但是直接执行命令并没有问题,所以一般建议不要使用。官方建议使用subprocess模块来生成新进程并获取结果是更好的选择。

注意

1、使用os.system()用来执行cmd指令后返回结果为1表示执行成功,返回0表示失败;

2、os.system()是简单粗暴的执行cmd指令,如果想获取在cmd输出的内容,是没办法获到的;

3、os.system()调用外部系统命令,返回命令结果码,但是无法获取命令执行输出结果。

os.popen( )
该函数的语法如下:
popen(cmd, mode='r', buffering = -1)
 
参数:   
cmd:要执行的命令。
mode:打开文件的模式,默认为'r',用法与open()相同。
buffering:(可选参数)0意味着无缓冲;1意味着行缓冲;其它正值表示使用参数大小的缓冲。负的bufsize意味着使用系统的默认值
1、这个方法会打开一个管道,返回结果是一个连接管道的文件对象,该文件对象的操作方法同open(),可以从该文件对象中读取返回结果。如果执行成功,不会返回状态码,如果执行失败,则会返回错误信息。这里官方也表示subprocess模块已经实现了更为强大的subprocess.Popen()方法。

2、如果想获取控制台输出的内容,那就用os.popen() 的方法了,popen返回的是一个file对象,跟open打开文件一样操作了,"r"是以读的方式打开。

cmd = os.popen("adb devices","r") #将返回的结果赋于一变量,便于程序的处理。
Cmd_result = cmd.read()
cmd.close()
print(Cmd_result) # cmd输出结果

注:
1、os.system(cmd)的返回值只会有0(成功),1,2。返回值是脚本的退出状态码

2、os.popen(cmd)会把执行的cmd的输出作为值返回。返回值是脚本执行过程中的输出内容

3、os.popen()可以实现一个'管道',从这个命令获取的值可以继续被调用。而os.system不同,它只是调用,调用完后自身退出,可能返回个0

4、os.popen()好处在于:将返回的结果赋于一变量,便于程序的处理。

subprocess模块
1、优先介绍subprocess模块的是由于该模块可以替代旧模块的方法,如os.system()、os.popen()等,推荐使用。subporcess模块可以调用外部系统命令来创建新子进程,同时可以连接到子进程的nput/output/error管道上,并得到子进程的返回值。

2、subprocess模块主要有call()、check_call()、check_output()、Popen()函数,简要描述如下:

 

subprocess模块
1、优先介绍subprocess模块的是由于该模块可以替代旧模块的方法,如os.system()、os.popen()等,推荐使用。subporcess模块可以调用外部系统命令来创建新子进程,同时可以连接到子进程的nput/output/error管道上,并得到子进程的返回值。

2、subprocess模块主要有call()、check_call()、check_output()、Popen()函数,简要描述如下


subprocess.Popen 该函数的语法如下:

subprocess.Popen(args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags)  
 

参数说明: 

args:要调用的外部系统命令。参数args可以是字符串或者序列类型(如:list,元组),用于指定进程的可执行文件及其参数。如果是序列类型,第一个元素通常是可执行文件的路径。

bufsize:默认值为0, 表示不缓存,。为1表示行缓存,。其他正数表示缓存使用的大小,,负数-1表示使用系统默认的缓存大小。

stdin、stdout、stdout:分别表示标准输入、标准输出和标准错误。其值可以为PIPE、文件描述符和None等。默认值为None,表示从父进程继承。

shell
Linux:参数值为False时,Linux上通过调用os.execvp执行对应的程序。为True时,Linux上直接调用系统shell来执行程序。
Windows:参数shell设为true,程序将通过shell来执行。

executable:用于指定可执行程序。一般情况下我们通过args参数来设置所要运行的程序。如果将参数shell设为 True,executable将指定程序使用的shell。在windows平台下,默认的shell由COMSPEC环境变量来指定。

preexec_fn:只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用

cwd:设置子进程当前目录

env:env是字典类型,用于指定子进程的环境变量。默认值为None,表示子进程的环境变量将从父进程中继承。

Universal_newlines:不同操作系统下,文本的换行符是不一样的。如:windows下用’/r/n’表示换,而Linux下用 ‘/n’。如果将此参数设置为True,Python统一把这些换行符当作’/n’来处理。

subprocess.PIPE:在创建Popen对象时,subprocess.PIPE可以初始化stdin, stdout或stderr参数,表示与子进程通信的标准流。
subprocess.STDOUT:创建Popen对象时,用于初始化stderr参数,表示将错误通过标准输出流输出。

subprocess.call()

函数原型:call(*popenargs, **kwargs)。

call( )调用外部系统命令执行,并返回程序执行结果码。执行成功时返回1,未成功返回0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值