Pygame线程引发小行星游戏内存泄漏,需保留线程并修复
小行星游戏内存泄漏问题及线程优化方案
问题描述
我正在开发一款可长期运行的小行星游戏,通过PyCharm调试观察到:
- 启动时内存占用约30.1MB;
- 固定60FPS运行时,内存每秒增长约0.1MB;
- 无限制FPS时,内存增长速率随FPS提升而加快。
已定位内存增长源于EventHandler类的update函数,现需修改该函数,保留线程(或同等高效方案)——因为批量blit图像时Pygame速度较慢,线程可尽早启动blit进程,通过join确保线程不滞后于帧,避免视觉bug。
原代码
import pygame import math import random import threading class Main: def __init__(self): self.image = None self.Dimensions = 1000 self.display = pygame.display.set_mode((self.Dimensions, self.Dimensions)) self.fps = 60 self.MainPage = True self.Game = False self.clock = pygame.time.Clock() pygame.init() def start(self): self.running = True self.character = Player() self.handler = EventHandler() self.display = pygame.display.set_mode((self.Dimensions, self.Dimensions)) while self.running: self.event_list = pygame.event.get() for self.event in self.event_list: if self.event.type == pygame.QUIT: self.running = False pygame.display.quit() pygame.quit() quit() white = [255, 255, 255] self.display.fill(white) self.Dimensions = 1000 self.clock.tick(self.fps) self.handler.spawn() self.handler.update() pygame.display.flip() class Player: def __init__(self): self.Dimensions = Game.Dimensions self.x = self.Dimensions / 2 self.y = self.Dimensions / 2 self.looking_directions = 0 self.velocity = 0 self.image = "Pictures/Player.png" class Asteroids(): def __init__(self): self.size = 100 self.x = random.choice(list(set([x for x in range(0, 9)]) - set(range(int(Game.Dimensions/5), int(Game.Dimensions*(4/5)))))) self.y = random.choice(list(set([x for x in range(0, 9)]) - set(range(int(Game.Dimensions/5), int(Game.Dimensions*(4/5)))))) self.velocity = random.uniform(1, 2) self.looking_direction = random.randrange(0, 360) self.picture = "Pictures/Asteroids.png" size = 75 self.image = pygame.image.load(self.picture) self.image = pygame.transform.scale(self.image, (size, size)) self.image = pygame.transform.rotate(self.image, self.looking_direction) def Update(self): Game.display.blit(self.image, (self.x, self.y)) class EventHandler(): def __init__(self): self.asteroid_list = [] self.tick = 0 self.asteroid_cap = 15 def spawn(self): self.tick += 1 if len(self.asteroid_list) >= self.asteroid_cap: pass elif self.tick >= 60: temp_asteroid = Asteroids() self.asteroid_list.append(temp_asteroid) print("Asteroid created: " + str(len(self.asteroid_list)) + " currently alive") self.tick = 0 def update(self): for current in self.asteroid_list: x = threading.Thread(target=current.Update) x.start() try: x.join() except: pass if __name__ == "__main__": Game = Main() Game.start()
引发内存泄漏的update函数(补充版)
def update(self): for current in self.asteroid_list: x = threading.Thread(target=current.Update) y = threading.Thread(target=current.Drift) x.start() y.start() try: x.join() y.join() except: pass
问题根源分析
- 线程对象未被正确回收:每次循环只join最后创建的线程,之前的线程对象无法被垃圾回收(GC),导致内存持续累积;
- Pygame线程安全风险:
blit操作并非线程安全,多线程直接操作主display可能引发渲染异常,同时加剧内存泄漏。
修复方案
方案1:正确管理所有线程,解决内存泄漏
修改update函数,保存所有线程对象并逐一join,确保线程资源被完全释放:
def update(self): threads = [] for current in self.asteroid_list: # 创建线程并加入列表 blit_thread = threading.Thread(target=current.Update) drift_thread = threading.Thread(target=current.Drift) blit_thread.start() drift_thread.start() threads.append(blit_thread) threads.append(drift_thread) # 等待所有线程执行完毕 for thread in threads: try: thread.join() except Exception as e: print(f"线程join失败: {e}")
- 核心逻辑:将所有线程对象存入列表,循环join每个线程,确保每个线程执行完成后资源被回收,避免线程对象堆积。
方案2:线程池优化(更高效的线程复用方案)
使用concurrent.futures.ThreadPoolExecutor复用线程,避免频繁创建销毁线程的开销,同时自动管理线程生命周期:
from concurrent.futures import ThreadPoolExecutor class EventHandler(): def __init__(self): self.asteroid_list = [] self.tick = 0 self.asteroid_cap = 15 # 初始化线程池,大小可根据CPU核心数或小行星数量调整 self.executor = ThreadPoolExecutor(max_workers=10) def update(self): futures = [] for current in self.asteroid_list: # 提交任务到线程池 futures.append(self.executor.submit(current.Update)) futures.append(self.executor.submit(current.Drift)) # 等待所有任务完成 for future in futures: try: future.result() except Exception as e: print(f"任务执行失败: {e}") # 游戏退出时关闭线程池,释放资源 def shutdown(self): self.executor.shutdown(wait=True)
- 核心逻辑:线程池复用已创建的线程,减少线程创建销毁的内存开销,自动处理线程回收,从根源避免内存泄漏;
- 注意:在游戏退出流程中调用
handler.shutdown()确保线程池正确关闭。
方案3:Pygame离屏渲染优化(非线程方案,性能相当)
如果担心Pygame线程安全问题,可采用离屏渲染+批量blit的方式,性能不逊于线程方案,且完全避免内存泄漏:
# 修改Asteroids类,新增获取绘制数据的方法 class Asteroids(): # ... 其他代码保持不变 ... def get_draw_data(self): return (self.image, (self.x, self.y)) # 修改EventHandler的update方法 def update(self): # 创建与主display尺寸一致的离屏Surface offscreen_surf = pygame.Surface((Game.Dimensions, Game.Dimensions)) # 先更新所有小行星位置,再批量绘制到离屏Surface for current in self.asteroid_list: current.Drift() img, pos = current.get_draw_data() offscreen_surf.blit(img, pos) # 一次性将离屏Surface绘制到主display Game.display.blit(offscreen_surf, (0, 0))
- 核心逻辑:减少主display的blit次数,Pygame对单个Surface的批量绘制优化更好,同时避免线程安全问题,彻底消除内存泄漏。
额外优化建议
- 预加载图像资源:在游戏启动时统一加载并缓存小行星图像,避免每次创建
Asteroids对象时重复调用pygame.image.load(),减少内存占用; - 清理无用代码:原代码中
Player类未被实际使用,可暂时注释或删除;Main类中重复设置self.display的代码也可清理。
内容的提问来源于stack exchange,提问作者Kamil Leocadie-Olsen
相关产品推荐
相关产品推荐

