【问题标题】:StringBuilder with Caching, ThreadStatic带缓存、ThreadStatic 的 StringBuilder
【发布时间】:2017-08-30 20:11:01
【问题描述】:

我从Writing Large, Responsive .NET Framework Apps 发现了下面的代码。

下面的代码使用StringBuilder 创建了一个类似SomeType<T1, T2, T3> 的字符串,并演示了缓存StringBuilder 以提高性能。

 public void Test3()
        {
            Console.WriteLine(GenerateFullTypeName("SomeType", 3));
        }

        // Constructs a name like "SomeType<T1, T2, T3>"  
        public string GenerateFullTypeName(string name, int arity)
        {
            //StringBuilder sb = new StringBuilder();
            StringBuilder sb = AcquireBuilder();

            sb.Append(name);
            if (arity != 0)
            {
                sb.Append("<");
                for (int i = 1; i < arity; i++)
                {
                    sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
                }
                sb.Append("T"); sb.Append(arity.ToString()); sb.Append(">");
            }

            //return sb.ToString();
            /* Use sb as before */
            return GetStringAndReleaseBuilder(sb);
        }
        [ThreadStatic]
        private static StringBuilder cachedStringBuilder;

        private static StringBuilder AcquireBuilder()
        {
            StringBuilder result = cachedStringBuilder;
            if (result == null)
            {
                return new StringBuilder();
            }
            result.Clear();
            cachedStringBuilder = null;
            return result;
        }

        private static string GetStringAndReleaseBuilder(StringBuilder sb)
        {
            string result = sb.ToString();
            cachedStringBuilder = sb;
            return result;
        }

但是,下面的两个修改方法在缓存StringBuilder方面更好是正确的吗?只有 AcquireBuilder 需要知道如何缓存它。

 private static StringBuilder AcquireBuilder()
        {
            StringBuilder result = cachedStringBuilder;
            if (result == null)
            {
                //unlike the method above, assign it to the cache
                cachedStringBuilder = result = new StringBuilder();
                return result;
            }
            result.Clear();
            //no need to null it
           // cachedStringBuilder = null;
            return result;
        }

        private static string GetStringAndReleaseBuilder(StringBuilder sb)
        {
            string result = sb.ToString();
             //other method does not to assign it again.
            //cachedStringBuilder = sb;
            return result;
        }

另外一个问题是原来的方法不是线程安全的,为什么demo中使用了ThreadStatic?

【问题讨论】:

  • 这是AcquireBuilder 的更好实现:ObjectPool&lt;StringBuilder&gt;.Get。这是 ASP.NET 本身使用的;我不知道为什么作者觉得有必要想出一些原创的东西。
  • 这已经是built into the framework。看起来很相似。请确保您需要它,请记住,没有过期策略的缓存是内存泄漏。

标签: c#


【解决方案1】:

焦点在创建新 StringBuilder 实例的那一行。 该代码导致分配 sb.ToString() 和 internal StringBuilder 实现中的分配,但您不能 如果您想要字符串结果,请控制这些分配。

根据例子,他们忽略了自己的话。最好只缓存它并重用它(使用前清理它)。除了那些需要的,没有分配:

    public static string GenerateFullTypeName(string name, int arity)
    {
        //StringBuilder sb = new StringBuilder();
        StringBuilder sb = cached.Value;
        sb.Clear();

        sb.Append(name);
        if (arity != 0)
        {
            sb.Append("<");
            for (int i = 1; i < arity; i++)
            {
                sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
            }
            sb.Append("T"); sb.Append(arity.ToString()); sb.Append(">");
        }

        //return sb.ToString();
        /* Use sb as before */
        return sb.ToString();
    }

    [ThreadStatic]
    private static Lazy<StringBuilder> cached = new Lazy<StringBuilder>(()=> new StringBuilder());

另外,我认为这是 GC 如何损害您的应用程序性能的非常糟糕的例子。临时和短字符串基本不会进入第二代,很快就会被处理掉。更好的是像 WCF 传输流的缓冲区,在池中返回这个缓冲区,或者 Task 一般如何工作(同样的想法)并分配它们的肠子,但不是 StringBuilder,duh。

【讨论】:

  • 同意,使用 Lazy 比我的版本好。但是我不明白为什么AcquireBuilderGetStringAndReleaseBuilder的原始方法与StringBuilder相关,据说是VS的阅读示例。
  • 不好的例子。就如此容易。没有人是完美的。
  • 如果需要多线程构建字符串,可以使用ThreadLocal,获取构建字符串后清除。我在生产代码中使用了它的全局版本,在这些地方一遍又一遍地分配 StringBuilders 会影响性能。这确实有助于降低适用场景中的 GC 压力。
【解决方案2】:

这里是“原始方法不是线程安全的”的答案

基本上,作者所做的是 - 用ThreadStaticAttribute 标记属性,这使其成为线程安全的,因为值仅适用于该线程。不同的线程将有不同的价值/参考。而且这个“缓存”只会在线程本身的生命周期内存在。即使方法本身不是线程安全的,它访问的值也是。

现在,我不认为,这通常是一个很好的例子,因为这有什么意义?您保留了一个无论如何都要清除的 sting builder 实例。

如果您对每个线程的静态值特别感兴趣,ThreadStaticAttribute 是不错的选择。如果您对线程安全的静态方法更感兴趣,请查看lock

private static MyClass _myClass;
private static object _lock = new object();

public static MyClass GetMyClass()
{
    if (_myClass == null)
    {
        lock(_lock)
        {
            if (_myClass == null)
            {
                _myClass = new MyClass();
            }
        }

    }
    return _myClass;
}

【讨论】:

  • 我知道这一点。谢谢。但我不明白为什么 AcquireBuilder 和 GetStringAndReleaseBuilder 的原始方法与 StringBuilder 相关,据说是 VS 的读取示例。
  • @Pingpong 这只是个坏例子。这在现实世界中毫无意义。您不会在不断清理它的同时创建和保留类似StringBuilder 的东西。一个很好的例子是保持某种执行上下文,这对于每个线程都是不同的。在线程执行开始时,您设置此上下文并沿途使用该静态属性。
  • 你的想法是对的,但在某些平台上它不是线程安全的。见this thread。请注意,Lazy 实现了同样的目的。
【解决方案3】:

这个例子只展示了主要思想,并没有深入。让我们用新方法扩展我们的类来添加命名空间。

public string GenerateFullTypeName(string name, int arity, string @namespace)
{
    StringBuilder sb = AcquireBuilder();
    sb.Append(this.GenerateNamespace(@namespace));
    sb.Append(this.GenerateFullTypeName(name, arity));
    return GetStringAndReleaseBuilder(sb);
}

public string GenerateNamespace(string @namespace)
{
    StringBuilder sb = AcquireBuilder();

    sb.Append(@namespace);
    sb.Append(".");

    return GetStringAndReleaseBuilder(sb);
}

并测试它Console.WriteLine(test.GenerateFullTypeName("SomeType", 3, "SomeNamespace")); 原始代码按预期工作(输出 字符串是SomeNamespace.SomeType&lt;T1, T2, T3&gt;),但是如果我们应用您的“优化”会发生什么?输出字符串将是错误的 (SomeType&lt;T1, T2, T3&gt;SomeType&lt;T1, T2, T3&gt;),因为对于此类中的所有方法,即使该实例仍在使用中,我们也只会使用 StringBuilder 的一个(兑现)实例。这就是为什么实例只有在使用后才存储在字段中,如果再次使用则从字段中删除。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-12
    • 1970-01-01
    • 1970-01-01
    • 2010-09-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多