【问题标题】:Instance null when using ThreadStatic attribute for static variable对静态变量使用 ThreadStatic 属性时实例为空
【发布时间】:2017-05-25 11:30:32
【问题描述】:

我有使用System.Threading.Task 调用服务函数的操作方法。

服务函数有一个静态全局变量,我设置了属性ThreadStatic 以使我的函数线程安全。

我的问题是,有时当我运行我的操作方法时,共享变量_sharedList 在我的HotelService 中访问它时会引发空引用异常。

这是一个复制问题的示例实现:

调用 HotelService.TestMultiThread 的控制器操作方法

public ActionResult MultiThread()
{
  HotelService svc = new HotelService();
  var resp = new List<TestPnrHeaderResponse>();

  var tasks = Enumerable.Range(0, 5).Select(i => Task.Run(() => svc.TestMultiThread(i)));

  var results = await Task.WhenAll(tasks);

  return View(resp);
}

HotelService 类

_sharedList 在_sharedList.listInt.AddRange(GetIntList()); 行中为 Null

public class HotelService
{
    [ThreadStatic]
    private static TestPnrHeaderResponse _sharedList;

    private void LoadCache()
    {
        _sharedList = new TestPnrHeaderResponse();
        _sharedList.PnrLegs = new List<PnrLegVM>();
        _sharedList.listInt = new List<int>();
        Task.Factory.StartNew(() =>
        {
            _sharedList.listInt.AddRange(GetIntList());
        });
    }

    private IEnumerable<int> GetIntList()
    {
        return Enumerable.Range(0, 5);
    }
    public TestPnrHeaderResponse TestMultiThread(int count)
    {
        LoadCache();

        if (count % 2 == 0)
        {
            _sharedList.PnrLegs.Add(new PnrLegVM
            {
                ApplicationType = count.ToString(),
                PKCity = count,
                PKNationality = 1,
                PKPnrHeader = 1,
                PKPnrLeg = 1
            });
        }
        else
        {
            _sharedList.PnrLegs.Add(new PnrLegVM
            {
                ApplicationType = count.ToString(),
                PKCity = count,
                PKNationality = 99,
                PKPnrHeader = 99,
                PKPnrLeg = 99
            });

        }
        return _sharedList;
    }
}

共享变量类

public class TestPnrHeaderResponse
{
   public List<PnrLegVM> PnrLegs { get; set; }
   public List<int> listInt { get; set; }
}
public class PnrLegVM
{
   public int PKPnrLeg{get;set;}

   public int PKPnrHeader{get;set;}      
   public string ApplicationType{get;set;}
   public int PKNationality {get;set;}
}

请帮助寻找解决方案。此外,是否有更好的方法使函数线程安全,因为它在实际实现中使用了许多共享变量。

