c#线程安全
先看一个例子:
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); for (int i = 0; i < 5; i++) { Task.Run(() => { Console.WriteLine($"This is {i} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($"This is {i} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); }); } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
结果:
*** ** btnSafe_Click start -主线程ID:1-2021-09-18 08:44:36***** *** This is 1 start -3 This is 3 start -4 This is 4 start -7 This is 5 start -6 This is 5 start -9 *** **** btnSafe_Click end -主线程ID:1-2021-09-18 08:44:36********** * This is 5 end -7 This is 5 end -6 This is 5 end -9 This is 5 end -4 This is 5 end -3
从结果我们可以看出来多个线程中的i值在实际执行中是相同的,都是5,按照正常逻辑应该是0 1 2 3 4这4个值,为什么会出现这种情况呢?首先,必须要知道线程是属于操作系统的,操作系统根据调度策略来分配线程,不是实时响应的,可能等到i已经循环到5了才开始执行这个线程。
我们可以做一下更改,设置一个局部变量,这样每循环一次都会产生一个新的局部变量值,互不干扰。
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"This is {i} -- {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($"This is {i} -- {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); }); } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
结果:
*** ** btnSafe_Click start -主线程ID:1-2021-09-18 09:05:25***** *** This is 0 -- 0 start -3 This is 5 -- 1 start -5 *** **** btnSafe_Click end -主线程ID:1-2021-09-18 09:05:25********** * This is 5 -- 4 start -8 This is 5 -- 3 start -7 This is 5 -- 2 start -6 This is 5 -- 4 end -8 This is 5 -- 0 end -3 This is 5 -- 2 end -6 This is 5 -- 3 end -7 This is 5 -- 1 end -5
案例2:
多线程同时访问一个集合一般是没什么问题的,但是线程安全问题一般是出现在修改一个对象的时候。
多线程安全定义:一段代码,单线程执行和多线程执行的结果不一致,就表明有线程安全问题。
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); #region MyRegion List<int> intList = new List<int>(); for (int i = 0; i < 10000; i++) { int k = i; Task.Run(() => { intList.Add(i); }); } Thread.Sleep(5000); Console.WriteLine(intList.Count); #endregion Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
结果:
*** ** btnSafe_Click start -主线程ID:1-2021-09-18 09:32:12***** *** 9997 *** **** btnSafe_Click end -主线程ID:1-2021-09-18 09:32:17********** *
结果的长度是9997,小于10000,其实下一次执行的时候结果可能就变成另一个了,这都是不确定的,上面的代码是想让每一个线程都单独添加一个数,比如说线程1添加数0,线程2添加数1,,,,,知道10000,。但是线程的请求不是实时的,导致最终会导致intList中存在大量的重复数据。数组在内存中是连续存在的, 如果在同一时刻上同时由多个线程同时在这个内存位置上做更改,就会出现覆盖,所以存在数据丢失。
解决数据丢失问题:
private static readonly object LOCK = new object(); /// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); #region MyRegion List<int> intList = new List<int>(); for (int i = 0; i < 10000; i++) { int k = i; Task.Run(() => { lock(LOCK) { intList.Add(i); } }); } Thread.Sleep(5000); Console.WriteLine(intList.Count); #endregion Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
加了lock就保证每次lock方法体中每一次都只能有一个线程进入,此时intList数量为10000。但是还是解决不了数据重复的问题。要解决数据重复的话就做一个局部变量,和第一个案例一样就可以了。
Lock原理:Lock其实是一个语法糖,等价于Monitor,是锁定一个内存引用地址,对这个引用对象做了一个状态标识,但是不能锁定值类型以及null。等价于下面的写法:
try { Monitor.Enter(obj) } catch() {} finally { Monitor.Exit(obj) }
案例3:Lock相关问题
如果使用共同的一个变量来进行锁定:
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); TestLock.Show(); #region MyRegion for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (TestLock.LOCK) { Console.WriteLine($" MainShow This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" MainShow This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } #endregion Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
/// <summary> /// 测试Lock类 /// </summary> public class TestLock { public static readonly object LOCK = new object(); public static void Show() { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (LOCK) { Console.WriteLine($" TestLockShow This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" TestLockShow This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } }
结果:
*** ** btnSafe_Click start -主线程ID:1-2021-09-18 10:43:18***** *** TestLockShow This is 0 start -13 *** **** btnSafe_Click end -主线程ID:1-2021-09-18 10:43:18********** * TestLockShow This is 0 end -13 TestLockShow This is 1 start -22 TestLockShow This is 1 end -22 MainShow This is 4 start -13 MainShow This is 4 end -13 TestLockShow This is 3 start -24 TestLockShow This is 3 end -24 TestLockShow This is 2 start -23 TestLockShow This is 2 end -23 TestLockShow This is 4 start -25 TestLockShow This is 4 end -25 MainShow This is 0 start -26 MainShow This is 0 end -26 MainShow This is 1 start -27 MainShow This is 1 end -27 MainShow This is 2 start -28 MainShow This is 2 end -28 MainShow This is 3 start -29 MainShow This is 3 end -29
通过结果可以看出来这是同步执行的因为不管是执行TestLockShow还是MainShow,必须是同一个线程进入这个区域之后开始结束是连续的,2个方法没有混起来,所以共用一个相同的锁变量就会出现相互阻塞。
如果更改代码,锁变量不共用:
/// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); TestLock.Show(); #region MyRegion for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock ( LOCK) { Console.WriteLine($" MainShow This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" MainShow This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } #endregion Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
结果:
可以看出来此时是并发执行的。所以最好不要共用。
案例3:锁变量如果不是静态变量会如何?
代码:
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); TestLock.Show(); { TestLock testLock = new TestLock(); testLock.ShowTemp(1); TestLock testLock1 = new TestLock(); testLock1.ShowTemp(2); } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
测试类:
/// <summary> /// 测试Lock类 /// </summary> public class TestLock { private static readonly object LOCK = new object(); public static void Show() { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (LOCK) { Console.WriteLine($" TestLockShow This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" TestLockShow This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } private readonly object LOCKTemp = new object(); public void ShowTemp(int index) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (LOCKTemp) { Console.WriteLine($"index={index}- ShowTemp This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" index={index}- ShowTemp This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } }
结果:
*** ** btnSafe_Click start -主线程ID:1-2021-09-18 12:00:47***** *** *** **** btnSafe_Click end -主线程ID:1-2021-09-18 12:00:47********** * TestLockShow This is 0 start -7 index=1- ShowTemp This is 1 start -4 TestLockShow This is 0 end -7 TestLockShow This is 3 start -3 index=2- ShowTemp This is 0 start -7 index=1- ShowTemp This is 1 end -4 index=1- ShowTemp This is 0 start -6 TestLockShow This is 3 end -3 TestLockShow This is 4 start -5 index=2- ShowTemp This is 0 end -7 index=2- ShowTemp This is 1 start -4 index=1- ShowTemp This is 0 end -6 index=1- ShowTemp This is 2 start -10 TestLockShow This is 4 end -5 TestLockShow This is 1 start -8 index=2- ShowTemp This is 1 end -4 index=2- ShowTemp This is 4 start -3 index=1- ShowTemp This is 2 end -10 index=1- ShowTemp This is 3 start -11 TestLockShow This is 1 end -8 TestLockShow This is 2 start -9 index=2- ShowTemp This is 4 end -3 index=2- ShowTemp This is 2 start -13 index=1- ShowTemp This is 3 end -11 index=1- ShowTemp This is 4 start -12 TestLockShow This is 2 end -9 index=2- ShowTemp This is 2 end -13 index=2- ShowTemp This is 3 start -14 index=1- ShowTemp This is 4 end -12 index=2- ShowTemp This is 3 end -14
可以看出来是并发执行的,原因就在于非静态成员变量在new类对象的时候都会重新初始化一次,这2次调用,锁变量就不是同一个值了,在堆中存的地址也不一样了。所以最好设置为静态变量。
案例3:锁变量为string类型
代码:
private readonly string LockString = "哈哈"; /// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); TestLock.Show(); { TestLock testLock = new TestLock(); testLock.ShowString(1); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (LockString) { Console.WriteLine($" MainShow This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" MainShow This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
测试类:
/// <summary> /// 测试Lock类 /// </summary> public class TestLock { private readonly string LockString = "哈哈"; public void ShowString(int index) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (LockString) { Console.WriteLine($"index={index}- ShowString This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" index={index}- ShowString This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } }
结果:
** btnSafe_Click start -主线程ID:1-2021-09-18 15:22:07***** *** *** **** btnSafe_Click end -主线程ID:1-2021-09-18 15:22:07********** * ShowString This is 0 start -3 ShowString This is 0 end -3 MainShow This is 0 start -4 MainShow This is 0 end -4 ShowString This is 2 start -5 ShowString This is 2 end -5 MainShow This is 2 start -6 MainShow This is 2 end -6 ShowString This is 1 start -7 ShowString This is 1 end -7 ShowString This is 4 start -8 ShowString This is 4 end -8 ShowString This is 3 start -9 ShowString This is 3 end -9 MainShow This is 1 start -10 MainShow This is 1 end -10 MainShow This is 3 start -11 MainShow This is 3 end -11 MainShow This is 4 start -12 MainShow This is 4 end -12
因为锁定的是内存引用,字符串是享元的,这2个字符串都是相同的值,虽然在栈上的地址不一样但是在堆中的地址是一样的,所以相当于锁的都是同一个,所以是不能并发的。
CLR中维护着一个驻留池(Intern Pool)的散列表(HashTable),这个表记录了所有在代码中使用字面量声明的字符串实例的引用 ,使用字面量声明的字符串都会被记录到散驻留池(散列表 键为字符串 值为字符串存储地址),
如:string str="abc"; 或 string str="a"+"bc"这种就可以称为字面量
但是 string str=变量+变量 或者 变量+字符串 这种都不能称为字面量
比如前后2个字符串的内容一致,那么在堆中的地址是一样的,这样就是同一个引用了。最好不要用。
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); {
TestLockGeneric<int>.Show(1); TestLockGeneric<int>.Show(2); TestLockGeneric<TestLock>.Show(3); } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
测试类:
/// <summary> /// 测试Lock类 /// </summary> public class TestLockGeneric<T> { private static readonly object LOCK = new object(); public static void Show(int index) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (LOCK) { Console.WriteLine($" TestLockGeneric This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" TestLockGeneric This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } }
结果:
*** ** btnSafe_Click start -主线程ID:1-2021-09-18 16:00:32***** *** TestLockGeneric This is 0 start -3 *** **** btnSafe_Click end -主线程ID:1-2021-09-18 16:00:33********** * TestLockGeneric This is 0 end -3 TestLockGeneric This is 4 start -4 TestLockGeneric This is 0 start -3 TestLockGeneric This is 4 end -4 TestLockGeneric This is 1 start -5 TestLockGeneric This is 0 end -3 TestLockGeneric This is 1 start -13 TestLockGeneric This is 1 end -5 TestLockGeneric This is 2 start -6 TestLockGeneric This is 1 end -13 TestLockGeneric This is 4 start -3 TestLockGeneric This is 2 end -6 TestLockGeneric This is 0 start -7 TestLockGeneric This is 4 end -3 TestLockGeneric This is 3 start -4 TestLockGeneric This is 0 end -7 TestLockGeneric This is 3 start -8 TestLockGeneric This is 3 end -4 TestLockGeneric This is 2 start -14 TestLockGeneric This is 3 end -8 TestLockGeneric This is 2 start -9 TestLockGeneric This is 2 end -14 TestLockGeneric This is 2 end -9 TestLockGeneric This is 1 start -10 TestLockGeneric This is 1 end -10 TestLockGeneric This is 3 start -11 TestLockGeneric This is 3 end -11 TestLockGeneric This is 4 start -12 TestLockGeneric This is 4 end -12
因为泛型类在类型参数相同时候是同一类,参数类型不同时候就不是同一类,所以1与2是不并发,1与3是并发的。
案例4:Lock(this)
代码:
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); { TestLock testLock = new TestLock(); testLock.ShowThis(1); TestLock testLock2 = new TestLock(); testLock2.ShowThis(2); } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
测试类:
/// <summary> /// 测试Lock类 /// </summary> public class TestLock { public void ShowThis(int index) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { //this是当前实例 lock (this) { Console.WriteLine($" ShowThis This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" ShowThis This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } }
这个不用看就知道肯定是会出现并发,因为this是当前实例,没法判断。
还有一种写法:
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); { TestLock testLock = new TestLock(); testLock.ShowThis(1); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (testLock) { Console.WriteLine($" MainShow This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(2000); Console.WriteLine($" MainShow This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); } }); } } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); }
这种肯定是不会并发的,因为ShowThis中的用的是lock(this),和 lock (testLock)中的testLock是同一个实例,而且都是引用类型。
案例5:Lock(this) 的死锁问题
代码:
/// <summary> /// 多线程安全 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSafe_Click(object sender, EventArgs e) { Console.WriteLine($"*** ** btnSafe_Click start -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}***** ***"); { TestLock testLock = new TestLock(); testLock.ShowThisAnother(1); } Console.WriteLine($"*** **** btnSafe_Click end -主线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}********** *"); } }
测试类:
/// <summary> /// 测试Lock类 /// </summary> public class TestLock { private int _Num = 0; public void ShowThisAnother(int index) { this._Num++; for (int i = 0; i < 5; i++) { int k = i; //this是当前实例 lock (this) { Console.WriteLine($" ShowThis This is {k} start -{Thread.CurrentThread.ManagedThreadId.ToString()} "); Thread.Sleep(1000); Console.WriteLine($" ShowThis This is {k} end -{Thread.CurrentThread.ManagedThreadId.ToString()} "); if (this._Num < 5) { this.ShowThisAnother(index); } else { break; } } } } }
结果:不会死锁。
只所以不会死锁是因为lock(this)中的this是当前实例, if (this._Num < 5)的时候再次调用ShowThisAnother这个方法,此时的this和上一次的this就不同了,所以不会形成死锁。因为此时锁不住,第二次调这个方法的时候是可以直接进行lock方法体中的,如下图,这里的this中包含了3个参数,其中没调一次这个方法,_Num都会递增,所以每次的this都不同,但是如果去掉这个_Num成员变量,就会发生死锁,因为不管调用多少次,LOCKTemp和LockString的值都不变,都是一样的值,所以形成了死锁。
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术