python 线程教程,python多线程详解
Yyds干货库存
在当今的现代世界,从社交媒体到智能设备,数据是我们生活的核心。程序的性能取决于它经常通过网络操作和计算数据的能力。处理大量数据会出问题;特别是程序执行时间的增加会导致“阻塞”或“滞后”。
为了高效的程序执行和日益复杂的多核操作系统/硬件架构的需要,编程语言试图更好地利用这种行为。“并发”一词的字面意思是“同时发生”。由于计算机可以同时运行多条指令,因此可以显著减少并发程序的执行时间。
Python的并发模型中交织着三个主要的操作系统概念;即线程、任务和进程。
什么是线程?你为什么想要它?本质上,Python是一种线性语言,但是当你需要更多的处理能力时,线程模块非常方便。虽然Python中的线程不能用于并行CPU计算,但它非常适合I/O操作,比如web抓取,因为处理器是空闲的,在等待数据。
线程正在改变游戏规则,因为许多与网络/数据I/O相关的脚本花费大部分时间等待来自远程源的数据。由于下载中可能没有链接(即抓取一个单独的网站),处理器可以从不同的数据源并行下载,并在最后合并结果。对于CPU密集型的进程,使用线程模块没有任何优势。
幸运的是,标准库中包含了线程:
导入线程
从队列导入队列
导入您可以将target用作可调用对象,使用args将参数传递给函数,并启动线程。
定义测试线程(数量):
打印编号
if __name__==__main__ :
对于范围(5)中的I:
t=螺纹。线程(target=testThread,arg=(i,))
T.start()如果你没见过if _ _ name _ _= _ main _ _ :之前基本上是一种保证嵌入其中的代码只在脚本直接运行(不导入)时运行的方式。
锁定同一操作系统进程的线程将计算工作负载分配给多个内核,如C和Java等编程语言所示。通常python只使用一个进程,从中生成一个主线程来执行运行时。由于一种称为全局解释器锁的锁定机制,它停留在单个核心上,无论计算机有多少个核心或生成多少个新线程。这种机制是为了防止所谓的竞态条件。
说到比赛,我会想到纳斯卡和一级方程式赛车。让我们用这个类比来想象一下,所有的一级方程式赛车手都试图同时驾驶一辆赛车参赛。听起来很荒谬,对吧?只有每个司机都能用自己的车,或者每次跑一圈,把车交给下一个司机,这才有可能。
这与线程中发生的情况非常相似。线程是从“主”线程“派生”出来的,每个后续线程都是前一个线程的副本。这些线程都存在于同一个进程“上下文”(事件或竞赛)中,所以分配给这个进程的所有资源(比如内存)都是共享的。例如,在典型的python解释器会话中:
A=8在这里,A通过将值8临时保存在内存中的某个位置来消耗非常少的内存(RAM)。
到目前为止一切顺利,让我们开始一些线程,观察它们的行为,当添加两个数字x,y:
导入时间
导入线程
从线程导入线程
a=8
def threaded_add(x,y):
#通过提问模拟更复杂的任务
# python睡眠,因为添加发生得太快了!
对于范围(2)中的I:
全球a
打印(“不同线程中的计算任务!”)
时间.睡眠(1)
#这不太好!但是python将会强制同步,稍后会详细介绍!
a=10
打印(一份)
#当前线程将是一个子集分叉!
if __name__!=__main__ :
当前线程=线程化.当前线程()
#这里我们告诉python从主
#执行线程使其他
if __name__==__main__ :
Thread=Thread(target=threaded _ add,args=(1,2))
thread.start()
thread.join()
打印(一份)
打印(主线程完成.正在退出)不同线程中的计算任务!
10
不同线程中的计算任务!
10
10
主线程完成.退出当前正在运行的两个线程。我们称它们为线程一和线程二。如果thread_one想修改一个值为10的a,而thread_two试图同时更新同一个变量,我们就有问题了!会出现一种情况叫数据竞赛,A的结果值会不一致。
一场你没有观看,却从你的两个朋友那里听到了两个相互矛盾的结果的汽车比赛!Thread_one告诉你一件事,thread 2反驳!以下是伪代码片段描述:
a=8
#产生两个不同的线程1和2
# thread_one将a的值更新为10
如果(a==10):
#一张支票
#thread_two将a的值更新为15
a=15
b=a * 2
#如果thread_one先完成,结果将是20
#如果thread_two先完成,结果将是30
#谁是对的?
到底是怎么回事?Python是一种解释型语言,这意味着它带有一个解释器3354,一个从另一种语言中解析源代码的程序!python中的一些这样的解释器包括cpython、pypypy、Jpython和Ironpython,其中cpython是python的原始实现。
Cpython是一个解释器,用C和其他编程语言提供外部函数接口。它将Python源代码编译成中间字节码,由CPython虚拟机解释。目前和未来的讨论都是关于CPython和理解环境中的行为。
内存和锁定机制编程语言使用程序中的对象来执行操作。这些对象由基本数据类型组成,如字符串、整数或布尔。它们还包括更复杂的数据结构,如列表或类/对象。程序的值存储在内存中,以便快速访问。当一个变量在程序中使用时,进程将从内存中读取值并对其进行操作。在早期的编程语言中,大多数开发人员负责程序中的所有内存管理。这意味着在创建列表或对象之前,必须先为变量分配内存。在这样做的时候,你可以继续释放来“释放”内存。
在python中,对象通过引用存储在内存中。是指一个对象的一种标签,所以一个对象可以有很多名字,比如你怎么有一个名和昵称。是引用对象的确切内存位置。在python中,计数器用于垃圾收集,这是一个自动内存管理过程。
在引用计数器的帮助下,python通过在对象被创建或引用时递增引用计数器,在对象被取消引用时递减引用计数器来跟踪每个对象。当引用计数为0时,对象的内存将被释放。
导入系统
导入gc
hello= world #对 world 的引用是2
print (sys.getrefcount(hello))
再见=世界
其他再见=再见
print(sys.getrefcount(bye))
print(GC . get _ referers(other _ bye))4
六
[[sys , gc , hello , world , sys , getrefcount , hello , bye , world , other_bye , sys , getrefcount , bye , print , gc , get _ referrers , other_bye],(0,None, world ),{__name__: __main__ , __doc__: None,__package__: None, _ _ loader _ _ :_ frozed _ import lib _ external。0x0138ADF0处的SourceFileLoader对象, __spec__: None, __annotations__: {}, __builtins__: module builtins (内置), __file__: test.py , __cached__: None, sys: module sys (内置),GC: module GC (内置), hello: world , bye: world , other _ bye: world}]这些引用计数器变量需要受到保护,以防止争用情况或内存泄漏。来保护这些变量;您可以为所有跨线程共享的数据结构添加锁。
解释器的GIL通过一次允许一个线程控制解释器来控制计算机编程语言解释器。它为单线程程序提供了性能提升,因为只需要管理一个锁,但代价是它阻止了多线程解释器程序在某些情况下充分利用多处理器系统。
当用户编写大蟒程序时,性能受中央处理器限制的程序和受输入-输出限制的程序之间存在差异中央处理器.通过同时执行许多操作将程序推到极限,而输入-输出程序必须花费时间等待输入输出。
因此,只有多线程程序在GIL中花费大量时间来解释解释器字节码;GIL成为瓶颈。即使没有严格必要,GIL也会降低性能。例如,一个用大蟒编写的同时处理超正析象管(图片Orthicon)和中央处理器任务的程序:
导入时间,操作系统
从线程导入线程,当前线程
从多重处理导入当前进程
计数=200000000
睡眠=10
定义io_bound(秒):
pid=os.getpid()
threadName=current_thread().名字
processName=当前进程().名字
print(f"{ PID } * {进程名} * {线程名} \
-开始睡觉.)
时间.睡眠(秒)
print(f"{ PID } * {进程名} * {线程名} \
-睡完了.)
定义cpu_bound(n):
pid=os.getpid()
threadName=current_thread().名字
processName=当前进程().名字
print(f"{ PID } * {进程名} * {线程名} \
-开始数数.)
而n 0:
n -=1
print(f"{ PID } * {进程名} * {线程名} \
-数完了.)
def timeit(function,args,threaded=False):
start=time.time()
如果有螺纹:
t1=线程(目标=函数,args=(args,))
t2=线程(目标=函数,args=(args,))
t1.start()
t2.start()
t1.join()
t2.join()
否则:
函数(参数)
end=time.time()
打印(对参数{}运行{}所用的时间(秒)为{}s -{} .格式(函数、参数、结束起始、如果有线程则为"有线程",否则为"无线程"))
if __name__==__main__ :
#运行io_bound任务
打印(“木卫一绑定任务非线程化")
timeit(io_bound,SLEEP)
打印(“木卫一绑定任务线程化")
#在线程中运行io_bound任务
timeit(io_bound,SLEEP,threaded=True)
打印(“CPU限制任务非线程化")
#正在运行cpu _绑定任务
timeit(cpu_bound,计数)
打印(“CPU绑定任务线程化")
#在线程中运行cpu _绑定任务
timeit(cpu_bound,COUNT,threaded=True) IO绑定任务非线程化
17244 *主进程*主线程-开始睡眠.
17244 *主进程*主线程-睡眠结束.
17244 *主进程*主线程-开始睡眠.
17244 *主进程*主线程-睡眠结束.
运行参数10为0x00C50810的函数io_bound所用的时间(秒)为20.049976s-无线程
超正析象管(Image Orthicon)绑定任务线程化
10180 *主进程*线程-1 -开始睡眠.
10180 *主进程*线程-2 -开始睡眠.
10180 *主进程*线程-1 -睡眠结束.
10180 *主进程*线程-2 -睡眠结束.
在参数10的0x01B90810处运行函数io_bound所用的时间(秒)是10.06968689s-螺纹
中央处理器绑定任务非线程化
14172 *主进程*主线程-开始计数.
14172 *主进程*主线程-完成计数.
14172 *主进程*主线程-开始计数.
14172 *主进程*主线程-完成计数.
在参数20000000的0x018F4468处运行函数cpu _绑定所用的时间(秒)是44秒-没有线程化。36304.66666666667
中央处理器绑定任务增加了
15616 *主进程*线程-1 -开始计数.
15616 *主进程*线程-2 -开始计数.
15616 *主进程*线程-1 -完成计数.
15616 *主进程*线程-2 -完成计数.
在参数2000000000上的0x01e4468处运行函数CPU _ bound所用的时间(秒)为106.0971360931396s-threaded从结果中,我们注意到多线程在多个IO绑定任务中表现良好,执行时间为10秒,而非线程方法的执行时间为20秒。我们使用相同的方法来执行CPU密集型任务。好的,最初它确实同时启动了我们的线程,但是最后,我们看到整个程序的执行用了大约106秒!然后发生了什么?这是因为当线程1启动时,它会获得全局解释器锁(GIL),从而阻止线程2使用CPU。因此,线程2必须等待线程1完成其任务并释放锁,以便它可以获取锁并执行其任务。锁的获取和释放增加了总执行时间的开销。所以可以肯定的说,线程并不是依靠CPU执行任务的理想解决方案。
这个特性使得并发编程变得困难。如果GIL在并发性方面阻碍了我们,我们应该摆脱它还是能够关闭它?嗯,不容易。其他功能、库和包都依赖于GIL,所以必须有东西取代它,否则整个生态系统就会崩溃。这是一个很难解决的问题。
多进程我们已经证实CPython使用锁来保护数据不被竞争。尽管存在这样的锁,程序员还是找到了一种显式实现并发的方法。对于GIL,我们可以使用多处理库来绕过全局锁。多处理实现了真正的并发性,因为它在不同的CPU内核上执行代码,并跨越不同的进程。它创建了一个新的Python解释器实例,运行在每个内核上。不同的进程位于不同的内存位置,因此在它们之间共享对象并不容易。在这个实现中,python为每个要运行的进程提供了不同的解释器;因此,在这种情况下,为多处理中的每个进程提供一个线程。
导入操作系统
导入时间
从多重处理导入流程,当前流程
睡眠=10
计数=200000000
定义递减计数(计数):
pid=os.getpid()
processName=current_process()。名字
print(f“{ PID } * { process name } \
-开始数数.)
而cnt 0:
cnt -=1
定义io_bound(秒):
pid=os.getpid()
threadName=current_thread()。名字
processName=current_process()。名字
print(f“{ PID } * { process name } * { thread name } \
-开始睡觉.)
time.sleep(秒)
print(f“{ PID } * { process name } * { thread name } \
-睡完了.)
if __name__==__main__ :
#创建流程
start=time.time()
#CPU受限
p1=进程(target=count_down,args=(COUNT,))
p2=进程(target=count_down,args=(COUNT,))
#IO绑定
#p1=进程(目标=,参数=(睡眠,))
#p2=进程(target=count_down,args=(SLEEP,))
#启动进程_线程
p1.start()
p2.start()
#等到完成
p1.join()
p2.join()
stop=time.time()
经过时间=停止-开始
print(所用时间(秒)为:,已用时间)1660 * Process-2 -开始计数.
10184 *进程-1 -开始计数.
用秒计算的时间为:12.81547525448608可见多处理对于cpu和io绑定任务有着出色的性能。MainProcess启动了两个子进程,Process-1和Process-2,它们有不同的PID,每个子进程都执行将计数减少到零的任务。每个进程并行运行,使用独立的CPU内核和自己的Python解释器实例,所以整个程序只需要12秒就可以执行。
请注意,输出可能会乱序打印,因为这些过程是相互独立的。这是因为每个进程都在自己的默认主线程中执行函数。
我们还可以使用asyncio库(我在上一节已经讲过了,如果你没有读过,可以回到上一节学习)来绕过GIL锁。asyncio的基本概念是,一个名为event loop的python对象控制每个任务如何以及何时运行。事件循环知道每个任务及其状态。就绪状态表示任务已准备好运行,等待阶段表示任务正在等待外部任务完成。在异步IO中,任务永远不会放弃控制权,在执行过程中也不会被中断,所以对象共享是线程安全的。
导入时间
进口异步
计数=200000000
#异步函数定义
异步定义函数名(计数):
而cnt 0:
cnt -=1
#异步主函数定义
异步定义main():
#创建2个任务.您可以创建任意多的任务(n个任务)
task1=loop.create_task(函数名(计数))
task2=loop.create_task(函数名(计数))
#等待每个任务执行,然后将控制权交还给程序
wait asyncio . wait([任务1,任务2])
if __name__==__main__ :
#获取事件循环
start_time=time.time()
loop=asyncio.get_event_loop()
#运行事件循环中的所有任务,直到完成
loop.run_until_complete(main())
loop.close()
Print (-%s秒- %(time . time()-start _ time))-41.74118399620056秒-我们可以看到asyncio完成倒计时需要41秒,比多线程的106秒要好,但是对于cpu受限的任务来说,Asyncio创建一个eventloop和两个任务task1和task2,然后将这些任务放在eventloop上。然后,程序等待任务的执行,因为事件循环执行所有的任务直到完成。
为了充分利用python中并发的所有功能,我们还可以使用不同的解释器。JPython和IronPython没有GIL,这意味着用户可以充分利用多处理器系统。
像线程一样,多进程也有缺点:
I/O开销将由数据进程间的混洗引起。整个内存被复制到每个子进程,对于更重要的程序来说,这可能是一个很大的开销。
总而言之,如果您的代码有大量的I/O或网络使用:
多线程是您的最佳选择,因为如果您有GUI,它的开销会很低。
多线程,因为你的UI线程不会被锁定。如果您的代码受到CPU的限制:
您应该使用进程(如果您的机器有多个内核)
原创作品来自程,
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。