.NET Core 获取 HttpContext.Current 以及 AsyncLocal 与 ThreadLocal


在 DotNetCore 当中不再像 MVC5 那样可以通过 HttpContext.Current 来获取到当前请求的上下文。

不过微软提供了一个 IHttpContextAccessor 来让我们访问当前请求的 Http 上下文,其定义
如下:

namespace Microsoft.AspNetCore.Http
{
	public interface IHttpContextAccessor
	{
	    HttpContext HttpContext { get; set; }
	}
}

需要使用的话需要将其添加到 Ioc 容器当中,在 Startup 类的 ConfigureService 我们可以将其默认实现注册到 Ioc 之中。

public void ConfigureService(IServiceCollection services)
{
    services.TryAddSingleton();
}

那么我们可以来看看 HttpContextAccessor 的具体实现:

using System.Threading;

namespace Microsoft.AspNetCore.Http
{
    public class HttpContextAccessor : IHttpContextAccessor
    {
        private static AsyncLocal _httpContextCurrent = new AsyncLocal();

        public HttpContext HttpContext
        {
            get
            {
                return _httpContextCurrent.Value;
            }
            set
            {
                _httpContextCurrent.Value = value;
            }
        }
    }
}

在其内部主要是用了一个 AsyncLocal 来保存一个 HttpContext 实例,那么 Accessor 是什么时候被赋值的呢?答案就是在每次 HTTP 请求的时候会将其赋值。

AsyncLocal 是什么东西?
AsyncLocal 是在 .Net 4.6 之后推出的一个对象,该对象接受一个泛型参数,其主要作用是保存异步等待上下文中共享某个变量的值。
而异步方法是基于 Task 的自动线程调度,在异步上下文切换的时候可能导致数据丢失。例如在 await 调用之前对某个变量进行了赋值,而这个变量是多个线程间共享的,当 await 调用返回之前的调用点的时候,可能调用点之后的代码还处在之前的线程上,也有可能被调度到其他线程上。

举个例子:

static async Task TestMethod()
{
    Console.WriteLine($"当前线程ID{Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(100);
    Console.WriteLine($"当前线程ID{Thread.CurrentThread.ManagedThreadId}");
}

在 await 等待任务执行完成之后,后面的代码输出的 ID 与调用之前的 ID 不一样,说明发生了线程切换:

static void Main(string[] args)
{
    Action @delegate = async () => await TestMethod();

    @delegate();
    Console.ReadKey();
}

Img1
从代码上看他们似乎在同一个线程,但是在执行的时候就已经发生了线程切换的操作了。
而我们在这里如果使用一个 ThreadLocal变量来存储的话,会发生什么事情呢?

static ThreadLocal _threadLocal = new ThreadLocal();
static AsyncLocal _asyncLocal = new AsyncLocal();

static void Main(string[] args)
{
    Action @delegate = async () => await TestMethod();

    @delegate();
    Console.ReadKey();
}

static async Task TestMethod()
{
    _threadLocal.Value = 1000;
    _asyncLocal.Value = 2000;
    Console.WriteLine($"当前线程ID{Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"{nameof(_threadLocal)},值:{_threadLocal.Value}");
    Console.WriteLine($"{nameof(_asyncLocal)},值:{_asyncLocal.Value}");
    await Task.Delay(100);
    Console.WriteLine($"当前线程ID{Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"{nameof(_threadLocal)},值:{_threadLocal.Value}");
    Console.WriteLine($"{nameof(_asyncLocal)},值:{_asyncLocal.Value}");
}

Img
SO,在这里解释一下,ThreadLocal 是用于为不同的线程保存不同的变量值的,即同一个变量在不同线程当中存储的值可以不一样。在这里使用是为了保证在 TestMethod 方法中变量的唯一性,这个在同步方法用是没问题的,但这里使用了 await 关键字导致等待异步调用结束后代码已经被调度到其他的线程了,所以这里没用。而 AsyncLocal 正是为了这种情况而准备的。