我正在使用.NET Core 3.1。我想知道HttpContext.Session.SetString(...)
是否线程安全?例如,客户端同时向同一控制器操作(Test
)发出两个请求。控制器将字符串添加到会话(请参阅下面的示例)。会话结束时(例如,当我刷新页面时)中会有两个、一个或零个键吗?
public IActionResult Test()
{
HttpContext.Session.SetString(Guid.NewGuid().ToString(), "test");
return Ok();
}
我在使用DevExtreme FileUploader时将一些值保存到会话中。当我一次上传多个文件时,组件同时发出多个请求,最后,会话中通常会缺少一些键。我认为有一些竞争条件正在发生。
新增:客户端代码
我注意到只有当我使用会话密钥时才会丢失method: 'POST'
(只有 1 个密钥)。如果我使用method: 'GET'
,有 3 个键(正确)。
$(document).ready(function () {
var method = 'GET'; // works (3 keys)
//var method = 'POST'; // doesn't work (1 key)
$('#fire').on('click', function () {
$.when(
$.ajax({
url: 'Session/Test',
method: method,
success: function(){
console.log('response', 1);
}
}),
$.ajax({
url: 'Session/Test',
method: method,
success: function(){
console.log('response', 2);
}
}),
$.ajax({
url: 'Session/Test',
method: method,
success: function(){
console.log('response', 3);
}
})
).then(function () {
alert('Done');
});
});
});
假设您使用 ASP.Net Core提供的默认会话实现。
在HttpContext.Session
方面:
HttpContext.Session
返回DistributedSession
的实例,该实例在内部使用Dictionary<TKey, TValaue>
。字典不是线程安全的,因此如果您从多个线程访问Session
(例如Task.Run
),可能会导致意外结果。
就不同请求的会话而言:
在 ASP.Net 核心中,Session
来自ISessionStore
,它具有短暂的寿命。这意味着,Session
对象不被请求共享。因此,如果您有并发请求,则每个请求都有自己的Session
对象。
在竞争条件方面:
会话的默认实现从 Cookie 读取/写入会话状态.AspNetCore.Session
。这可能会导致争用条件。
由于Session
是按客户端进行的,因此当您有来自同一客户端的并发请求触及相同 cookie/会话状态中的相同位和片段时,您可能会遇到争用条件。但是,争用条件不是因为服务器端的Session
。它实际上是由客户端的 cookie 管理引起的。
会话状态为非锁定。如果两个请求同时尝试修改会话的内容,则最后一个请求将覆盖第一个请求。
请考虑以下示例:
假设您有一个控制器操作,该操作使用提供的value
设置Session
,另一个控制器操作从Session
中检索值并将其返回到正文中:
[HttpGet]
[Route("create")]
public IActionResult CreateSession([FromQuery]string value)
{
HttpContext.Session.SetString("key", value);
return Ok();
}
[HttpGet]
[Route("get")]
public IActionResult ReturnSession([FromQuery] string expected)
{
var actual = HttpContext.Session.GetString("key");
return Ok(new { actual, expected });
}
如果使用HttpClient
测试这些操作:
async Task TestSession(HttpClient client, string str)
{
await client.GetAsync($"https://localhost:5001/session/create?value={str}");
var r = await client.GetAsync($"https://localhost:5001/session/get?expected={str}");
var session = await r.Content.ReadAsStringAsync();
Console.WriteLine(session);
}
using (var client = new HttpClient())
{
await TestSession(client, "abc");
}
输出应如下所示:
{"actual":"abc","expected":"abc"}
当您有来自同一客户端的并发请求时,将引发问题:
using (var client = new HttpClient())
{
var tasks = new List<Task>();
for (var i = 0; i < 10; i++)
{
var str = i.ToString();
tasks.Add(Task.Run(() => TestSession(client, str)));
}
await Task.WhenAll(tasks);
}
输出如下所示:
{"actual":"2","expected":"1"}
{"actual":"3","expected":"6"}
{"actual":"4","expected":"5"}
{"actual":"4","expected":"3"}
{"actual":"4","expected":"2"}
{"actual":"8","expected":"4"}
{"actual":"7","expected":"8"}
{"actual":"7","expected":"7"}
{"actual":"9","expected":"0"}
{"actual":"9","expected":"9"}
在上述情况下,请求 3在请求6的创建和获取之间更改了会话状态,这意味着请求 6可能无法正确查看其会话状态。
若要避免此问题,可以对每个批次使用不同的HttpClient
。
不,dotnet 集合不是线程安全的,会话也不是。 如果要在多个线程上共享同一会话,则应将其视为只读