多线程
多进程
最近同事接手我维护的打包项目时, 使用了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