【问题讨论】:

  • 任务不是线程。它们是在可重用线程池线程上运行的作业。这意味着将任何内容存储在 ThreadStatic 存储中几乎可以保证您会丢失它。无法保证您的任何任务都会重用同一个线程
  • 您的代码也过于复杂。没有理由使用冷任务或将它们放在数组中。它们不是线程。如果您想在后台执行 5 次调用,只需使用 5 次 Task.Run 调用并使用 await Task.WhenAll(...); 等待结果。
  • @PanagiotisKanavos 我知道任务不是线程。正如我所问的那样,我正在尝试为ThreadStatic 找到更好的解决方案。
  • 然而,你是这样对待它们的,通过创建冷任务,将它们存储在数组中,调用.Start,使用ThreadStatic,当你不知道你的线程是没有意义的正在运行。你为什么想要ThreadStatic 呢?你想做什么?如果这是您的意图,它不会使列表成为线程安全的。
  • 如果您尝试延迟初始化列表,请使用Lazy&lt;T&gt;。要生成 5 个正在运行的任务,请使用 var tasks=Enumerable.Range(0,5).Select(int i=&gt;Task.Run(()=&gt;svc.TestMultiThread(i));var results=await Task.WhenAll(tasks);。或者您可以使用 PLINQ - 只需在查询中使用 AsParallel()

标签: c# asp.net-mvc multithreading thread-safety


【解决方案1】:

_sharedList 在 _sharedList.listInt.AddRange(GetIntList()); 行中为 Null;

令人惊讶的是,它并不总是 null 在这一点上。您正在一个全新的线程中执行该代码,您 not 调用了LoadCache(),因此尚未初始化该字段。如果你不小心得到了一个非null 值,那只是因为你在一个线程池线程中被执行,对于count 参数的一些其他 值,LoadCache()方法被调用。当然,在这种情况下,虽然值不是null,但它不是您认为的列表。

我不清楚为什么要将这项工作委托给另一个线程,尤其是当您试图将 _sharedList 变量绑定到单个线程时。我也不清楚为什么要在没有任何同步的情况下修改列表,即使您显然希望该对象是在两个不同的非同步线程中使用的对象。

但是暂时忽略这些问题,您可以通过捕获局部变量中的值并在匿名方法中使用该值来解决null 问题:

private void LoadCache()
{
    _sharedList = new TestPnrHeaderResponse();
    _sharedList.PnrLegs = new List<PnrLegVM>();
    _sharedList.listInt = new List<int>();

    List<int> listInt = _sharedList.listInt;

    Task.Factory.StartNew(() =>
    {
        listInt.AddRange(GetIntList());
    });
}

本地 listInt 变量将获取对您的 _sharedList 对象引用的列表的引用,然后在匿名方法中捕获该变量,因此可以确保它是您真正想要使用的列表任务。

在您发布的代码中,TestMultiThread() 方法实际上并未使用_sharedList.listInt,因此理论上不存在冲突。但是您的问题中没有足够的上下文来了解该字段是否在其他时间在其他地方使用。坦率地说,甚至不清楚为什么 _sharedList 首先是 static 字段。您发布的所有代码都可以通过将变量设为局部变量来编写,仅使用显示的方法。

但是,如果您确实需要代码按照您显示的那样工作,上面将解决您所询问的 null 值。

【讨论】:

  • 感谢您的澄清。在我的实际实现中,_sharedList 是一个全局私有 非静态 变量,在我的 HotelService 函数的许多地方都使用过。我想让我的 HotelService 函数在从另一个服务调用时使其成为线程安全的。所以,我找到了这个方法来使用 ThreadStatic 并尝试了一下。
  • _sharedList.listInt 这里 listInt 在我的实际代码中是一个非常复杂的对象,它使用Tasks 使用 EF 从数据库中获取数据。像国家列表,城市列表,机场列表作为类属性。为了模仿我使用的代码listInt。它在我的代码中HotelService 的许多地方使用。基本上,我的服务是搜索酒店服务,它在搜索开始时使用LoadCache() 方法设置的初始数据。
  • @User3250:抱歉,我不知道如何处理您 cmets 中的额外信息。事实上,[ThreadStatic] 就是这样:针对特定线程。您发布的代码有意执行一个线程所依赖的代码,在另一个线程中,该字段的值保证是不同的(因为您使用了[ThreadStatic])。您发布的代码代表您实际在做什么,在这种情况下,我的回答可以解决您的问题,或者不是,在这种情况下,您需要更改代码示例才能做到这一点。
  • 是的,您的回答确实解决了我的问题。您对ConcurrentDictionary 或其他并发方法用法有什么好的阅读吗?
  • @User3250:并发和线程安全是一个非常广泛的话题。恐怕我不知道有任何单一资源可以做到这一点。我会说,[ThreadStatic] 不是灵丹妙药,它有自己的陷阱。虽然有许多安全并发策略,恕我直言,最好的两个是不可变数据结构(在您的情况下可能不可能),以及将数据结构专用于特定线程(在您的情况下可能,但不需要使用[ThreadStatic])。换句话说,要么让数据不改变,要么让数据改变,只有一个线程关心
【解决方案2】:

根据上面的建议和here,我已经解决了使用ConcurrentDictionary&lt;TKey, Lazy&lt;TValue&gt;&gt;的以下实现:

控制器

public ActionResult MultiThread()
{
 HotelService svc = new HotelService();
 var resp = new List<TestPnrHeaderResponse>();
 var tasks = Enumerable.Range(0, 5).Select(i => Task.Run(() => svc.TestMultiThread(i)));
 var results = await Task.WhenAll(tasks);
 return View(resp);
}

酒店服务等级

public class HotelService
{
    private ConcurrentDictionary<int, Lazy<TestPnrHeaderResponse>> _sharedList
        = new ConcurrentDictionary<int, Lazy<TestPnrHeaderResponse>>();

    private TestPnrHeaderResponse LoadGetCache(int count)
    {
        var resp = new Lazy<TestPnrHeaderResponse>();
        resp.Value.PnrLegs = new List<PnrLegVM>();
        resp.Value.listInt = new List<int>();
        List<int> listInt = resp.Value.listInt;
        Task.Factory.StartNew(() =>
        {
            listInt.AddRange(GetIntList());
        });

        return resp.Value;
    }

    private IEnumerable<int> GetIntList()
    {
        return Enumerable.Range(0, 5);
    }
    public TestPnrHeaderResponse TestMultiThread(int count)
    {
        var resp = new TestPnrHeaderResponse();
        resp = _sharedList.GetOrAddLazy(count,(k)=> LoadGetCache(k));

        if (resp == null)
        {
            LoadGetCache(count);
        }
        if (resp != null)
        {
            if (count % 2 == 0)
            {
                resp.PnrLegs.Add(new PnrLegVM
                {
                    ApplicationType = count.ToString(),
                    PKCity = count,
                    PKNationality = 1,
                    PKPnrHeader = 1,
                    PKPnrLeg = 1
                });
                resp.StatusCode = System.Net.HttpStatusCode.Accepted;
            }
            else
            {
                resp.PnrLegs.Add(new PnrLegVM
                {
                    ApplicationType = count.ToString(),
                    PKCity = count,
                    PKNationality = 99,
                    PKPnrHeader = 99,
                    PKPnrLeg = 99
                });
                resp.StatusCode = System.Net.HttpStatusCode.Redirect;
            }
        }

        return resp;
    }
}

来自答案here的扩展方法

public static V GetOrAddLazy<T, V>(this System.Collections.Concurrent.ConcurrentDictionary<T, Lazy<V>> dictionary, T key, Func<T, V> valueFactory)
{
      var lazy = dictionary.GetOrAdd(key, new Lazy<V>(() => valueFactory(key), LazyThreadSafetyMode.ExecutionAndPublication));
      return lazy.Value;
}

希望它可以帮助面临同样问题的人。谢谢。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-28
    • 2015-08-05
    • 1970-01-01
    相关资源
    最近更新 更多