標簽:線程安全、延遲初始化、按需初始化、提升啟動性能
項目地址:NitasDemo/12Lazy/LazyDemo at main · Nita121388/NitasDemo
目錄
- Lazy<T>
- 1. 概念
- 2. 基本用法
- 3. 異常處理
- 4. 線程安全模式
- 5. 示例
- 1. 線程安全模式 (`ExecutionAndPublication`)
- 2. 發布模式 (`PublicationOnly`)
- 3. 結合依賴注入 (DI)
- 使用場景總結
- 6. 注意事項與最佳實踐
- 7. 總結
- LazyInitializer
- 1. 概念
- 2. 方法重載詳解
- 3. 與 Lazy<T> 的對比
- 4. 最佳實踐
- 5. 總結
- 選擇建議
- 總結
Lazy
官方文檔:Lazy<T> 類 (System) | Microsoft Learn
源碼:Lazy.cs
1. 概念
Lazy<T>
是 .NET 4.0 引入的泛型類,實現延遲初始化。
它確保對象僅在首次訪問時被創建,從而避免不必要的資源消耗并提升啟動性能。
該類封裝了延遲初始化的邏輯,并提供了線程安全的初始化機制。
-
特性
特性 說明 延遲初始化 對象在第一次訪問 `.Value` 時才被實例化,提升啟動性能。 節省資源 如果對象從未被使用,則不會創建,節省內存和 CPU。 線程安全 默認支持多線程環境下的安全初始化(可設置線程安全模式) 可復用 一旦初始化完成,后續訪問 `.Value` 將直接返回已創建的對象,不會重復創建。 支持復雜邏輯 可通過委托傳入自定義初始化邏輯。 -
優點:
減少啟動時間,按需加載資源。
-
缺點:
- 首次訪問可能有延遲。
- 線程安全模式存在鎖競爭和異常放大,濫用會浪費性能
- 過度使用會導致代碼可讀性下降。
-
最佳實踐:
僅對真正需要延遲初始化的對象使用
Lazy<T>
。
2. 基本用法
-
用法1:默認構造函數(要求無參構造)
// 簡單創建(非線程安全) Lazy<ExpensiveObject> lazySimple = new Lazy<ExpensiveObject>(); ExpensiveObject obj = lazySimple.Value; // 首次訪問時初始化
-
用法2:使用委托自定義初始化邏輯
Lazy<MyClass> lazy = new Lazy<MyClass>(() => new MyClass("自定義構造"));
3. 異常處理
-
初始化異常被緩存:
首次初始化失敗后,異常會在每次調用時重新拋出。
-
解決方案:
使用
Lazy<T>
的構造函數重載捕獲異常并重試。
Lazy<ExpensiveObject> lazyWithRetry = new Lazy<ExpensiveObject>(() =>
{try { return new ExpensiveObject(); }catch { /* 重試邏輯 */ }
});
4. 線程安全模式
PublicationOnly
通過 LazyThreadSafetyMode
指定初始化行為:
-
構造函數
public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode);
模式枚舉值 含義說明 適用場景 `ExecutionAndPublication` 線程安全,確保只有一個線程執行初始化邏輯。 多線程環境(默認) `PublicationOnly` 多線程下允許多個線程同時初始化,但只保留第一個完成的實例。 避免加鎖阻塞、初始化成本高、重復初始化無影響,允許短暫浪費資源時 `None` 非線程安全 單線程環境(高性能) -
ExecutionAndPublication
模式部分源碼private void ExecutionAndPublication(LazyHelper executionAndPublication, bool useDefaultConstructor){lock (executionAndPublication){// it's possible for multiple calls to have piled up behind the lock, so we need to check// to see if the ExecutionAndPublication object is still the current implementation.if (ReferenceEquals(_state, executionAndPublication)){if (useDefaultConstructor){ViaConstructor();}else{ViaFactory(LazyThreadSafetyMode.ExecutionAndPublication);}}}}
-
PublicationOnly
模式部分源碼private void PublicationOnly(LazyHelper publicationOnly, T possibleValue){LazyHelper? previous = Interlocked.CompareExchange(ref _state, LazyHelper.PublicationOnlyWaitForOtherThreadToPublish, publicationOnly);if (previous == publicationOnly){_factory = null;_value = possibleValue;_state = null; // volatile write, must occur after setting _value}}
-
// 線程安全模式(默認:確保僅初始化一次)
Lazy<ExpensiveObject> safeLazy = new Lazy<ExpensiveObject>(() => new ExpensiveObject(),LazyThreadSafetyMode.ExecutionAndPublication
);//避免加鎖阻塞、初始化成本高、允許短暫浪費資源時,使用 PublicationOnly
Lazy<ExpensiveCache> cache = new Lazy<ExpensiveCache>(
() => new ExpensiveObject(),
LazyThreadSafetyMode.PublicationOnly);// 非線程安全模式(高性能單線程場景)
Lazy<ExpensiveObject> unsafeLazy = new Lazy<ExpensiveObject>(() => new ExpensiveObject(),LazyThreadSafetyMode.None
);
5. 示例
1. 線程安全模式 (ExecutionAndPublication
)
場景:全局配置加載
using System;
using System.Threading;
using System.Threading.Tasks;#region 模擬配置類 Configurationpublic class Configuration
{public string Environment { get; set; }public int MaxConnections { get; set; }public DateTime LoadTime { get; set; }public override string ToString() => $"[{Environment}] MaxConnections={MaxConnections}, LoadTime={LoadTime:HH:mm:ss.fff}";
}#endregion 模擬配置類#region 配置服務使用線程安全的Lazy初始化
public class AppConfigService
{private static int _loadCounter = 0; // 用于跟蹤實際加載次數private static readonly Lazy<Configuration> _config = new Lazy<Configuration>(() => {Interlocked.Increment(ref _loadCounter);//記錄實際初始化次數Console.WriteLine($">>> [線程 {Thread.CurrentThread.ManagedThreadId}] 開始加載配置...");Thread.Sleep(2000); // 模擬數據庫/IO延遲return new Configuration {Environment = "Production",MaxConnections = 100,LoadTime = DateTime.Now};}, LazyThreadSafetyMode.ExecutionAndPublication);public static Configuration Config => _config.Value; public static int LoadCount => _loadCounter;
}#endregion#region Usageclass Program
{static void Main(string[] args){Console.WriteLine("=== 配置加載測試(線程安全模式)===");Console.WriteLine($"主線程ID: {Thread.CurrentThread.ManagedThreadId}\n");// 創建10個并發請求線程Parallel.For(0, 10, i => {Thread.Sleep(new Random().Next(50)); // 隨機延遲增加并發沖突概率Console.WriteLine($"[線程 {Thread.CurrentThread.ManagedThreadId}] 請求配置...");var config = AppConfigService.Config;Console.WriteLine($"[線程 {Thread.CurrentThread.ManagedThreadId}] 獲取配置: {config}");});Console.WriteLine($"\n實際加載次數: {AppConfigService.LoadCount}");Console.WriteLine("測試完成。按任意鍵退出...");Console.ReadKey();}
}#endregion
輸出:
=== 配置加載測試(線程安全模式)===
主線程ID: 1[線程 12] 請求配置...
>>> [線程 12] 開始加載配置...
[線程 6] 請求配置...
[線程 7] 請求配置...
[線程 8] 請求配置...
[線程 9] 請求配置...
[線程 4] 請求配置...
[線程 10] 請求配置...
[線程 1] 請求配置...
[線程 11] 請求配置...
[線程 13] 請求配置...
[線程 1] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 9] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 11] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 6] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 13] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 7] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 8] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 12] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 4] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[線程 10] 獲取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947實際加載次數: 1
測試完成。按任意鍵退出...
為何適用:
- 配置加載成本高(數據庫查詢)
- 需確保所有線程獲取同一實例
- 需避免重復初始化導致資源浪費
2. 發布模式 (PublicationOnly
)
場景:輕量級日志記錄器
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;namespace PublicationOnlyLoggerDemo
{// 日志接口public interface ILogger{void Log(string message);}// 線程安全的文件日志記錄器public class FileLogger : ILogger{private readonly string _filePath;// 靜態鎖確保多實例寫入時的線程安全private static readonly object _fileLock = new object();// 記錄已創建實例數量(用于演示)public static int InstanceCount = 0;// 記錄實際寫入次數(用于演示)public static readonly ConcurrentBag<string> AllLogs = new ConcurrentBag<string>();public FileLogger(string filePath){if (!File.Exists(filePath)){File.Create(filePath).Close();}_filePath = filePath;Interlocked.Increment(ref InstanceCount);lock (_fileLock){// 初始化日志文件File.WriteAllText(filePath, $"Log initialized at {DateTime.Now:HH:mm:ss.fff}\n");}}public void Log(string message){lock (_fileLock){File.AppendAllText(_filePath, $"{DateTime.Now:HH:mm:ss.fff} - {message}\n");}AllLogs.Add(message);}}// 日志工廠(使用PublicationOnly模式)public static class LoggerFactory{private static readonly Lazy<ILogger> _logger = new Lazy<ILogger>(() => new FileLogger("app.log"), LazyThreadSafetyMode.PublicationOnly);public static ILogger GetLogger() => _logger.Value;}class Program{static void Main(string[] args){Console.WriteLine("===開啟并行日志記錄測試===");// 并行日志記錄測試Parallel.For(0, 10, i => {var logger = LoggerFactory.GetLogger();logger.Log($"Task {i} 開啟");Thread.Sleep(50); // 模擬工作負載logger.Log($"Task {i} 完成");});// 顯示統計結果Console.WriteLine("\n測試結果:");Console.WriteLine($"日志實例創建個數: {FileLogger.InstanceCount}");Console.WriteLine($"總計寫入日志條目:{FileLogger.AllLogs.Count}");Console.WriteLine($"首次使用的日志實例:{FileLogger.AllLogs.First()}");Console.WriteLine($"最后使用的日志實例:{FileLogger.AllLogs.Last()}");Console.WriteLine("\n日志文件內容:");Console.WriteLine("-----------------");Console.WriteLine(File.ReadAllText("app.log"));Console.WriteLine("\n按任意鍵退出...");Console.ReadKey();}}
}
輸出:
測試結果:
日志實例創建個數: 10
總計寫入日志條目:20
首次使用的日志實例:Task 3 完成
最后使用的日志實例:Task 5 開啟日志文件內容:
-----------------
Log initialized at 10:13:27.464
10:13:27.464 - Task 3 開啟
10:13:27.518 - Task 0 完成
10:13:27.518 - Task 2 完成
10:13:27.518 - Task 4 完成
10:13:27.518 - Task 3 完成
10:13:27.518 - Task 5 完成
10:13:27.519 - Task 7 完成
10:13:27.519 - Task 8 完成
10:13:27.519 - Task 1 完成
10:13:27.519 - Task 6 完成
10:13:27.519 - Task 9 完成按任意鍵退出..."
===開啟并行日志記錄測試===測試結果:
日志實例創建個數: 10
總計寫入日志條目:20
首次使用的日志實例:Task 3 完成
最后使用的日志實例:Task 5 開啟日志文件內容:
-----------------
Log initialized at 10:13:27.464
10:13:27.464 - Task 3 開啟
10:13:27.518 - Task 0 完成
10:13:27.518 - Task 2 完成
10:13:27.518 - Task 4 完成
10:13:27.518 - Task 3 完成
10:13:27.518 - Task 5 完成
10:13:27.519 - Task 7 完成
10:13:27.519 - Task 8 完成
10:13:27.519 - Task 1 完成
10:13:27.519 - Task 6 完成
10:13:27.519 - Task 9 完成按任意鍵退出...
為何適用:
- 日志初始化簡單(創建文件句柄)
- 可容忍短暫存在多個日志實例
- 避免鎖競爭提升性能
3. 結合依賴注入 (DI)
場景:解決循環依賴
遵循了依賴倒置原則,通過引入抽象層(Lazy代理)解耦了服務之間的直接依賴關系,解決循環依賴問題 。
using System;
using Microsoft.Extensions.DependencyInjection;public class Program
{static void Main(string[] args){// 設置依賴注入容器var services = new ServiceCollection();// 注冊服務(注意順序很重要)services.AddScoped<PaymentService>();services.AddScoped<OrderService>();// 使用Lazy打破循環依賴services.AddScoped(sp => new Lazy<OrderService>(() => sp.GetRequiredService<OrderService>()));using var serviceProvider = services.BuildServiceProvider();// 模擬請求范圍using (var scope = serviceProvider.CreateScope()){var scopedProvider = scope.ServiceProvider;Console.WriteLine("解析OrderService...");var orderService = scopedProvider.GetRequiredService<OrderService>();Console.WriteLine("\n調用OrderService處理訂單:");orderService.ProcessOrder(100.50m);}Console.WriteLine("\n按任意鍵退出...");Console.ReadKey();}
}public class OrderService
{private readonly PaymentService _paymentService;// 正常依賴PaymentServicepublic OrderService(PaymentService paymentService){Console.WriteLine(">>> OrderService 已創建");_paymentService = paymentService;}public void ProcessOrder(decimal amount){Console.WriteLine($"處理訂單: ${amount}");_paymentService.ProcessPayment(amount);// 模擬其他操作Console.WriteLine("訂單處理完成!");}public Order GetCurrentOrder() => new Order(DateTime.Now, 100.50m);
}public class PaymentService
{private readonly Lazy<OrderService> _lazyOrderService;// 通過Lazy間接依賴OrderServicepublic PaymentService(Lazy<OrderService> lazyOrderService){Console.WriteLine(">>> PaymentService 已創建");_lazyOrderService = lazyOrderService;}public void ProcessPayment(decimal amount){Console.WriteLine($"處理支付: ${amount}");// 按需訪問OrderService(實際使用時才解析)Console.WriteLine("\n需要訂單信息,訪問Lazy.Value...");var currentOrder = _lazyOrderService.Value.GetCurrentOrder();Console.WriteLine($"獲取到當前訂單: {currentOrder}");}
}public record Order(DateTime CreatedTime, decimal Amount)
{public override string ToString() => $"[{CreatedTime:HH:mm:ss}] ${Amount}";
}
輸出
解析OrderService...
>>> PaymentService 已創建
>>> OrderService 已創建調用OrderService處理訂單:
處理訂單: $100.50
處理支付: $100.50需要訂單信息,訪問Lazy.Value...
獲取到當前訂單: [10:44:31] $100.50按任意鍵退出...
說明:
遵循了依賴倒置原則,引入抽象層(Lazy代理),解耦了服務之間的直接依賴關系,解決循環依賴問題 。
-
依賴倒置(DIP,Dependency Inversion Principle)設計
- OrderService → 直接依賴 PaymentService
- PaymentService → 依賴
Lazy<OrderService>
(非直接依賴) - 關鍵點:將強依賴轉換為弱依賴
-
階段說明
-
構造階段:只需要Lazy代理,不觸發實際解析
- PaymentService 只需要一個"承諾"(Lazy代理),不需要實際 OrderService 實例
- 避免初始化死鎖
-
執行階段:依賴樹已建立,安全訪問
- 首次訪問
.Value
觸發實際解析 - 此時 OrderService 已完全初始化
- 后續訪問使用緩存實例
- 首次訪問
-
為何適用
- 解決緊耦合服務的循環引用問題
- 延遲初始化高開銷服務(如數據庫連接)
- 動態插件加載(
Lazy<IPlugin>
按需激活)
使用場景總結
場景 | 技術選型 | 理由 |
---|---|---|
全局配置/緩存 | 線程安全模式 | 多線程共享 + 單次初始化 |
輕量級工具類(如日志) | 發布模式 | 允許冗余初始化 + 避免鎖性能損耗 |
UI組件/單線程模塊 | 非線程安全模式 | 明確線程上下文 + 零開銷 |
DI容器中的復雜服務 | `Lazy<T>`注入 | 打破循環依賴 + 按需加載 |
網絡請求/文件加載 | `Lazy<Task<T>>` 或 `AsyncLazy<T>` | 非阻塞初始化 + 結果緩存 |
6. 注意事項與最佳實踐
注意點 | 建議做法 |
---|---|
構造函數中不要訪問 `.Value` | 會導致死循環或異常 |
避免在委托中拋出異常 | 使用 try-catch 包裹初始化邏輯,或設置異常處理策略 |
使用 `IsValueCreated` 檢查狀態 | 可用于調試或日志輸出 |
7. 總結
-
優勢:
平衡性能與資源,提供線程安全的按需初始化。
-
注意:
- 優先選擇默認線程安全模式。
- 避免在頻繁調用的方法中濫用。 (存在鎖競爭/異常放大)
- 謹慎處理初始化異常。
LazyInitializer
官方文檔:LazyInitializer.EnsureInitialized 方法 (System.Threading) | Microsoft Learn
源碼:LazyInitializer.cs
1. 概念
- 基本信息
- 命名空間:
System.Threading
- 程序集:
System.Threading.dll
- 繼承關系:
Object
→LazyInitializer
- 命名空間:
- 特性
特性 說明 零分配開銷? 避免創建專用延遲初始化實例,比 `Lazy<T>` 更輕量(無額外對象分配) 引用初始化? 通過引用傳遞確保初始化狀態一致性 線程安全? 內部通過鎖或原子操作保證線程安全,支持多線程并發調用 靜態方法? 直接操作字段,無需創建包裝器 異常處理? 初始化函數拋出異常時,后續訪問會重試(與 `Lazy<T>` 的緩存異常不同) 輕量級替代方案? 相比 `Lazy<T>` ,無需額外包裝對象,直接操作目標字段。 - 示例
ExpensiveData _data = null; bool _dataInitialized = false; object _dataLock = new object();// 使用示例 ExpensiveData dataToUse = LazyInitializer.EnsureInitialized(ref _data,ref _dataInitialized,ref _dataLock);
2. 方法重載詳解
-
🔧 方法列表
EnsureInitialized\<T>(T) 在目標引用或值類型尚未初始化的情況下,使用其類型的無參數構造函數初始化目標引用類型。 EnsureInitialized\<T>(T, Boolean, Object) 在目標引用或值類型尚未初始化的情況下,使用其無參數構造函數對其進行初始化。 EnsureInitialized\<T>(T, Boolean, Object, Func\<T>) 在目標引用或值類型尚未初始化的情況下,使用指定函數初始化目標引用或值類型。 EnsureInitialized\<T>(T, Func\<T>) 在目標引用類型尚未初始化的情況下,使用指定函數初始化目標引用類型。 EnsureInitialized\<T>(T, Object, Func\<T>) 在目標引用類型尚未初始化的情況下,使用指定函數初始化目標引用類型。 -
重載 1:默認構造函數
public static T EnsureInitialized<T> (ref T? target) where T : class;
- 適用場景:類型有無參構造函數
- 線程安全:低競爭環境下安全(可能多次構造但最終保留一個實例,類似與Lazy<T>的
PublicationOnly
模式) - 返回: 已初始化的對象。
- 示例:
private ExpensiveResource _resource; public ExpensiveResource Resource => LazyInitializer.EnsureInitialized(ref _resource);
-
重載 2:自定義初始化函數
public static T EnsureInitialized<T> (ref T? target, Func<T> valueFactory) where T : class;
- 適用場景:需參數化構造或復雜初始化邏輯
- 線程安全:低競爭環境下安全(可能多次構造但最終保留一個實例)
- 返回: 已初始化的對象。
- 示例:
private DatabaseConnection _db; public DatabaseConnection Db => LazyInitializer.EnsureInitialized(ref _db, () => new DatabaseConnection(_config));
-
重載 3:完全線程安全控制
public static T EnsureInitialized<T> (ref T target, ref bool initialized, ref object? syncLock, Func<T> valueFactory);
- 適用場景:高并發環境要求嚴格單次初始化
- 參數說明:
ref T target
:需確保初始化的目標對象。 如果是null
,則將其視為未初始化;否則,將其視為已初始化。ref bool initialized
:跟蹤初始化狀態。如果initialized
指定為 true,則不會進一步初始化。ref object syncLock
:同步鎖對象(可傳入null
,方法內部初始化)Func<T> valueFactory
:創建對象的工廠方法,未初始化時調用此方法生成新實例,支持自定義邏輯(如構造函數、依賴注入等)。
- 示例:
private Logger _logger; private bool _loggerInitialized; private object _loggerLock = new object();public Logger Logger => LazyInitializer.EnsureInitialized(ref _logger, ref _loggerInitialized, ref _loggerLock, () => new Logger("app.log"));
3. 與 Lazy 的對比
特性 | LazyInitializer.EnsureInitialized() | Lazy\<T> |
---|---|---|
內存開銷? | 無額外對象分配(直接操作字段) | 需分配 `Lazy<T>` 包裝器實例 |
異常緩存? | 不緩存異常(每次失敗后重試) | 緩存異常(首次異常后永遠拋出) |
適用類型? | 僅引用類型 | 支持值類型和引用類型 |
初始化狀態跟蹤? | 需手動管理 | 內置狀態管理 |
4. 最佳實踐
- 首選場景:
- 性能敏感且需最小化內存開銷時
- 直接初始化現有字段(非新屬性)
- 線程安全建議:
- 低競爭環境 → 使用重載 1 或 2
- 高并發場景 → 使用重載 3(嚴格單次初始化)
- 鎖對象管理:
-
傳入
null
讓方法初始化鎖對象:object _lock = null; // 方法內部會替換為 new object()
-
若需復用同一鎖,提前初始化鎖對象
-
- 避免值類型:不支持值類型(編譯錯誤),需改用
Lazy<T>
。
5. 總結
通過引用傳遞和鎖對象機制,LazyInitializer
提供了比 Lazy<T>
更輕量的延遲初始化方案,適用于需要精細控制初始化過程的場景。
- 優勢:輕量高效、直接操作字段、靈活控制線程安全。
- 適用:引用類型的延遲初始化,性能敏感場景。
- 注意:
- 高并發環境須使用重載 3
- 需要異常重試邏輯時優先選擇(與
Lazy<T>
異常緩存行為不同) - 避免用于值類型
選擇建議
- 如果需要快速實現線程安全的延遲初始化且對性能要求不是極致,可優先選擇
Lazy<T>
。 - 如果在性能敏感的場景下,且初始化邏輯簡單,可選擇
LazyInitializer.EnsureInitialized
。
總結
Lazy<T>
和LazyInitializer.EnsureInitialized
可以實現延遲初始化,按需加載資源,提升啟動性能并節省內存。
Lazy<T>
提供了封裝良好的延遲初始化機制,支持復雜的初始化邏輯和多種線程安全模式,適用于需要延遲加載的場景。
LazyInitializer.EnsureInitialized
則更加輕量級,直接操作字段,適用于性能敏感且需要最小化內存開銷的場景。
在實際應用中,需要根據具體需求選擇合適的方式。
- 需要線程安全的場景,
Lazy<T>
的默認模式(ExecutionAndPublication
)是首選; - 而對于性能敏感且初始化邏輯簡單的場景,
LazyInitializer.EnsureInitialized
的重載提供了更高效的解決方案。 - 同時,開發者還需要注意異常處理、線程安全模式的選擇以及避免濫用延遲初始化導致的性能問題。