-
Notifications
You must be signed in to change notification settings - Fork 0
Fix critical issues: thread safety, memory leaks, and error handling #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
- Add thread-safe caching using ConcurrentDictionary - Implement proper IDisposable pattern with GC.SuppressFinalize - Add comprehensive null/parameter validation - Improve error messages with specific exceptions - Fix memory leaks in asset unloading - Add null checks before unloading assets - Add logger to AddressableAssetsManager for consistency - Handle destroyed objects gracefully
tuha263
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent Critical Fixes - Highly Recommended for Production
This PR addresses several critical issues that could cause production failures:
🔒 Thread Safety: The move to ConcurrentDictionary with proper locking prevents race conditions during concurrent asset loading - essential for Unity's async operations.
🧹 Memory Management: Proper IDisposable implementation with GC.SuppressFinalize prevents memory leaks and improves performance.
✅ Error Handling: Comprehensive validation and exception improvements make debugging much easier and prevent silent failures.
🏗️ Robustness: Null checks and defensive programming practices make the system much more resilient to edge cases.
These changes transform potentially unstable code into production-ready, thread-safe asset management. The fixes are particularly important for applications with heavy asset loading or concurrent operations.
|
|
||
| private readonly Dictionary<string, Object> cacheSingle = new Dictionary<string, Object>(); | ||
| private readonly Dictionary<string, Object[]> cacheMultiple = new Dictionary<string, Object[]>(); | ||
| private readonly ConcurrentDictionary<string, Object> cacheSingle = new ConcurrentDictionary<string, Object>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical Thread Safety Fix: Replacing Dictionary with ConcurrentDictionary is essential here because:
- Multiple threads can simultaneously load assets, creating race conditions with regular dictionaries
- Unity's async asset loading can trigger callbacks on different threads, causing data corruption
- ConcurrentDictionary provides built-in thread-safe operations like
GetOrAdd, eliminating the need for external locking in most scenarios
This change prevents crashes and data corruption when assets are loaded concurrently from different threads or coroutines.
| private readonly Dictionary<string, Object> cacheSingle = new Dictionary<string, Object>(); | ||
| private readonly Dictionary<string, Object[]> cacheMultiple = new Dictionary<string, Object[]>(); | ||
| private readonly ConcurrentDictionary<string, Object> cacheSingle = new ConcurrentDictionary<string, Object>(); | ||
| private readonly ConcurrentDictionary<string, Object[]> cacheMultiple = new ConcurrentDictionary<string, Object[]>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread Safety Lock: The loadLock object provides additional protection for critical sections where we need atomic operations beyond what ConcurrentDictionary offers. This is necessary when we need to:
- Check for existence AND add in one atomic operation
- Prevent duplicate loading when multiple threads request the same asset simultaneously
- Ensure consistent state during complex load operations
Without this lock, two threads could both see that an asset doesn't exist and both attempt to load it, causing redundant work and potential issues.
| private readonly Dictionary<string, Object[]> cacheMultiple = new Dictionary<string, Object[]>(); | ||
| private readonly ConcurrentDictionary<string, Object> cacheSingle = new ConcurrentDictionary<string, Object>(); | ||
| private readonly ConcurrentDictionary<string, Object[]> cacheMultiple = new ConcurrentDictionary<string, Object[]>(); | ||
| private readonly object loadLock = new object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proper Disposal State Tracking: The disposed flag prevents:
- Use-after-dispose scenarios that could cause crashes or undefined behavior
- Resource leaks by ensuring cleanup only happens once
- ObjectDisposedException when methods are called on disposed instances
This follows the standard .NET disposal pattern and makes the code more robust in complex scenarios where the manager might be disposed while operations are in progress.
| T IAssetsManager.Load<T>(string key) | ||
| { | ||
| return (T)this.cacheSingle.GetOrAdd(key, () => | ||
| if (string.IsNullOrWhiteSpace(key)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Input Validation: Adding null/empty key validation is crucial because:
- Prevents crashes in downstream asset loading systems that expect valid keys
- Provides clear error messages early in the call stack, making debugging easier
- Follows defensive programming principles by validating inputs at API boundaries
- Prevents cache pollution with invalid keys that could cause issues later
This validation catches programmer errors early rather than letting them propagate through the system.
| if (string.IsNullOrWhiteSpace(key)) | ||
| throw new ArgumentException("Asset key cannot be null or empty", nameof(key)); | ||
|
|
||
| if (this.disposed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use-After-Disposal Protection: This check ensures that:
- Operations fail fast with a clear exception rather than unpredictable behavior
- Resource safety is maintained by preventing access to cleaned-up resources
- Debugging is easier with specific ObjectDisposedException rather than NullReferenceException
- API contracts are enforced - once disposed, the object should not be used
This is especially important in Unity where GameObjects and managers can have complex lifetimes.
| void IDisposable.Dispose() | ||
| { | ||
| this.Dispose(); | ||
| this.Dispose(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GC.SuppressFinalize Optimization: Calling GC.SuppressFinalize(this) is crucial because:
- Prevents unnecessary finalization when resources are properly disposed
- Improves performance by removing the object from the finalization queue
- Reduces GC pressure since finalizeable objects require additional collection cycles
- Follows .NET best practices for IDisposable implementation
This optimization can significantly improve performance in applications that create/dispose many asset managers.
| var scopedKey = this.GetScopedKey(key); | ||
| var asset = Resources.Load<T>(scopedKey); | ||
|
|
||
| if (asset == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improved Exception Semantics: Changing from ArgumentOutOfRangeException to InvalidOperationException is more appropriate because:
- ArgumentOutOfRangeException is for parameter values outside valid ranges (like negative array indices)
- InvalidOperationException is for operations that fail due to object state (like asset not found)
- The asset key parameter itself is valid, but the operation failed due to the asset not existing
- Provides better semantic meaning for exception handling and debugging
This makes the API more intuitive and follows .NET exception conventions more closely.
| public sealed class AddressableAssetsManager : AssetsManager | ||
| { | ||
| private readonly string? scope; | ||
| private readonly ILogger logger; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consistent Logging: Adding a logger instance to AddressableAssetsManager provides:
- Consistency with the base AssetsManager which already has logging
- Better debugging capabilities for Addressable-specific operations
- Centralized logging configuration through the ILoggerManager
- Error tracking and diagnostics for asset loading failures
This enables better monitoring and troubleshooting of Addressable asset operations in production.
| } | ||
|
|
||
| protected override void Unload(Object asset) | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Safe Asset Unloading: Adding null checks before Addressables.Release() prevents:
- NullReferenceException when attempting to release null assets
- Crashes in Addressable system which expects valid asset references
- Log spam from failed release operations
- Inconsistent cleanup behavior across different asset loading scenarios
The try-catch block with logging provides additional safety and diagnostics for release failures that might occur due to timing issues or Unity lifecycle problems.
| var handle = this.LoadInternal<T>(key); | ||
| var result = handle.WaitForResultOrThrow(); | ||
|
|
||
| if (result == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comprehensive Error Handling: The enhanced error handling provides:
- Early validation to catch programming errors before expensive asset operations
- Null result detection to handle cases where Addressables returns null unexpectedly
- Clear error messages with context about which asset failed to load
- Robust failure modes that help identify issues in production
This prevents silent failures and provides clear diagnostics when asset loading problems occur.
dc64167 to
132298d
Compare
Summary
Critical Issues Fixed
Thread Safety
Memory Management
Error Handling & Validation
Code Quality Improvements
Test Plan