超简单的Python教程系列——第15篇:多线程

Posted 飞天程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超简单的Python教程系列——第15篇:多线程相关的知识,希望对你有一定的参考价值。

超简单的Python教程系列——第15篇:多线程_线程

在当今的现代世界,从社交媒体到智能设备,数据都是我们生活的核心。程序的性能取决于其经常通过网络操作和计算数据的能力。处理大量数据会出现问题;尤其是程序执行时间的增加会导致“阻塞”或“滞后”。

出于对程序的高效执行和日益复杂的多核操作系统/硬件架构的需要,编程语言试图更好地利用这种行为。“并发”一词的字面意思是“同时发生”。由于计算机可以同时运行多条指令,因此可以显著减少并发程序的执行时间。

Python有三个主要的操作系统概念交织在其并发模型中;即线程任务进程


什么是线程?你为什么想要它?

从本质上讲,Python是一种线性语言,但当你需要更多的处理能力时,线程模块非常方便。虽然Python中的线程不能用于并行CPU计算,但它非常适合I/O操作,如web抓取,因为处理器处于空闲状态,等待数据。

线程正在改变游戏规则,因为许多与网络/数据I/O相关的脚本将大部分时间用于等待来自远程源的数据。由于下载可能没有链接(即,抓取单独的网站),处理器可以并行地从不同的数据源下载,并在最后合并结果。对于CPU密集型进程,使用线程模块没有什么好处。

超简单的Python教程系列——第15篇:多线程_python_02

幸运的是,线程包含在标准库中:

import threading
from queue import Queue
import time

你可以使用​​target​​​作为可调用对象,使用​​args​​​将参数传递给函数,并​​start​​启动线程。

def testThread(num):
print num

if __name__ == __main__:
for i in range(5):
t = threading.Thread(target=testThread, arg=(i,))
t.start()

如果你以前从未见过​​if __name__ == __main__:​​,这基本上是一种确保嵌套在其中的代码仅在脚本直接运行(而不是导入)时运行的方法。


同一操作系统进程的线程将计算工作负载分布到多个内核中,如C++和Java等编程语言所示。通常,python只使用一个进程,从该进程生成一个主线程来执行运行时。由于一种称为全局解释器锁(global interpreter lock)的锁定机制,它保持在单个核上,而不管计算机有多少核,也不管产生了多少新线程,这种机制是为了防止所谓的竞争条件。

超简单的Python教程系列——第15篇:多线程_python教程_03

提到竞争,我想到了想到 NASCAR 和一级方程式赛车。让我们用这个类比,想象所有一级方程式赛车手都试图同时在一辆赛车上比赛。听起来很荒谬,对吧?,这只有在每个司机都可以使用自己的车的情况下才有可能,或者最好还是一次跑一圈,每次把车交给下一个司机。

超简单的Python教程系列——第15篇:多线程_python_04

这与线程中发生的情况非常相似。线程是从“主”线程“派生”的,每个后续线程都是前一个线程的副本。这些线程都存在于同一进程“上下文”(事件或竞争)中,因此分配给该进程的所有资源(如内存)都是共享的。例如,在典型的python解释器会话中:

>>> a = 8

在这里,​​a​​ 通过让内存中的某个任意位置暂时保持值 8 来消耗很少的内存 (RAM)。

到目前为止一切顺利,让我们启动一些线程并观察它们的行为,当添加两个数字​​x​​​时​​y​​:

import time
import threading
from threading import Thread

a = 8

def threaded_add(x, y):
# simulation of a more complex task by asking
# python to sleep, since adding happens so quick!
for i in range(2):
global a
print("computing task in a different thread!")
time.sleep(1)
#this is not okay! but python will force sync, more on that later!
a = 10
print(a)

# the current thread will be a subset fork!
if __name__ != "__main__":
current_thread = threading.current_thread()


# here we tell python from the main
# thread of execution make others
if __name__ == "__main__":

thread = Thread(target = threaded_add, args = (1, 2))
thread.start()
thread.join()
print(a)
print("main thread finished...exiting")
>>> computing task in a different thread!
>>> 10
>>> computing task in a different thread!
>>> 10
>>> 10
>>> main thread finished...exiting

