You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题根源分析

  1. 线程对象未被正确回收:每次循环只join最后创建的线程,之前的线程对象无法被垃圾回收(GC),导致内存持续累积;
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 00:45:55