博客 (8)

  1. 临界区与 lock 关键字

    核心作用:

    通过将多线程访问串行化,保护共享资源或代码段。lock 关键字是 Monitor 类的语法糖,提供异常安全的临界区实现。

    实现示例:

    // 创建私有静态只读对象
    // private static readonly object _lockObj = new object();
    private static readonly System.Threading.Lock _locker = new(); // .NET 9+ 推荐使用 Lock 类型,避免传统 object 的性能损耗
    
    public void ThreadSafeMethod()
    {
        lock (_lockObj) 
        {
            // 临界区代码(每次仅一个线程可进入)
        }
    }

    超时机制:

    高并发场景可结合 Monitor.TryEnter 设置超时,避免无限等待:


    if (Monitor.TryEnter(lockObject, TimeSpan.FromSeconds(1)))
    {
        try { /* 操作 */ }
        finally { Monitor.Exit(lockObject); }
    }

    关键特性:

    用户态锁(无内核切换开销)

    自动调用Monitor.Enter和Monitor.Exit

    必须使用专用私有对象作为锁标识

    注意事项:

    ❌ 避免锁定this、Type实例或字符串(易引发死锁)

    ❌ 避免嵌套锁(需严格按顺序释放)

    ✅ 推荐readonly修饰锁对象

    ❌ lock 不适用于异步代码(async/await),需使用 SemaphoreSlim 实现异步锁

    lock 示例

     

  2. 互斥锁(Mutex)

    核心作用:

    系统级内核锁,支持跨进程同步,但性能开销较高(用户态/内核态切换)。

    实现示例:

    using var mutex = new Mutex(false, "Global\\MyAppMutex");
    
    try 
    {
        // 等待锁(最大等待时间500ms)
        if (mutex.WaitOne(500)) 
        {
            // 临界区代码
        }
    }
    finally 
    {
        if (mutex != null)
        {
            mutex.ReleaseMutex();
        }
    }

    关键特性:

    支持跨应用程序域同步

    线程终止时自动释放锁

    支持命名互斥体(系统全局可见)

    适用场景:

    单实例应用程序控制

    进程间共享文件访问

    硬件设备独占访问

    Mutex 示例

     

  3. 信号量(Semaphore)

    核心作用:

    通过许可计数器控制并发线程数,SemaphoreSlim为轻量级版本(用户态实现)。

    实现对比:

    类型
    跨进程
    性能
    最大许可数
    Semaphore
    ✔️

    系统限制
    SemaphoreSlim


    Int32.Max

    代码示例:

    // 创建初始3许可、最大5许可的信号量
    var semaphore = new SemaphoreSlim(3, 5);
    
    semaphore.Wait();  // 获取许可
    try 
    {
        // 资源访问代码
    }
    finally 
    {
        semaphore.Release();
    }

    异步编程

    private readonly SemaphoreSlim _asyncLock = new(1, 1);
    public async Task UpdateAsync()
    {
        await _asyncLock.WaitAsync();
        try { /* 异步操作 */ }
        finally { _asyncLock.Release(); }
    }

    典型应用:

    数据库连接池(限制最大连接数)

    API 请求限流

    批量任务并发控制

    Semaphore 示例

     

  4. 事件(Event)

    核心机制:

    通过信号机制实现线程间通知,分为两种类型:

    类型
    信号重置方式
    唤醒线程数
    AutoResetEvent
    自动
    单个
    ManualResetEvent
    手动
    所有

    使用示例:

    var autoEvent = new AutoResetEvent(false);
    
    // 等待线程
    Task.Run(() => 
    {
        autoEvent.WaitOne();
        // 收到信号后执行
    });
    
    // 信号发送线程
    autoEvent.Set();  // 唤醒一个等待线程

    高级用法:

    配合WaitHandle.WaitAll实现多事件等待

    使用ManualResetEventSlim提升性能

    AutoResetEvent 示例

     

  5. 读写锁(ReaderWriterLockSlim)

    核心优势:

    实现读写分离的并发策略,适合读多写少场景(如缓存系统)。

    锁模式对比:

    模式
    并发性
    升级支持
    读模式(EnterReadLock)
    多线程并发读

    写模式(EnterWriteLock)
    独占访问

    可升级模式
    单线程读→写
    ✔️

    代码示例:

    var rwLock = new ReaderWriterLockSlim();
    
    // 读操作
    rwLock.EnterReadLock();
    try 
    {
        // 只读访问
    }
    finally 
    {
        rwLock.ExitReadLock();
    }
    
    // 写操作
    rwLock.EnterWriteLock();
    try 
    {
        // 排他写入
    }
    finally 
    {
        rwLock.ExitWriteLock();
    }

    最佳实践:

    优先使用ReaderWriterLockSlim(旧版有死锁风险)

    避免长时间持有读锁(可能饿死写线程)

    ReaderWriterLockSlim 示例

  6. 原子操作(Interlocked)

    原理:

    通过CPU指令实现无锁线程安全操作。

    常用方法:

    int counter = 0;
    Interlocked.Increment(ref counter);      // 原子递增
    Interlocked.Decrement(ref counter);      // 原子递减
    Interlocked.CompareExchange(ref value, newVal, oldVal);  // CAS操作

    适用场景:

    简单计数器

    标志位状态切换

    无锁数据结构实现

     

  7. 自旋锁(SpinLock)

    核心特点:

    通过忙等待(busy-wait)避免上下文切换,适用极短临界区(<1微秒)。

    实现示例:

    private SpinLock _spinLock = new SpinLock();
    
    public void CriticalOperation()
    {
        bool lockTaken = false;
        try 
        {
            _spinLock.Enter(ref lockTaken);
            // 极短临界区代码
        }
        finally 
        {
            if (lockTaken) _spinLock.Exit();
        }
    }

    优化技巧:

    单核CPU需调用Thread.SpinWait或Thread.Yield

    配合SpinWait结构实现自适应等待

     

  8. 同步机制对比指南

    机制
    跨进程
    开销级别
    最佳适用场景
    lock


    通用临界区保护
    Mutex
    ✔️

    进程间资源独占
    Semaphore
    ✔️

    并发数限制(跨进程)
    SemaphoreSlim


    并发数限制(进程内)
    ReaderWriterLockSlim


    读多写少场景
    SpinLock

    极低
    纳秒级临界区
    Interlocked
    -无锁
    简单原子操作