两个线程当前正在运行。让我们把它们称为​​thread_one​​​和​​thread_two​​​。如果​​thread_one​​​想要用值10修改​​a​​​,而​​thread_two​​​同时尝试更新同一变量,我们就有问题了!将出现称为数据竞争的情况,并且​​a​​的结果值将不一致。

一场你没有看的赛车比赛,但从你的两个朋友那里听到了两个相互矛盾的结果!​​thread_one​​​告诉你一件事,​​thread two​​反驳了这一点!这里有一个伪代码片段说明:

a = 8
# spawns two different threads 1 and 2
# thread_one updates the value of a to 10

if (a == 10):
# a check

#thread_two updates the value of a to 15
a = 15
b = a * 2

# if thread_one finished first the result will be 20
# if thread_two finished first the result will be 30
# who is right?


到底是怎么回事?

Python是一种解释语言,这意味着它带有一个解释器——一个从另一种语言解析其源代码的程序!python中的一些此类解释器包括cpython、pypypy、Jpython和IronPython,其中,cpython是python的原始实现。

CPython是一个解释器,它提供与C以及其他编程语言的外部函数接口,它将python源代码编译成中间字节码,由CPython虚拟机进行解释。迄今为止和未来的讨论都是关于CPython和理解环境中的行为。


内存模型和锁定机制

编程语言使用程序中的对象来执行操作。这些对象由基本数据类型组成,如​​string​​​、​​integer​​​或​​boolean​​​。它们还包括更复杂的数据结构,如​​list​​​或​​classes/objects​​。程序对象的值存储在内存中,以便快速访问。在程序中使用变量时,进程将从内存中读取值并对其进行操作。在早期的编程语言中,大多数开发人员负责他们程序中的所有内存管理。这意味着在创建列表或对象之前,首先必须为变量分配内存。在这样做时,你可以继续释放以“释放”内存。

在python中,对象通过引用存储在内存中。引用是对象的一种标签,因此一个对象可以有许多名称,比如你如何拥有给定的名称和昵称。引用是对象的精确内存位置。引用计数器用于python中的垃圾收集,这是一种自动内存管理过程。

在引用计数器的帮助下,python通过在创建或引用对象时递增引用计数器和在取消引用对象时递减来跟踪每个对象。当引用计数为0时,对象的内存将被释放。

import sys
import gc

hello = "world" #reference to world is 2
print (sys.getrefcount(hello))

bye = "world"
other_bye = bye
print(sys.getrefcount(bye))
print(gc.get_referrers(other_bye))
>>> 4
>>> 6
>>> [[sys, gc, hello, world, print, sys, getrefcount, hello, bye, world, other_bye, bye, print, sys, getrefcount, bye, print, gc, get_referrers, other_bye], (0, None, world), __name__: __main__, __doc__: None, __package__: None, __loader__: <_frozen_importlib_external.SourceFileLoader object at 0x0138ADF0>, __spec__: None, __annotations__: , __builtins__: <module builtins (built-in)>, __file__: test.py, __cached__: None, sys: <module sys (built-in)>, gc: <module gc (built-in)>, hello: world, bye: world, other_bye: world]

需要保护这些参考计数器变量,防止竞争条件或内存泄漏。以保护这些变量;可以将锁添加到跨线程共享的所有数据结构中。

超简单的Python教程系列——第15篇:多线程_python教程_05


CPython 的 GIL 通过一次允许一个线程控制解释器来控制 Python 解释器。它为单线程程序提供了性能提升,因为只需要管理一个锁,但代价是它阻止了多线程 CPython 程序在某些情况下充分利用多处理器系统。

以上是关于超简单的Python教程系列——第15篇:多线程的主要内容,如果未能解决你的问题,请参考以下文章

超简单的Python教程系列——第14篇:异步

超简单的Python教程系列——第3篇:项目结构和导入

超简单的Python教程系列——第5篇:类

超简单的Python教程系列——第12篇:文件处理

超简单的Python教程系列——第8篇:迭代工具

超简单的Python教程系列——第6篇:错误异常