小编在前两天开通了一个 Python 金融的专栏,顺便用 TuShare 下载了几只 A股的数据,有兴趣的小伙伴可以去看一下:

多多教Python:Python 金融: TuShare API 获取股票数据 (1)​zhuanlan.zhihu.com

大概下载了十几只股票,然后在接下来的教程中做数据清洗和整理。但是一只一只股票按顺序做下来速度会很慢,所以在这之前我们先来讲一下如何在 Python 做多线程运算,来帮助我们提高多任务处理的速度。

这篇教程讲建立在之前两篇的基础之上,所以有兴趣的小伙伴可以去阅读一下:多多教Python:Python 基本功: 6. 第一个完整的程序​zhuanlan.zhihu.com多多教Python:Python 基本功: 10. 面对对象-类 Class​zhuanlan.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. 多线程运算提速相关推荐

  1. python携程多核_python 协程

    最近对Python中的协程挺感兴趣,这里记录对协程的个人理解. 要理解协程,首先需要知道生成器是什么.生成器其实就是不断产出值的函数,只不过在函数中需要使用yield这一个关键词将值产出.下面来看一个 ...

  2. python协程实时输出_python协程

    不知道你有没有被问到过有没有使用过的python协程? 协程是什么? 协程是一种用户态轻量级,是实现并发编程的一种方式.说到并发,就能想到了多线程 / 多进程模型,是解决并发问题的经典模型之一. 但是 ...

  3. python 协程原理_Python协程greenlet实现原理

    greenlet是stackless Python中剥离出来的一个项目,可以作为官方CPython的一个扩展来使用,从而支持Python协程.gevent正是基于greenlet实现. 协程实现原理 ...

  4. python 字节码 优化_python,_Python 字节码优化问题,python - phpStudy

    Python 字节码优化问题 问题背景: Python在执行的时候会加载每一个模块的PyCodeObject,其中这个对象就包含有opcode,也就是这个模块所有的指令集合,具体定义在源码目录的 /i ...

  5. python 冒泡排序及优化_Python冒泡排序及优化

    一.冒泡排序简介 冒泡排序(Bubble Sort)是一种常见的排序算法,相对来说比较简单. 冒泡排序重复地走访需要排序的元素列表,依次比较两个相邻的元素,如果顺序(如从大到小或从小到大)错误就交换它 ...

  6. python 协程库_python协程概念

    什么是协程 协程是单线程下的并发,又称微线程,纤程.它是实现多任务的另一种方式,只不过是比线程更小的执行单元.因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程.英文名 ...

  7. python主线程执行_python 并发执行之多线程

    正常情况下,我们在启动一个程序的时候.这个程序会先启动一个进程,启动之后这个进程会拉起来一个线程.这个线程再去处理事务.也就是说真正干活的是线程,进程这玩意只负责向系统要内存,要资源但是进程自己是不干 ...

  8. python释放类对象_Python 基本功: 10. 面对对象-类 Class

    虽然 Python 可以写函数式编程,但是本质上是一门面对对象编程语言 (object-oriented programming language),简称 oop.面对对象编程是把代码包装成一个对象 ...

  9. python 协程爬虫_Python爬虫进阶教程(二):线程、协程

    简介 线程 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID.程序计数器.寄存器集合和堆栈共同组成.线程的引入减小了程序并发执行时的开销,提高了操作系统的并发 ...

最新文章

  1. JCheckBox 默认选择_[注册表] 将Windows 10默认应用程序设置页面添加到桌面右键菜单中...
  2. 分子生物学之蛋白质与氨基酸
  3. sgi allocate
  4. 透视映射和射影映射的关系 Perspective and Projectivity
  5. opengl层次建模_层次建模简介
  6. 第 21 章 中介者模式
  7. java数据结构编写二叉树_java 数据结构与算法 BinaryTree二叉树编写
  8. 怎么用命令开远程主机的telnet服务1
  9. 一些抄来的冷知识...
  10. 周立功上位机获取CAN通讯数据解析 V2.0
  11. filenet分布式部署
  12. cve_2019_0708 bluekeep复现踩坑
  13. 注塑工艺需要考虑的7个因素
  14. 51单片机程序存储器和数据存…
  15. 口袋里只有一百块钱,也要活出十个亿的气势
  16. 南京邮电大学操作系统实验二:线程的互斥与同步
  17. MySQL中IN对NULL的处理
  18. 计算机的用途越来越广泛英语翻译,英语翻译在计算机技术飞速发展的今天,网络技术日趋成熟,在各行各业中得到越来越广泛的应用,企业管理信息化使企业能适用瞬息万...
  19. 苏菲的世界-part2
  20. 网易游戏面试经验(三)

热门文章

  1. Fast construction of FM-index for long sequence reads
  2. Linux环境编译qtmqtt,qtmqttclient
  3. php7 v8js,Centos 7PHP7.0 安装V8JS扩展几乎都能安装成功
  4. tensorflow中keep_prob的修改方法
  5. Qt安装及配置_很详细(附下载网址)
  6. LeetCode 11. Container With Most Water--Java 解法--困雨水简单版
  7. LeetCode 468. Validate IP Address--笔试题--Python解法
  8. Java调用PHP,跑PHP代码
  9. 数据库字符mysql_MySQL数据库之字符函数详解
  10. java程序设计输入输出实验_20145320《Java程序设计》第五次实验报告