选择原则:

  1. 优先考虑用户态锁(lock/SpinLock/SemaphoreSlim)

  2. 跨进程需求必须使用内核对象(Mutex/Semaphore)

  3. 读写比例超过10:1时考虑读写锁

  4. 自旋锁仅用于高频短操作(如链表指针修改)

通过以上结构化的分类和对比,开发者可以更精准地选择适合特定场景的线程同步方案。建议在实际使用中配合性能分析工具(如BenchmarkDotNet)进行量化验证。

💡 ASP.NET 的异步编程(async/await)本质是单进程内的线程调度,不算“跨进程”。每个 IIS 应用程序池对应一个独立的工作进程(w3wp.exe),不同用户访问同一应用程序池下的 ASP.NET 网站,两者的请求均由同一个 w3wp.exe 进程处理。可能跨进程的场景有:Web Garden 配置、多应用程序池部署等。

在 C# 中,除了常规锁机制(如 lock、Mutex、Semaphore 等),还有一些内置类型通过内部锁或无锁设计实现线程安全。以下是常见的几类:

  1. 线程安全集合(System.Collections.Concurrent)这些集合通过细粒度锁或无锁算法(如 CAS)实现线程安全,适合高并发场景。

    • ConcurrentDictionary:分段锁机制,将数据分片存储,每个分片独立加锁,减少锁竞争。

    • ConcurrentQueue / ConcurrentStack基于原子操作(Interlocked)保证线程安全。

    • ConcurrentBag:每个线程维护本地存储,减少争用,适合频繁添加和移除的场景。

    • BlockingCollection:基于 ConcurrentQueue 和信号量(SemaphoreSlim)实现生产-消费者模式,支持阻塞和超时。

  2. 不可变集合(System.Collections.Immutable) 通过数据不可变性实现线程安全(无需锁),每次修改返回新对象。

  3. Lazy 的线程安全初始化(Lazy<T>) 通过锁或 Interlocked 确保延迟初始化的线程安全。

  4. 通道(System.Threading.Channels)用于异步生产-消费者模型,内部通过锁和信号量管理容量限制。

  5. 内存缓存(System.Runtime.Caching.MemoryCache)内部使用锁保护共享状态,确保线程安全。

  6. 原子操作类型(Interlocked 类、Volatile 关键字、Unsafe 类)通过 CPU 指令实现无锁线程安全。

  7. 其他同步工具(Barrier、CountdownEvent)虽然不是严格意义上的锁,但用于协调线程。

