C# MVC并发调用POST方法时如何实现同步临界区?
.NET MVC并发请求下临界区实现问题解答
问题背景
我用C# .NET开发MVC项目时遇到了并发问题:控制器的PostDog POST接口被客户端每秒多次调用,因为MVC会并行处理请求,导致AddDog方法里的临界区代码并发执行,出现数据丢失。
原控制器代码:
[HttpPost] public IActionResult PostDog(){ AddDog(TempData, new Dog()); return Json("success"); }
AddDog临界区代码:
public bool AddDog(ITempDataDictionary tempData, Dog dog){ /* 临界区 - 并发执行会丢失数据 */ List<Dog> dogs = (List<Dog>)tempData["myDogList"]; dogs.Add(dog); tempData["myDogList"] = dogs; /* 临界区结束 */ return true; }
我尝试用lock解决,但发现TempData根本没更新,怀疑和TempData的传递方式有关,尝试的代码如下:
private static readonly object TempDataLock = new(); public static bool AddDog(ITempDataDictionary tempData, Dog dog){ lock(TempDataLock ) { List<Dog> dogs = (List<Dog>)tempData["myDogList"]; dogs.Add(dog); tempData["myDogList"] = dogs; } return true; }
问题根源
你用lock没用的核心原因是用错了存储载体:
TempData是请求级别的独立对象,每个HTTP请求都会创建自己的TempData实例,存储的是当前请求的临时数据(设计用于重定向这类请求间的数据传递),根本不是跨请求共享的集合。- 你加的静态锁虽然能锁住代码执行,但每个请求操作的都是自己
TempData里的myDogList副本,完全不是同一个集合,自然锁了也白锁,更不可能实现跨请求的数据同步。
正确解决方案
方案1:全局共享线程安全集合(适合所有请求共享数据场景)
如果你的需求是让所有客户端的请求都往同一个Dog列表里加数据,直接用.NET自带的线程安全集合(比如ConcurrentBag<T>),这类集合的操作本身就是线程安全的,无需额外加锁:
// 全局静态线程安全集合,所有请求共享 private static readonly ConcurrentBag<Dog> _globalDogList = new ConcurrentBag<Dog>(); [HttpPost] public IActionResult PostDog(){ AddDog(new Dog()); return Json("success"); } public bool AddDog(Dog dog){ // ConcurrentBag.Add本身线程安全,直接调用即可 _globalDogList.Add(dog); return true; }
如果需要有序的列表(ConcurrentBag是无序的),可以用静态List<T>配合lock:
private static readonly List<Dog> _globalDogList = new List<Dog>(); // 静态锁对象,确保所有请求都用同一把锁 private static readonly object _listLock = new object(); [HttpPost] public IActionResult PostDog(){ AddDog(new Dog()); return Json("success"); } public bool AddDog(Dog dog){ lock(_listLock){ _globalDogList.Add(dog); } return true; }
方案2:Session存储(适合单个用户跨请求共享数据场景)
如果你的需求是每个用户的请求只往自己的Dog列表里加数据,那应该用Session(每个用户有独立的Session),同时处理Session的并发访问:
[HttpPost] public async Task<IActionResult> PostDog(){ await AddDogAsync(new Dog()); return Json("success"); } public async Task<bool> AddDogAsync(Dog dog){ // 先加载Session中的数据 await HttpContext.Session.LoadAsync(); // 锁定当前用户的Session,防止同一用户的并发请求冲突 lock(HttpContext.Session){ // 从Session中获取Dog列表,不存在则新建 var dogs = HttpContext.Session.Get<List<Dog>>("myDogList") ?? new List<Dog>(); dogs.Add(dog); // 将更新后的列表存回Session HttpContext.Session.Set("myDogList", dogs); } // 保存Session变更 await HttpContext.Session.CommitAsync(); return true; }
注意:需要先配置Session的序列化方式,比如在Program.cs中添加:
builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); }); // 配置Session的序列化(以System.Text.Json为例) builder.Services.AddControllersWithViews() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // 注册Session序列化扩展方法 public static class SessionExtensions { public static void Set<T>(this ISession session, string key, T value) { session.SetString(key, JsonSerializer.Serialize(value)); } public static T? Get<T>(this ISession session, string key) { var value = session.GetString(key); return value == null ? default : JsonSerializer.Deserialize<T>(value); } }
关键总结
- 永远不要用
TempData做跨请求的共享存储,它的设计目标是请求间临时传递数据,不是共享集合。 - 全局共享数据优先用线程安全集合,或者静态集合+全局锁。
- 单个用户的跨请求数据用
Session,并注意处理Session的并发锁定。
内容的提问来源于stack exchange,提问作者Uuuuuumm
相关产品推荐
相关产品推荐

