Python中的并发编程(2)线程的实现

简介: Python中的并发编程(2)线程的实现

Python中线程的实现

1. 线程

在Python中,threading 库提供了线程的接口。我们通过threading 中提供的接口创建、启动、同步线程。

例1. 使用线程旋转指针

想象一个场景:程序执行了一个耗时较长的操作,如复制一个大文件,我们希望这个过程中程序显示一个动画,表示程序正常运行没有卡死。


简化一下:启动一个函数,执行 3 秒。在这3秒内,在终端持续显示指针旋转的动画。下面用线程来实现这个操作。


注:本例代码主要来自《流畅的Python》(第二版) 19.4.

1首先我们定义旋转函数spin和阻塞函数slow

spin函数每隔0.1s依次打印\|/-,看起来就像是指针转动:

import itertools
import time
def spin(msg: str) -> None:  
  for char in itertools.cycle(r'\|/-'): 
    status = f'\r{char} {msg}' 
    print(status, end='', flush=True)
    time.sleep(0.1)
  blanks = ' ' * len(status)
  print(f'\r{blanks}\r', end='')


if __name__ == '__main__':
  spin("thinking...")

slow函数用来模拟一个耗时的操作。这里我们直接调用time.sleep(3) 等待3秒,然后返回一个结果。

# 阻塞3秒,并返回42
def slow() -> int:
  time.sleep(3) 
  return 42

调用time.sleep() 阻塞所在的线程,但是释放 GIL,其他 Python 线程可以继续运行。

现在,我们要用线程实现并发。看起来就像是slowspin同时进行。

下面对spin函数做了一些修改,通过threading.Event信号量来同步线程。

import itertools
import time
from threading import Thread, Event

# 旋转
def spin(msg: str, done: Event) -> None:  # done用于同步线程
  for char in itertools.cycle(r'\|/-'): 
    status = f'\r{char} {msg}' 
    print(status, end='', flush=True)
    if done.wait(.1): #等待/阻塞 。除非有其他线程set了这个事件,则返回True;或者经过指定的时间(0.1s)后,返回 False。
      break
  blanks = ' ' * len(status)
  print(f'\r{blanks}\r', end='')

# 阻塞3秒,并返回42
def slow() -> int:
  time.sleep(3) 
  return 42

使用线程来并发执行两个函数。

下面我们只手动启动了一个spinner线程,因为程序本身就有一个主线程。

def supervisor() -> int: 
  done = Event()  # 信号量,用于线程同步
  spinner = Thread(target=spin, args=('thinking!', done)) # 使用Thread创建线程实例spinner。
  print(f'spinner object: {spinner}') 
  spinner.start() # 启动spinner线程
  result = slow()  # 调用slow,阻塞 main 线程。同时,次线程spinner运行旋转指针动画
  done.set() # 设置done为真,唤醒等待done的线程。结束spinner中的循环。
  spinner.join() # 等待spinner 线程结束。-貌似这里加不加都不影响。
  return result
  
def main() -> None:
  result = supervisor() 
  print(f'Answer: {result}')
  
if __name__ == '__main__':
  main()

程序的执行顺序,主要步骤都发生在supervisor函数中,我们跳过main从supervisor开始看。

由于GIL的存在,同一时刻只有一个线程在执行。所以下面是一个顺序执行的过程。

执行过程大致如下:

主线程:创建spinner线程,启动spinner线程

spinner线程:输出字符,然后遇到done.wait(.1) 阻塞自己。

主线程:调用slow函数,遇到time.sleep(3) 阻塞

spinner线程:done.wait(.1) 超过了0.1秒返回False,继续输出字符。重复进行阻塞0.1秒、输出字符。

3秒后…

主线程:slow执行完毕,返回结果42。主线程继续执行done.set(),这会唤醒等待done的线程spinner。

spinner线程:运行到done.wait(.1),由于主线程执行了done.set()使得这里的结果为True,所以执行break,结束循环。执行循环下面的print语句后spinner线程结束。

主线程:返回结果。


例2.计算因子

第二个例子我们看一个(失败的)并行计算的例子:

我们希望用n个线程并行计算n个数各自的因子。


注:本例代码来自《Effective Python》(第二版) 第53章

基准方法

逐个计算。

import time

# 计算number的因子
def factorize(number):
    for i in range(1, number + 1):
        if number % i == 0:
            yield i

numbers = [2139079, 1214759, 1516637, 1852285, 14256346, 12456533]
start = time.time()
 
for number in numbers:
    list(factorize(number))
 
end = time.time()
delta = end - start
print(f'串行方法花费了 {delta:.3f} 秒')

多线程方式

可以像例1中使用Thread函数实现线程:

def get_factor(number):
    factors = list(factorize(number))
    return factors

start = time.time()
threads = []
for number in numbers:
    thread = Thread(target=get_factor, args=(number,))
    thread.start() # 启动
    threads.append(thread)
    
# 等待所有线程完成
for thread in threads:
    thread.join() # 等待完成

end = time.time()
delta = end - start
print(f'Thread方法花费了 {delta:.3f} 秒')

实现线程的另一种方式是继承Thread类并实现run方法:

from threading import Thread

# 继承Thread,需要实现run方法,在run方法中执行要做的事情
class FactorizeThread(Thread):
    def __init__(self, number):
        super().__init__()
        self.number = number
 
    def run(self):
        self.factors = list(factorize(self.number))


start = time.time()

threads = []
for number in numbers:
    thread = FactorizeThread(number)
    thread.start() # 启动
    threads.append(thread)
    
# 等待所有线程完成
for thread in threads:
    thread.join() # 等待完成
 
end = time.time()
delta = end - start
print(f'Thread方法花费了 {delta:.3f} 秒')

运行结果:

你会发现这个多线程的版本并没有变快,这并不意外。

介绍线程时说过,因为GIL的存在,多线程无法同时执行,甚至因为创建和切换线程产生额外的开销导致耗时增加。

小结

在GIL的限制下,Python线程对于并行计算没有用处,但是对于等待(IO、网络、后台任务)是有用处的。下一节我们会看一些Python线程的实际案例。

相关文章
|
2月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
133 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
17天前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
42 20
|
5月前
|
安全 数据处理 开发者
Python中的多线程编程:从入门到精通
本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
|
4天前
|
数据采集 Java 数据处理
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
15 0
|
2月前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
79 11
|
2月前
|
数据采集 消息中间件 Java
python并发编程:什么是并发编程?python对并发编程有哪些支持?
并发编程能够显著提升程序的效率和响应速度。例如,网络爬虫通过并发下载将耗时从1小时缩短至20分钟;APP页面加载时间从3秒优化到200毫秒。Python支持多线程、多进程、异步I/O和协程等并发编程方式,适用于不同场景。线程通信方式包括共享变量、消息传递和同步机制,如Lock、Queue等。Python的并发编程特性使其在处理大规模数据和高并发访问时表现出色,成为许多领域的首选语言。
|
4月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
434 6
|
4月前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
4月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
212 1
|
4月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####

热门文章

最新文章