xoyozo 半个月前
190

未能找到路径“\roslyn\csc.exe”的一部分。

解决方法:

在 nuget 中直接卸载 Microsoft.CodeDom.Providers.DotNetCompilerPlatform


如果 web.config 报错,删除与 Microsoft.CodeDom.Providers.DotNetCompilerPlatform 相关的项,如:

<system.codedom>
	<compilers>
		<compiler language="c#;cs;csharp" extension=".cs" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701;612;618" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
		<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008,40000,40008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
	</compilers>
</system.codedom>


然后遇到错误:

功能 空传播运算符 在 C# 5 中不可用。请使用 6 或更高的语言版本


所以还是少不了 Microsoft.CodeDom.Providers.DotNetCompilerPlatform

又把这货装上了,竟然不报错了。

这期间我更改过在 Release 模式下调试,将 .NET Framework 4.8 升级到 .NET Framework 4.8.1 等,然后又退回到原来的状态和版本,竟然都正常了。

就这样吧!

xoyozo 1 年前
1,456

本文使用 Oracle 官方提供的 MySql.EntityFrameworkCore,如使用 Pomelo.EntityFrameworkCore.MySql 请移步

MySql.EntityFrameworkCore 是 MySql.Data.EntityFrameworkCore 的升级版


本文以 Visual Studio 2019、ASP.NET Core 5.0 开发环境为例。

  1. 新建 ASP.NET Core Web 应用程序。

  2. 安装 NuGet 包:

    MySql.EntityFrameworkCore

    Microsoft.EntityFrameworkCore.Design

    image.png

  3. 根据已有数据库创建数据模型。在 NuGet 的程序包管理(Package Manager)控制台中(PowerShell)执行命令:

    Scaffold-DbContext "server=数据库服务器;port=3306;user=数据库用户名;password=数据库密码;database=数据库名" MySql.EntityFrameworkCore -OutputDir Data -f

    .Net Core CLi:

    dotnet ef dbcontext scaffold "server=数据库服务器;port=3306;user=数据库用户名;password=数据库密码;database=数据库名" MySql.EntityFrameworkCore -o Data -f
  4. 搞定。


注:开发环境和生产环境都不需要安装 Connector/NET,只需要安装 ASP.NET Core。

补充:其它数据库提供程序请参考:https://docs.microsoft.com/zh-cn/ef/core/providers/

更多高级用法请参考官方文档


xoyozo 4 年前
4,786

本文使用 Oracle 官方提供的 MySql.Data.EntityFrameworkCore,如使用 Pomelo.EntityFrameworkCore.MySql 请移步

对比 MySql.Data.EntityFrameworkCore 与 Pomelo.EntityFrameworkCore.MySql

在 ASP.NET Core 5.0 中使用 MySql.EntityFrameworkCore


