关于.net:如何在-C-中使用-ValueTask

51次阅读

共计 2781 个字符,预计需要花费 7 分钟才能阅读完成。

异步编程 置信大家曾经应用很多年了,尤其在 C# 5.0 中引入的 await,async 关键词让代码编写的更加简略粗犷,你能够利用 异步编程 来进步应用程序的 响应速度 吞吐率

异步办法中举荐的做法是返回 Task,如果异步办法中须要返回一些数据的话,能够将 Task 改成 Task<T>,还有一种状况是你的异步办法什么都不须要返回,那就改成 void 就能够了。

在 C# 7.0 之前,异步办法的返回值有以下三种。

  • Task
  • Task<T>
  • void

在 C# 7.0 之后,除了下面三种还能够返回 ValueTaskValueTask<T>,这些类都是在 System.Threading.Tasks.Extensions 命名空间下,这篇文章我筹备和大家一起探讨下 ValueTask。

为什么要应用 ValueTask

Task 可用来示意操作的状态,什么意思呢?比如说:这个操作是否实现?是否被勾销 等等,同时 异步办法 中能够返回 Task 或者 ValueTask,这里有个潜在的问题不晓得大家是否留神到,因为 Task 是一个援用类型,如下代码:


    public class Task : IAsyncResult, IDisposable
    { }

这就意味着每次调用异步办法都会在 托管堆 上生成一个 Task 实例,如果霎时调用 1w 次,那么托管堆上也霎时存在 1w 个 Task 实例,那这有什么问题呢?问题在于有些场景下,你的异步办法是能够间接返回数据的,或者说能够齐全同步的,比方:你的异步办法仅仅是从缓存中取数据,这时候无谓的给 GC 减少回收压力就不那么柔美了。


        static Task<int> GetCustomerCountAsyc(string key)
        {return Task.FromResult<int>(NativeCache[key]);
        }

这时候就须要用 ValueTask 了,它提供了如下两点益处:

  • ValueTask 是值类型,所以防止了在 托管堆 上的内存调配,便领有了更高的性能。
  • ValueTask 在实现上更加便捷 而且 灵活性更强。

总的来说: 如果你的异步办法能够间接获取后果,那么倡议将 Task<T> 改成 ValueTask<T>,从而防止不必要的性能开销,TaskValueTask 都是示意 可期待 的操作,这里要留神的是,千万不要阻塞 ValueTask,毕竟它是值类型,如果真要这么做的话,调用 ValueTask.AsTask 办法将 ValueTask 转成 Task,而后在这个援用的 Task 上进行阻塞,还有一点要留神的是,ValueTask 只能被 await 一次,如果要破除这个限度的话,还是调用 AsTask 办法 将 ValueTask 转成 Task 即可。

ValueTask 的例子

假如你有一个异步办法,须要返回 Task,你能够利用 Task.FromResult 去生成一个 Task 对象,如下代码所示:


public Task<int> GetCustomerIdAsync()
{return Task.FromResult(1);
}

下面的代码不会在 IL 层面生成残缺的 状态机代码,而仅仅是在 托管堆 中生成一个 Task 对象,要防止这种无谓的 Task 调配,能够用 ValueTask 撸掉它,如下代码所示:


public ValueTask<int> GetCustomerIdAsync()
{return new ValueTask(1);
}

接下来定义一个 IRepository 接口,新增一个返回值为 ValueTask<int> 的同步办法,如下代码所示:


    public interface IRepository<T>
    {ValueTask<T> GetData();
    }

从 IRepository 接口上派生一个 Repository 类,如下代码所示:


    public class Repository<T> : IRepository<T>
    {public ValueTask<T> GetData()
        {var value = default(T);
            return new ValueTask<T>(value);
        }
    }

最初在 Main 中来调用 GetData 办法。


        static void Main(string[] args)
        {IRepository<int> repository = new Repository<int>();
            var result = repository.GetData();
            if(result.IsCompleted)
                 Console.WriteLine("Operation complete...");
            else
                Console.WriteLine("Operation incomplete...");
            Console.ReadKey();}

当初在 IRepository 接口中增加一个异步办法 GetDataAsync,批改后的代码如下:


    public interface IRepository<T>
    {ValueTask<T> GetData();
        ValueTask<T> GetDataAsync();}
    
    public class Repository<T> : IRepository<T>
    {public ValueTask<T> GetData()
        {var value = default(T);
            return new ValueTask<T>(value);
        }
        public async ValueTask<T> GetDataAsync()
        {var value = default(T);
            await Task.Delay(100);
            return value;
        }
    }

什么时候应该应用 ValueTask

只管 ValueTask 提供了很多益处,但用 ValueTask 替换掉 Task 也必须再三衡量,因为 ValueTask 是一个蕴含两个字段的值类型,而 Task 仅仅是一个字段的援用类型,这就意味着从办法返回 ValueTask 会有两个字段的开销,同时在 await 场景下生成的 异步状态机 须要更大的空间来存储 两个字段的 ValueTask 类型。

再扩大的话,如果异步办法的调用者应用 Task.WhenAll 或者 Task.WhenAny 时,而这个异步办法返回值是 ValueTask 的话开销通常会更大,为什么这么说呢?因为你必须要将 ValueTask 转成 Task 能力在 WhenXXX 中应用,这个过程中就造成了托管堆的内存调配,如果想优化的话,能够在第一次应用 Task 的时候将这个实例缓存起来,供后续再复用。

最初总结一些 教训法令 吧。

  • 如果你的异步办法不是可立刻实现的,请用 Task。
  • 如果你的异步办法是可立刻实现的,比方纯内存操作,读缓存,请用 ValueTask。

不管怎样,在应用 ValueTask 之前肯定要做好必要的性能剖析,给本人短缺的理由应用 ValueTask。

译文链接:https://www.infoworld.com/art…

更多高质量干货:参见我的 GitHub: csharptranslate

正文完
 0