python多程优化_Python 基本功: 13. 多线程运算提速
小编在前两天开通了一个 Python 金融的专栏,顺便用 TuShare 下载了几只 A股的数据,有兴趣的小伙伴可以去看一下:
多多教Python:Python 金融: TuShare API 获取股票数据 (1)zhuanlan.zhihu.com
大概下载了十几只股票,然后在接下来的教程中做数据清洗和整理。但是一只一只股票按顺序做下来速度会很慢,所以在这之前我们先来讲一下如何在 Python 做多线程运算,来帮助我们提高多任务处理的速度。
这篇教程讲建立在之前两篇的基础之上,所以有兴趣的小伙伴可以去阅读一下:多多教Python:Python 基本功: 6. 第一个完整的程序zhuanlan.zhihu.com多多教Python:Python 基本功: 10. 面对对象-类 Classzhuanlan.zhihu.com
教程需求:Mac OS (Windows, Linux 会略有不同)
安装了 Python 3.0 版本以上, PyCharm, Microsoft Office Excel
多线程 Multi-Threading
多线程是指在一个进程中,允许几段代码并发式的同时运行。Python 的多线程运算就是利用了这一点,可以让代码的运行方法更加丰富有效。这里需要用到的一个库叫 Threading,这个库可以直接调用其中的函数,或者通过继承类来实现,下面我们来分别通过这两个方法来对运算进行提速。函数多线程
import threading
def func(times, name, ret):
for i in range(times):
print(name + ' run: ' + str(i))
ret[name] = name + " finished with " + str(times) + " times printed"
return
if __name__ == '__main__':
thread_pool = []
ret = {}
th_1 = threading.Thread(target=func, args=[3, 'th_1', ret], name='th_1')
th_2 = threading.Thread(target=func, args=[5, 'th_2', ret], name='th_2')
thread_pool.append(th_1)
thread_pool.append(th_2)
for th in thread_pool:
th.start()
for th in thread_pool:
th.join()
print(ret)
这里我们首先定义了一个函数,func,并且运用 threading 库来做多线程式的函数调用。随后在主程序 main() 内部,我们引入了几个多线程运算的核心概念:线程池 Thread Pool: 线程池是管理多线程的一个容器,帮助分配资源,完成线程的生命周期。你可以自己创建一个容器来管理,或者调用其他容器来管理。这里的线程池是一个列表 List(), 给予变量 thread_pool。
线程类 Thread Class: threading.Thread 是一个线程库里的线程类,帮助你管理,运行目标函数。这里的第一个参数 target= 是目标函数,如果你阅读了 多多教Python:Python 基本功: 7. 介绍函数,就会了解函数也可以当做参数被传递;第二个参数是目标函数的参数,这里是一个列表;第三个参数是线程类的名字,在你自己定义了之后可以在之后管理线程类的时候用到。
开始 Start: 在创建了线程类之后,你需要通过 start() 函数来开始运行你的目标函数。这里通过对线程池列表的循环来一一启动其目标函数。
非阻塞 Non-Blocking: 线程类的 start() 函数是一个非阻塞函数,意思是当目标函数启动了之后,程序就返回了,而不是等到目标函数结束再返回。所以这里可以实现对线程池的线程同时启动,而不是等待前一个结束再开始下一个。
加入 Join: 第二次循环线程池列表的时候,调用了线程类的 join() 函数。join() 函数会等待目标函数运行结束之后返回,是一个阻塞 (Blocking) 函数。目前教程介绍的都是阻塞型函数,在之后的教程中讲到 异步(Asynchronous)的时候会了解如何写非阻塞函数。
返回值 Return:如果你想在多线程运算中获得返回值,有不同的办法,这里介绍其中一种:利用传入的参数来保存返回值。这里在参数列表里传入了一个字典,每一个目标函数把自己的返回值写入这个字典。在 join() 返回之后,就可以查看字典里的返回数值。
th_1 run: 0
th_1 run: 1
th_1 run: 2
th_2 run: 0
th_2 run: 1
th_2 run: 2
th_2 run: 3
th_2 run: 4
{'th_1': 'th_1 finished with 3 times printed', 'th_2': 'th_2 finished with 5 times printed'}
Process finished with exit code 0
这是程序运行完成之后的输出。当你们自己运行的时候,会发现输出内容顺序不一样。在教程的后半段我们会介绍一个概念:资源竞争 (Resource Contention)来解释这个现象。继承类多线程
除了定义目标函数,然后传入线程类的方法来做多线程之外,还可以通过直接继承线程类,然后覆盖继承类函数的方法。下面来一个示例:
import threading
class ThreadChild(threading.Thread):
def __init__(self, times, name, ret):
# 扩写父类的初始化,首先调用父类的初始化
threading.Thread.__init__(self)
self.times = times
self.name = name
self.ret = ret
return
def run(self):
# 覆盖重写函数 run
for i in range(self.times):
print(self.name + ' run: ' + str(i))
self.ret[self.name] = self.name + " finished with " + str(self.times) + " times printed"
return
if __name__ == '__main__':
thread_pool = []
ret = {}
th_1 = ThreadChild(times=3, name='th_1', ret=ret)
th_2 = ThreadChild(times=5, name='th_2', ret=ret)
thread_pool.append(th_1)
thread_pool.append(th_2)
for th in thread_pool:
th.start()
for th in thread_pool:
th.join()
print(ret)
这里在继承过程中,运用到了两个函数,初始化 init 和运行 run:初始化 init: 通常继承线程类会扩写父类的初始化,来传递参数等。因为这里是扩写,所以依然需要调用执行父类的初始化,也就是这里的初始化第一行做的。
运行 run: 这是一个必须要覆盖的函数。启动线程调用的 start() 函数就是运行这个函数,这里是需要运行的核心代码。
资源竞争 Resource Contention
多线程本质上是在一个 Python 程序里做的一个资源再分配,把几段代码的运行顺序进行先后调整达到 CPU 资源利用的最大化。但是这么做的一个缺点就是资源竞争,意思就是有可能几段代码同时在读写一个参数的时候,把这个参数的数值搞混。所以在多线程共享资源的情况下,需要在共享资源外部添加锁 Lock。
import threading
class ThreadChild(threading.Thread):
def __init__(self, times, name, ret, ret_lock):
threading.Thread.__init__(self)
self.times = times
self.name = name
self.ret = ret
self.ret_lock = ret_lock
return
def run(self):
for i in range(self.times):
print(self.name + ' run: ' + str(i))
self.ret_lock.acquire()
# 进入有可能竞争的共享资源,锁住
self.ret[self.name] = self.name + " finished with " + str(self.times) + " times printed"
# 共享资源读写结束,开锁
self.ret_lock.release()
return
if __name__ == '__main__':
thread_pool = []
ret = {}
ret_lock = threading.Lock()
th_1 = ThreadChild(times=3, name='th_1', ret=ret, ret_lock=ret_lock)
th_2 = ThreadChild(times=5, name='th_2', ret=ret, ret_lock=ret_lock)
thread_pool.append(th_1)
thread_pool.append(th_2)
for th in thread_pool:
th.start()
for th in thread_pool:
th.join()
print(ret)锁类 Lock: 在线程中需要读写一个共享资源的时候,通过锁类来锁住资源,防止另外的线程读写修改。这里在主程序中创建了一个锁 ret_lock, 并且传入了两个线程中。这个锁本身是可以在多个线程中共享的,因为锁本身不存在资源竞争的问题,否则就没有意义了。
锁住 Acquire: Acquire() 是锁的一个函数,表示获得资源,也可以表示锁住资源。在这个函数之后的代码将只能被一个线程执行下去,直到开锁。
开锁 Release: Release() 是锁的一个函数,表示释放资源,也可以表示开锁。开锁之后,被锁住的资源和代码行又可以重新被其他线程读写,运行。
Python 中有一种变量叫全局变量 (Global Variable),这种变量通常在文件的开头定义,然后可以在任意地方调用,不需要以参数的方式传入。比方说:
import pandas as pd
glob_a = 100
这里的 Pandas 库 pd, 和 glob_a 变量都是全局变量,可以在下面的任意地方调用获取。全局变量也是 Python 程序的共享资源,在多线程运算中,接触到全局变量的地方都需要加锁,或者你也可以直接把锁变成全局变量:
import threading
glob_lock = threading.Lock()
glob_a = 100
def increase_by_a(num): # 不好的例子,虽然对全局变量加了锁,但是在函数内部运用了全局变量
glob_lock.acquire()
result = glob_a + num
glob_lock.release()
return result
if __name__ == '__main__':
print(increase_by_a(100)) # 返回 200
glob_lock.acquire()
glob_a += 1
glob_lock.release()
print(increase_by_a(100)) # 返回 201,glob_a的改变破坏了函数 increase_by_a 的一致性
这里的锁是在全局变量中的,并且锁住了一个全局变量a。但是我们会发现,两次调用 increase_by_a 函数的过程中,返回数值因为一个全局变量而变化了,尽管我们在函数传入的参数是一致的,这就破坏了函数的一致性:函数的一致性:每次在同一个函数传入相同的参数,返回相同的结果
虽然有的函数天生就是没有一致性的,例如:
import numpy as np
print(np.random.randint(10)) #返回一个 0-9 的随机整数
这里 randint() 是 Numpy 库里的一个获取随机整数的函数,每次传入一个整数,返还的是随机的小于这个数的整数。
然而大多数情况我们需要的是有一致性的函数,一致性的函数可靠,稳定,可以给一个大型项目提供稳固的支撑,并且配合单元测试做不同的场景模拟。所以结论就是:
尽量在函数中避免调用全局变量,或全局锁,以防破坏函数的一致性。
提速 Accelerate
了解了多线程的用法,现在我们来看看多线程给我们带来的速度提升:
import threading
import math
import datetime
class ThreadChild(threading.Thread):
def __init__(self, num_list, name, ret, ret_lock):
threading.Thread.__init__(self)
self.num_list = num_list
self.name = name
self.ret = ret
self.ret_lock = ret_lock
return
def run(self):
result = 0
for num in self.num_list:
result += math.sqrt(num * math.tanh(num) / math.log2(num) / math.log10(num))
self.ret_lock.acquire()
self.ret[self.name] = result
self.ret_lock.release()
return
if __name__ == '__main__':
thread_pool = []
ret = {}
ret_lock = threading.Lock()
th_1 = ThreadChild(num_list=list(range(10, 3000000)), name='th_1', ret=ret, ret_lock=ret_lock)
th_2 = ThreadChild(num_list=list(range(3000000, 6000000)), name='th_2', ret=ret, ret_lock=ret_lock)
th_3 = ThreadChild(num_list=list(range(6000000, 9000000)), name='th_3', ret=ret, ret_lock=ret_lock)
thread_pool.append(th_1)
thread_pool.append(th_2)
thread_pool.append(th_3)
start_t = datetime.datetime.now()
for th in thread_pool:
th.start()
for th in thread_pool:
th.join()
final_result = sum(ret.values())
end_t = datetime.datetime.now()
elapsed_sec = (end_t - start_t).total_seconds()
print("多线程计算结果: " + "{:.1f}".format(final_result) + ", 共消耗: " + "{:.2f}".format(elapsed_sec) + " 秒")
ret.clear()
th_4 = ThreadChild(num_list=list(range(10, 9000000)), name='th_4', ret=ret, ret_lock=ret_lock)
start_t = datetime.datetime.now()
th_4.start()
th_4.join()
final_result = sum(ret.values())
end_t = datetime.datetime.now()
elapsed_sec = (end_t - start_t).total_seconds()
print("单线程计算结果: " + "{:.1f}".format(final_result) + ", 共消耗: " + "{:.2f}".format(elapsed_sec) + " 秒")
这里用了 datetime 模块来计算多线程 VS 单线程的计算时间,在运算中使用的是 i7-7700K, 4.2GHz CPU,这是跑分结果:
多线程计算结果: 1484922580.2, 共消耗: 3.48 秒
单线程计算结果: 1484922580.2, 共消耗: 3.64 秒
Process finished with exit code 0
从 3.64 秒的单线程,到多线程运算用的 3.48秒,提速了 4.6%。我们看到多线程的速度提升影响有限,原因是多线程本质上还是在一个进程里做的资源分配优化,还没有利用到多进程,多核心计算的能力。但是多线程的巨大优势是在遇到阻塞型函数,例如 API 调用,网络通信,文件读写的时候,可以不被网络速度,硬盘速度耽误了程序其他部分的运算。至于如何做到这些,在之后的教程会详细讲到。
小结:
多线程可以提高运算速度,还可以防止阻塞型函数影响程序的运行。但是真正运用到了当代多核计算能力的是多进程运算,在后面的教程中,小编带大家了解如何使用 Python 的多进程能力大幅提高计算能力。下面来介绍两篇外部的教程:
Python 官方的 Threading 文档:Python Thread 库docs.python.org
Python 锁 更加详细的图解:Python 多线程编程之 锁blog.csdn.net
python多程优化_Python 基本功: 13. 多线程运算提速相关推荐
- python携程多核_python 协程
最近对Python中的协程挺感兴趣,这里记录对协程的个人理解. 要理解协程,首先需要知道生成器是什么.生成器其实就是不断产出值的函数,只不过在函数中需要使用yield这一个关键词将值产出.下面来看一个 ...
- python协程实时输出_python协程
不知道你有没有被问到过有没有使用过的python协程? 协程是什么? 协程是一种用户态轻量级,是实现并发编程的一种方式.说到并发,就能想到了多线程 / 多进程模型,是解决并发问题的经典模型之一. 但是 ...
- python 协程原理_Python协程greenlet实现原理
greenlet是stackless Python中剥离出来的一个项目,可以作为官方CPython的一个扩展来使用,从而支持Python协程.gevent正是基于greenlet实现. 协程实现原理 ...
- python 字节码 优化_python,_Python 字节码优化问题,python - phpStudy
Python 字节码优化问题 问题背景: Python在执行的时候会加载每一个模块的PyCodeObject,其中这个对象就包含有opcode,也就是这个模块所有的指令集合,具体定义在源码目录的 /i ...
- python 冒泡排序及优化_Python冒泡排序及优化
一.冒泡排序简介 冒泡排序(Bubble Sort)是一种常见的排序算法,相对来说比较简单. 冒泡排序重复地走访需要排序的元素列表,依次比较两个相邻的元素,如果顺序(如从大到小或从小到大)错误就交换它 ...
- python 协程库_python协程概念
什么是协程 协程是单线程下的并发,又称微线程,纤程.它是实现多任务的另一种方式,只不过是比线程更小的执行单元.因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程.英文名 ...
- python主线程执行_python 并发执行之多线程
正常情况下,我们在启动一个程序的时候.这个程序会先启动一个进程,启动之后这个进程会拉起来一个线程.这个线程再去处理事务.也就是说真正干活的是线程,进程这玩意只负责向系统要内存,要资源但是进程自己是不干 ...
- python释放类对象_Python 基本功: 10. 面对对象-类 Class
虽然 Python 可以写函数式编程,但是本质上是一门面对对象编程语言 (object-oriented programming language),简称 oop.面对对象编程是把代码包装成一个对象 ...
- python 协程爬虫_Python爬虫进阶教程(二):线程、协程
简介 线程 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID.程序计数器.寄存器集合和堆栈共同组成.线程的引入减小了程序并发执行时的开销,提高了操作系统的并发 ...
最新文章
- JCheckBox 默认选择_[注册表] 将Windows 10默认应用程序设置页面添加到桌面右键菜单中...
- 分子生物学之蛋白质与氨基酸
- sgi allocate
- 透视映射和射影映射的关系 Perspective and Projectivity
- opengl层次建模_层次建模简介
- 第 21 章 中介者模式
- java数据结构编写二叉树_java 数据结构与算法 BinaryTree二叉树编写
- 怎么用命令开远程主机的telnet服务1
- 一些抄来的冷知识...
- 周立功上位机获取CAN通讯数据解析 V2.0
- filenet分布式部署
- cve_2019_0708 bluekeep复现踩坑
- 注塑工艺需要考虑的7个因素
- 51单片机程序存储器和数据存…
- 口袋里只有一百块钱,也要活出十个亿的气势
- 南京邮电大学操作系统实验二:线程的互斥与同步
- MySQL中IN对NULL的处理
- 计算机的用途越来越广泛英语翻译,英语翻译在计算机技术飞速发展的今天,网络技术日趋成熟,在各行各业中得到越来越广泛的应用,企业管理信息化使企业能适用瞬息万...
- 苏菲的世界-part2
- 网易游戏面试经验(三)
热门文章
- Fast construction of FM-index for long sequence reads
- Linux环境编译qtmqtt,qtmqttclient
- php7 v8js,Centos 7PHP7.0 安装V8JS扩展几乎都能安装成功
- tensorflow中keep_prob的修改方法
- Qt安装及配置_很详细(附下载网址)
- LeetCode 11. Container With Most Water--Java 解法--困雨水简单版
- LeetCode 468. Validate IP Address--笔试题--Python解法
- Java调用PHP,跑PHP代码
- 数据库字符mysql_MySQL数据库之字符函数详解
- java程序设计输入输出实验_20145320《Java程序设计》第五次实验报告