本文以 Visual Studio 2019、ASP.NET Core 3.1 开发环境为例。

  1. 新建 ASP.NET Core Web 应用程序。

  2. 安装 NuGet 包:

    MySql.Data.EntityFrameworkCore

    Microsoft.EntityFrameworkCore.Design

    image.png

    如果使用 EF Core 2.0 还需安装:Microsoft.EntityFrameworkCore.Tools

  3. 根据已有数据库创建数据模型。在 NuGet 的程序包管理(Package Manager)控制台中(PowerShell)执行命令:

    Scaffold-DbContext "server=数据库服务器;port=3306;user=数据库用户名;password=数据库密码;database=数据库名" MySql.Data.EntityFrameworkCore -OutputDir Data -f

    .Net Core CLi:

    dotnet ef dbcontext scaffold "server=数据库服务器;port=3306;user=数据库用户名;password=数据库密码;database=数据库名" MySql.Data.EntityFrameworkCore -o Data -f
  4. 搞定。


注:开发环境和生产环境都不需要安装 Connector/NET,只需要安装 ASP.NET Core。

补充:其它数据库提供程序请参考:https://docs.microsoft.com/zh-cn/ef/core/providers/

更多高级用法请参考官方文档

xoyozo 5 年前
3,766

本文使用 Pomelo 提供的 Pomelo.EntityFrameworkCore.MySql,如使用 MySql.Data.EntityFrameworkCore 请移步

对比 MySql.Data.EntityFrameworkCore 与 Pomelo.EntityFrameworkCore.MySql


本文以 Visual Studio 2019、ASP.NET Core 3.0 开发环境为例。

  1. 新建 ASP.NET Core Web 应用程序。

  2. 安装 NuGet 包:

    Microsoft.EntityFrameworkCore.Tools

    Pomelo.EntityFrameworkCore.MySql(3.0.0 预发行版以上)

    image.png

  3. 根据已有数据库创建数据模型。在 NuGet 的程序包管理(Package Manager)控制台中(PowerShell)执行命令:

    Scaffold-DbContext "server=数据库服务器;uid=数据库用户名;pwd=数据库密码;database=数据库名;" Pomelo.EntityFrameworkCore.MySql -OutputDir Data -Force

    .Net Core CLi:

    dotnet ef dbcontext scaffold "server=数据库服务器;uid=数据库用户名;pwd=数据库密码;database=数据库名;" Pomelo.EntityFrameworkCore.MySql -o Data -f
  4. 搞定。


补充:其它数据库提供程序请参考:https://docs.microsoft.com/zh-cn/ef/core/providers/


代码参数说明:

-OutputDir (-o) *** 实体文件所存放的文件目录

-ContextDir *** DbContext文件存放的目录

-Context *** DbContext文件名

-Schemas *** 需要生成实体数据的数据表所在的模式

-Tables(-t) *** 需要生成实体数据的数据表的集合

-DataAnnotations

-UseDatabaseNames 直接使用数据库中的表名和列名(某些版本不支持)

-Force (-f) 强制执行,重写已经存在的实体文件


更多高级用法请参考官方文档


xoyozo 6 年前
8,573

image.png

打开项目文件,在 <ItemGroup /> 中添加一个 <DotNetCliToolReference />:(注意版本号)

<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />

image.png

参考:https://stackoverflow.com/questions/45091909/dotnet-ef-database-update-no-executable-found-matching-command-dotnet-ef?r=SearchResults

结果:

image.png

xoyozo 6 年前
6,608

错误如下:

image.png

解决方法:

在“解决方案资源管理器”中点击项目右键,选择“编辑项目文件”

image.png

手动添加内容:

<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />

image.png

完成。如果编译出错,关闭项目并重新打开项目再试。

转自 听雨的人 6 年前
7,530

这是一篇可能解决困扰了 .NET 程序猿多年的文章!

首先,使用 Visual Studio 2019 创建一个 ASP.NET WEB 应用程序,直接选用了 MVC 模板。

直接发布项目,默认的设置如下图:

image.png

对于小型项目,按默认设置发布基本可满足正常运行,首次运行打开第一个页面基本在 5~6 秒(视服务器配置),其它页面的首次打开也基本在 1~2 秒完成,非首次瞬间打开。

一旦项目功能变得复杂,文件增多,会导致发布后首次运行打开第一个页面 30 秒以上,其它页面的首次打开 10 秒左右,非首次瞬间打开。

