1. 当多线程遇见共享资源:一场必须解决的"修罗场"

在ASP.NET Core开发中,我们常常会遇到这样的场景:订单处理系统需要同时更新库存,实时聊天室要处理并发的消息推送,分布式缓存需要同步多个节点的数据更新。这些场景就像超市收银台的混乱现场——如果所有人都同时伸手拿货架上的商品,必然会出现计算错误、数据混乱等问题。

最近,在处理一个秒杀系统时,我们遇到了典型的并发问题:当1000个用户同时抢购最后10件商品时,库存出现了-5件的诡异数据。这就是典型的线程竞争问题,不同线程同时读取并修改共享资源导致的灾难。

// 问题示例:线程不安全的库存扣减
public class UnsafeInventoryService
{
    private int _stock = 10;
    
    public void DeductStock()
    {
        if (_stock > 0)
        {
            Thread.Sleep(10); // 模拟处理耗时
            _stock -= 1;
            Console.WriteLine($"剩余库存:{_stock}");
        }
    }
}

// 当10个线程同时执行时,可能出现:
// 剩余库存:9
// 剩余库存:8
// ...
// 剩余库存:-5  <- 灾难发生了!

2. ASP.NET Core同步机制兵器库

2.1 基础护盾:lock关键字

就像超市储物柜的钥匙,lock是最简单的互斥解决方案。它相当于给代码段加上"施工中,请绕行"的告示牌。

public class LockInventoryService
{
    private readonly object _lockObj = new object();
    private int _stock = 10;

    public void DeductStock()
    {
        lock (_lockObj)
        {
            if (_stock > 0)
            {
                Thread.Sleep(10);
                _stock -= 1;
                Console.WriteLine($"安全库存:{_stock}");
            }
        }
    }
}

// 适用场景:简单的临界区保护
// 优点:使用简单,性能较好
// 缺点:无法跨方法控制,容易造成死锁

2.2 灵活卫士:Monitor类

如果说lock是自动门,那么Monitor就是带密码锁的手动门。它提供了更精细的控制能力,比如超时机制。

public class MonitorInventoryService
{
    private readonly object _monitorObj = new object();
    private int _stock = 10;

    public bool TryDeductStock(int timeoutMs = 100)
    {
        if (Monitor.TryEnter(_monitorObj, timeoutMs))
        {
            try
            {
                if (_stock > 0)
                {
                    Thread.Sleep(10);
                    _stock -= 1;
                    Console.WriteLine($"监控库存:{_stock}");
                    return true;
                }
                return false;
            }
            finally
            {
                Monitor.Exit(_monitorObj);
            }
        }
        return false;
    }
}

// 适用场景:需要超时控制的资源访问
// 优点:避免死锁,更灵活
// 缺点:需要手动管理退出

2.3 跨界特工:Mutex

当需要跨进程同步时,Mutex就像连接不同大楼的对讲系统,可以协调不同应用程序之间的资源访问。

public class MutexInventoryService : IDisposable
{
    private Mutex _mutex = new Mutex(false, "Global\\MyAppInventory");
    private int _stock = 10;

    public void DeductStock()
    {
        if (_mutex.WaitOne(1000))
        {
            try
            {
                if (_stock > 0)
                {
                    Thread.Sleep(10);
                    _stock -= 1;
                    Console.WriteLine($"跨进程库存:{_stock}");
                }
            }
            finally
            {
                _mutex.ReleaseMutex();
            }
        }
    }

    public void Dispose() => _mutex?.Dispose();
}

// 适用场景:分布式系统或跨进程同步
// 优点:支持跨进程,系统级锁
// 缺点:性能较低,需要处理异常

2.4 流量警察:Semaphore

像十字路口的红绿灯,Semaphore可以控制同时访问资源的线程数量,特别适合资源池管理。

public class SemaphoreInventoryService
{
    private SemaphoreSlim _semaphore = new SemaphoreSlim(3, 3);
    private int _stock = 10;

