大家好,我是Edison。
去年换工作时系统复习了一下.NET Core多线程相关专题,学习了一线码农老哥的《.NET 5多线程编程实战》课程,我将复习的知识进行了总结形成本专题。
理解lock锁的底层原理
lock (lockMe)
{
dict.Add(i.ToString(), DateTime.Now);
}
(3)lock的本质
通过ILSpy反编译查看可以知道,lock是个语法糖,编译后其实是Monitor.Enter 和 Monitor.Exit 的封装。
try
{
Monitor.Enter(lockMe, ref lockTake);
dict.Add(i.ToString(), DateTime.Now);
}
finally
{
if (lockTake)
{
Monitor.Exit(lockMe);
}
}
(4)lock为何需要引用类型?
首先,编译器要求lock中的所对象必须是引用类型。
其次,因为lock会用到对象头中的同步块索引来进行同步,值类型没有堆中的数据。

ThreadStatic(Attribute):
当前线程拿到的是定义好的值,其他线程拿到的可能是默认值(值类型可能是0,引用类型可能是null,需要注意容错)
ThreadLocal:
最大的区别:ThreadStatic只在第一个线程初始化,ThreadLocal则会为每个线程初始化
PEB 进程环境块 TEB 线程环境块 TLS 线程本地存储(Thread Local Storage),取决于一共有多少个DataSlot
用来做数据库连接池:DB连接池 基于 ThreadLocal实现,每个线程只能看见自己的请求队列;
用来做链式追踪:比如Skywalking或Zipkin等,用到ThreadLocal做本地存储,记录完整的调用链条如:A -> B -> C -> D;
AutoResetEvent / ManualResetEvent
Semaphore
Mutex
lock(obj)
{
... // todo [1ms]
}
Windows中一个时间片大概是30ms Thread.Sleep(0) 提前结束自己的时间片,然后把自己放入到就绪队列中,如果就绪队列中的线程优先级 >= Current Thread,那么其他线程会被调度 如果就绪队列中的线程优先级 < Current Thread,那么Current Thread只能继续执行【低优先级线程得不到执行】 整体CPU级别
Thread.Yield() 提前结束自己的时间片,如果当前逻辑CPU上的就绪队列上有待执行的线程,那么这个线程就会被调度(不考虑优先级)【低优先级线程可以得到执行】 逻辑CPU级别
Sleep(1) 本质上和Sleep(1000)一样,都需要休眠
read, operate, write => 打包成原子性
C# SpinWait CLR SpinWait
class Program
{
public static SpinLock spinLock = new SpinLock();
public static int counter = 0;
static void Main(string[] args)
{
Parallel.For(1, 1000001, (i) =>
{
var lockTaken = false;
spinLock.Enter(ref lockTaken);
++counter;
spinLock.Exit();
}
});
Console.WriteLine($"counter={counter}");
Console.ReadLine();
}
read
operation
write
class Program
{
public static SpinLock spinLock = new SpinLock();
public static int counter = 0;
static void Main(string[] args)
{
Parallel.For(1, 1000001, (i) =>
{
Interlocked.Increment(ref counter, 1);
});
Console.WriteLine($"counter={counter}");
Console.ReadLine();
}
lock ManualResetEvent CAS SpinWait(轻量级自旋锁)、SpinLock
ManualResetEvent + lock + SpinWait
生产端Add进去发布的消息
消费者端通过GetConsumingEnumerable()方法阻塞等待发布的消息
观察现象
业务场景:自己用ConcurrentDictionary封装了一个Cache
FullGC 将 LOH 上的对象回收了
所有>=85000byte的都会被纳入LOH
观察源码
Values方法每次都会生成一个新的List集合对象进行返回,每个对象都是大对象
如何改进
禁止调用Values方法
借助lock + Dictionary实现类似操作避免每次生成新的List集合对象
(2)GetOrAdd的坑
观察现象
业务场景:自己用ConcurrentDictionary封装了一个Redis连接池缓存
借助GetOrAdd实现的CreateInstance方法未能实现线程安全导致连接池被大量反复创建
观察源码
GetOrAdd方法中的valueFactory不是线程安全的
如何改进
借助Lazy改造字典的Value对象,保证创建方法只被执行一次,比如:将RedisConnection改为Lazy
Release模式
查看memory中的共享变量的值
CPU寄存器
查看共享变量的值
使用CancellationToken做取消
不用Cache,都读内存address中的对象,性能会相对较低
将共享变量 改为 易变结构,比如:private bool _shouldStop 改为 private volatile bool _shouldStop