这是因为项目在发布时没有进行预编译,而是在用户访问网页时动态编译,一旦应用程序池回收,或项目文件改动,都会重新编译,再次经历缓慢的“第一次”,这是不能忍的。

于是咱们来研究一下“预编译”。

在发布页面勾选“在发布期间预编译”,这时发布会在“输出”窗口显示正在执行编译命令(编译时间会比较长):

image.png

该选项对发布后的文件结构和内容影响不大,因此对首次执行效率的提升也不大,重要的在后面。

在“高级预编译设置”窗口中:

image.png

我们针对预编译选项和合并选项分别做测试。

不勾选“允许更新预编译站点”,会致 bin 目录中生成许多 .compiled 文件,而所有 .cshtml 文件的内容都是:

这是预编译工具生成的标记文件,不应删除!

如果是 Web From 网站,.aspx/.ashx 等文件内容同上。

尝试打开网站页面,你会发现,除了项目启动后的第一个页面仍然需要 1~2 秒(无 EF),其余每个页面的首次都是瞬间打开的(EF 的首次慢不在本文讨论范围)。这让我对预编译有一种相见恨晚的感觉!

这里偷偷地告诉你,把 Views 目录删掉也不影响网页正常打开哦~为什么不让删,咱也不敢问,咱也不敢删。

目的达到了,有一些后遗症需要解决,比如 bin 目录内杂乱无章。

选“不合并。为每个页面和控件创建单独的程序集”,结果是 bin 多出许多 App_Web_*.dll 文件。

选“将所有输出合并到单个程序集”,填写程序集名称。这时会发现“输出”窗口执行了命令:

image.png

如果程序集名称跟已有名称冲突,会发生错误:

合并程序集时出错: ILMerge.Merge: The target assembly '******' lists itself as an external reference.

查看 bin 目录,.compiled 文件还在,但 App_Web_*.dll 合并成了一个程序集。FTP 的“不合并”也是把所有 App_Web_*.dll 上传一遍,所以不存在相对于“合并”的增量发布优势。

是不是勾了“视为库组件删除(AppCode.compiled 文件)”.compiled 文件就会不见?意外之外,情理之中,bin 没有什么变化。

继续选择“将各个文件夹输出合并到其自己的程序集”,呃,bin 中文件数不降反增。

最后选择“将所有页和控件输出合并到单个程序集”,同样程序集名称不能冲突。结局令人失望,bin 中仍然一大堆 .compiled 和 App_Web_*.dll。


整理成表格:

1
允许更新预编译站点

aspnet_compiler.exe -v / -p \Source -u \TempBuildDir 

重复发布后 bin 目录文件数:不会增多(页面不进行预编译)

2

不允许更新预编译站点,不合并

aspnet_compiler.exe -v / -p \Source \TempBuildDir 

重复发布后 bin 目录文件数:.compiled 不会增多,App_Web_*.dll 增多

3

不允许更新预编译站点,不合并。为每个页面和控件创建单独的程序集

aspnet_compiler.exe -v / -p \Source -c -fixednames \TempBuildDir 

重复发布后 bin 目录文件数:不会增多(编译后的文件名固定,FTP 方式的部分 App_Web_*.dll 文件可能实现增量上传)

4

不允许更新预编译站点,将所有输出合并到单个程序集,不勾选“视为库组件”

aspnet_compiler.exe -v / -p \Source -c \TempBuildDir 

aspnet_merge.exe \TempBuildDir -o 程序集名称 -copyattrs AssemblyInfo.dll -a

重复发布后 bin 目录文件数:不会增多(.compiled 固定名称,App_Web_*.dll 合并为一个)

5

不允许更新预编译站点,将所有输出合并到单个程序集,勾选“视为库组件”

aspnet_compiler.exe -v / -p \Source \TempBuildDir 

aspnet_merge.exe \TempBuildDir -o 程序集名称 -copyattrs AssemblyInfo.dll -a -r 

重复发布后 bin 目录文件数:不会增多(.compiled 固定名称,App_Web_*.dll 合并为一个)

6