    public async Task DeductStockAsync()
    {
        await _semaphore.WaitAsync();
        try
        {
            if (_stock > 0)
            {
                await Task.Delay(10);
                _stock -= 1;
                Console.WriteLine($"信号量库存:{_stock}");
            }
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

// 适用场景:限制并发访问数量
// 优点:支持异步,可控制并发量
// 缺点:不保证执行顺序

2.5 读写判官:ReaderWriterLockSlim

像图书馆的借阅系统,区分读者和写者,解决读写竞争问题。

public class CacheService
{
    private ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();
    private Dictionary<string, string> _cache = new Dictionary<string, string>();

    public string Get(string key)
    {
        _cacheLock.EnterReadLock();
        try
        {
            return _cache.TryGetValue(key, out var value) ? value : null;
        }
        finally
        {
            _cacheLock.ExitReadLock();
        }
    }

    public void Update(string key, string value)
    {
        _cacheLock.EnterWriteLock();
        try
        {
            _cache[key] = value;
        }
        finally
        {
            _cacheLock.ExitWriteLock();
        }
    }
}

// 适用场景:读多写少的场景
// 优点:提升读性能,减少等待
// 缺点:实现复杂度较高

3. 选择武器的决策树

根据实际场景选择同步机制时,可以参考以下决策流程:

  1. 需要跨进程同步吗?

    • 是 → 使用Mutex
    • 否 → 进入下一步
  2. 需要限制并发数量吗?

    • 是 → 使用Semaphore
    • 否 → 进入下一步
  3. 是读多写少的场景吗?

    • 是 → 使用ReaderWriterLockSlim
    • 否 → 进入下一步
  4. 需要超时控制吗?

    • 是 → 使用Monitor
    • 否 → 使用lock

4. 实战避坑指南

4.1 死锁预防三原则

  • 锁顺序规则:所有线程按相同顺序获取锁
  • 超时机制:使用TryEnter替代Enter
  • 资源分层:将大锁拆分为多个小锁

4.2 性能优化技巧

  • 锁粒度控制:库存服务中按商品ID分桶加锁
  • 无锁编程:使用Interlocked类实现原子操作
  • 异步同步:SemaphoreSlim的WaitAsync方法
// 分桶锁示例
public class BucketLockInventoryService
{
    private readonly object[] _lockBuckets;
    private int[] _stocks;

    public BucketLockInventoryService(int bucketSize = 10)
    {
        _lockBuckets = Enumerable.Range(0, bucketSize)
                               .Select(_ => new object()).ToArray();
        _stocks = new int[bucketSize];
        Array.Fill(_stocks, 10);
    }

    public void DeductStock(int productId)
    {
        var bucketIndex = productId % _lockBuckets.Length;
        lock (_lockBuckets[bucketIndex])
        {
            if (_stocks[bucketIndex] > 0)
            {
                _stocks[bucketIndex] -= 1;
            }
        }
    }
}

5. 关联技术:并发集合类

当同步机制显得笨重时,不妨考虑System.Collections.Concurrent命名空间下的线程安全集合:

public class ConcurrentCache
{
    private ConcurrentDictionary<string, string> _cache = new ConcurrentDictionary<string, string>();

    // 自动处理并发的添加或更新
    public void AddOrUpdate(string key, string value)
    {
        _cache.AddOrUpdate(key, value, (k, old) => value);
    }

    // 线程安全的读取操作
    public bool TryGetValue(string key, out string value)
    {
        return _cache.TryGetValue(key, out value);
    }
}

6. 总结:没有银弹,只有合适的工具

通过多个实际案例的分析,我们可以得出以下结论:

  1. 简单临界区优先选择lock
  2. 需要超时控制使用Monitor
  3. 跨进程场景选择Mutex
  4. 流量控制用Semaphore
  5. 读写分离场景用ReaderWriterLockSlim

最后记住:最好的同步就是不同步。在ASP.NET Core开发中,合理使用异步编程模型、采用无状态设计、利用消息队列解耦,往往能从架构层面减少对同步机制的依赖。但当必须面对共享资源时,选择合适的同步工具就像选择合适的手术刀——既要足够锋利,又要避免误伤自己。