不允许更新预编译站点,将每个文件夹输出合并到其自己的程序集,前缀

aspnet_compiler.exe -v / -p \Source -c \TempBuildDir 

aspnet_merge.exe \TempBuildDir -prefix 前缀 -copyattrs AssemblyInfo.dll -a  

重复发布后 bin 目录文件数:.compiled 不会增多,App_Web_*.dll 增多

7

不允许更新预编译站点,将所有页和控件输出合并到单个程序集

aspnet_compiler.exe -v / -p \Source -c \TempBuildDir 

aspnet_merge.exe \TempBuildDir -w 程序集名称 -copyattrs AssemblyInfo.dll -a  

重复发布后 bin 目录文件数:.compiled 不会增多,App_Web_*.dll 增多

bin 目录下,页面的程序集文件(App_Web_*.dll)增多的原因是编译后的文件名不固定,发布到会导致残留许多无用的程序集文件。

相比较,如果第 3 种能实现 FTP 稳定的增量上传的话是比较完美的(还有一个缺点是:如果页面有删除,目标 bin 内对应该页面的 .compiled 和 .dll 不会删除,这跟“允许更新预编译站点”是一个情况),那么第 4 种 或第 5 种也是不错的选择(同样有缺点:执行合并比较慢)。

测试一个有 300 个页面的项目,compiler 用时约 2 分钟,merge 用时约 5 分钟,发布用时约 4 分钟。

这里有个槽点,执行预编译和合并是佛系的,CPU 和磁盘使用率永远保持在最低水平。

所以如果是要经常修改的项目,那么建议选择“不合并”的第 3 种发布方式:

微信图片_20190715160035.png

“视为库组件(删除 AppCode.compiled 文件)”:移除主代码程序集(App_Code 文件夹中的代码)的 .compiled 文件。 如果应用程序包含对主代码程序集的显式类型引用,则不要使用此选项。


补充:

  • 不允许更新预编译站点发布后,因为前端页面没有内容,因此无法单独修改发布(单独发布一个页面后,在访问时不会生效!),只能全站发布,耗时较长;bin 目录有变动将导致使用 InProc 方式存储的 Session 丢失。

  • 预编译的另一个优点是可以检查 .aspx / .cshtml 页面 C# 部分的错误(特别是命名空间和路径引用)。

  • 改为预编译发布后,可以将服务器上残留的 .master / .ascx / .asax 删除,但不能删除 .aspx / .ashx /.config 等。

  • VS 发布到 FTP 经常会遗漏一些页面文件,不合并时会遗漏独立文件,合并后也会遗漏合并后的 .dll 的文件,合并的好处就是方便检查是否完全上传发布。

  • 慎选“在发布前删除所有现有文件”!一旦勾选发布,世界就清静了,所有网友上传的图片附件以及网站运行产生的其它文件,消失得无影无踪。不管发布到文件系统还是发布到 FTP 都一样。当然,如果是先发布到文件系统,再通过 FileZilla 等 FTP 软件上传到服务器的,建议勾选此项。

  • .NET Core 应用程序部署:https://docs.microsoft.com/zh-cn/dotnet/core/deploying/index

  • 由于上传文件时 bin 目录文件较多,理论上 bin 目录内的文件有任何改动都会重启应用池,而且 VS 是单线程上传的,导致期间网站访问缓慢,服务器 CPU 升高,我的做法是:发布到文件系统,再使用专用 FTP 工具上传,上传用时约半分钟(如果大小不同或源文件较新则覆盖文件)。还嫌慢?那就打包上传,解压覆盖。

  • 关于本文研究对象的官方解释:高级预编译设置对话框

  • 出现“未能加载文件或程序集“***”或它的某一个依赖项。试图加载格式不正确的程序。”的问题时,先用改用 Debug 方式发布会报详细错误,一般是 .aspx 等客户端页面有 C# 语法问题,注意提示报错的是 /obj/ 目录下的克隆文件,应更改原文件。排除错误后关闭所有页面,再使用 Release 方式发布。

xoyozo 6 年前
12,149