框架自动生成的代码:

[HttpGet]
public async Task<ActionResult<IEnumerable<DocItem>>> GetDocItem()
{
 return await _context.DocItem.ToListAsync();
}

数据库中 4000 条数据,执行这个 api 卡顿 2 分钟都没有反应。
不要后台分页的建议,因为需求是想一次性把这 4000 条都返回给前台。

数据库表单条记录很大吗?
你的 2 分钟是哪里统计的,可以计算一下服务端的执行耗时

这个慢肯定和框架无关,这么点代码不太会有什么问题。要说的话也就是你其它的配置可能会有问题。但是我觉得可能最大的还是你的数据库慢。

先把性能探查器打开做个 profile ?

可以看下是不是数据传输慢

不要用数据库,返回 4000 条随机数据看看是不是慢。 如果不慢, 找数据部门查问题吧, 不是开发的问题。

显然不是框架问题

可数据库打查询日志, 看下查询耗费的事件, 这个不可能是框架的问题!

这无论如何也没法把锅甩 .Net 吧,4000 条两分钟唉,可能数据库哪里有问题,而且也不一定是数据库方面的问题,你查查你的运行环境和系统状态吧

用 profiler 跟踪一下,很久之前碰到过类似的问题, 用 profiler 跟踪发现是没有写入日志文件的权限

。。。这锅肯定不是从框架性能排查起的。。。

我觉得这锅不应该甩到框架上面去。先查 1 条数据库试试。

看你用的是 ef ,要不你打印一下 sql 看看

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory( );
}

单表记录不大,十来个字段,每最长的字段里就一句 100 个字不到的文本,我说的 2 分钟,是我用 swagger 调试时候的粗略估计,实际上每次不止 2 分钟。

没说是甩锅框架的啊,我就是发出来请教大家的,这也是微软成熟框架,我知道它 4000 条就卡成这样,所以才有疑惑。

应该不是权限问题,因为我加了 where 条件,设置成 100 条以内的返回的话,快的很。

是用的 ef ,但是这是我第一次用 net core 框架,不怎么熟悉,请问你贴的这个是在 startup.cs 文件里面配置的吗?

测试性能就不要用 DEBUG 模式,切到 Release ,另外 EF 框架是需要预热一下,你可以试试第二次、第三次应该就快了

试试看不要用 ToListAsync(),而是 ToList()之后再返回结果。印象里看到过一些 SO 上关于 ToListAsync 在 EF 莫名其妙慢的问题。

ToList 的时候调用下 AsNoTracking 试试

即使第一次这点数据也不会用你这么多时间的. 看你这几行代码,如果只读,你可以使用 AsNoTracking,剩下的自己看官方文档吧, docs.microsoft.com/zh-cn/ef/ef6/fundamentals/performance/perf-whitepaper?redirectedfrom=MSDN

有没有可能是你的哪个 async 操作 block 了?
c#一个 gc 语言 async 做的难用程度都快赶上 rust/cpp 了。

#18 😂啊,C#的 async 我感觉很方便啊,哪里难用了

语法啥的先不说,最主要的是 colored function 和 blocking 的问题。有运行时的语言 goroutine 或 actor 都是不错的模式,kotlin coroutine 实现的 structured concurrency 也还可以。相比之下 c# 和 py 等 (js 由于异步单线程的原因感觉上还好) 就比较难用了,当然 rust/cpp 这些无 gc 语言还有内存管理 pin 之类的问题更加痛苦。

需要到处插桩的 goroutine 实现好。。。。。。。。。?

接这条。
按印象里的搜了一下 SO 找到以前看过的帖子了,随后指向到这个 issue: github.com/dotnet/efcore/issues/18571
简单来说,如果你是用了 ToListAsync(),使用 SqlClient ,同时表结构里存在 VARCHAR(MAX)这样的变长列,就会碰到这个性能问题,好多年还没修,因为要修复就牵扯到非常底层的库。引用 issue 里的一条回复:
Workarounds: Don't use VARCHAR(MAX) or don't use async queries.

希望对你有帮助吧。

color blind function,不用担心 blocking ,插桩用户无感知,不需要用户到处插入 async/await ,你到说说哪里不好?

就单凭一条,你不能控制协程调度,这点就很不好了。

在 gc 语言中你真的需要控制 suspend point 吗?

控制调度和 GC 有什么关系?控制调度的主要目的是为了控制执行上下文,以达到一些特殊的需求。例如复数的独立单线程执行环境,或者并发不并行的执行环境等。

因为用 gc 语言一般不追求 precise control ,也就没有必要显式标识 suspend point 。

goroutine 确实不能为特定的 task 专门配置 executor ,但我觉得这是个 structural 的问题,即 spawn 一个 task 之后无法追踪该 task 后续 spawn 的其他 task 。与隐式插入几个 suspend point 关系不大。如果要防止调用某些系统 API 的时候 goroutine 被 move 的话 runtime.LockOSThread 就可以,能说说有什么场景 gc 语言需要关心 task 具体怎样调度么?

所以我都说了这个和 GC 根本一点关系都没有,自定义调度器的目的是为了控制并行和并发。一个最简单的例子,我需要不定数量的组,每组的协程只并发不并行,但是不同组的协程可以并行。同时我希望一些组的协程执行可以重入,另一些不能重入。Go 里你无法在不使用任何包装的情况下做到,而使用包装就会大幅降低性能同时代码难看难写。

变长的列改成固定长度的,会不会好点呢?

gc 语言的使用场景下一般不需要关心这种问题,尤其 go 这个号称“大道至简”的 opinionated 语言,更是几乎不在意任何 corner case 。不能为特定 task 自定义调度器是 go 的并发模型缺乏 structural 的问题,而不是 green thread 隐式插入 suspend point 本身的问题。如果支持 scoped thread/task 的话,理论上也是可以实现的。我只想到了 FFI 需要防止 task 被 move 到其他 thread (并且与 ffi 的交互也应该被包装而不是影响到上层的行为),除此之外这应该属于运行时提供的抽象层,如果正常写业务的时候需要关心那这个抽象就是 leaky 的。
所以我想知道,有什么具体的,gc 语言的使用场景下,无法包装且需要对某些特定的 task“只并发不并行”的需求呢?

这和 controller 生成的方法有什么关系,慢你看一下日志不就行了吗,框架路由执行执行到结束 ,ef core 查询日志,都有精确的时间。

await/async 是业内公认最佳异步编程模型,新老语言 js Python swift rust 都实现了这个语言特性,它只是编程模型,便利异步开发,难用从何说起?它并不改变开发平台本身的控制调度,你要想自己控制调度不用它就好了。你比较开发平台间异步编程的相同不同就算了,还把这种比较和 await/async 编程模型好不好用联系起来,不仅基本概念错误,比较本身也值得商榷,完全就是伪命题

没有银弹,不同模型都有自己的优劣,没有哪个是“公认最佳”。
我一直说的都是“异步编程模型”,以 goroutine/fiber 为代表的 green thread ,以 erlang 为代表的 actor model ,以 trio 和 kotlin coroutine 为代表的 structural concurrency ,以及经典的 async/await 。
说它难用主要体现在两个方面:

  1. colored function 。几乎所有的库 /函数都要实现两遍,sync 和 async 的版本。
  2. blocking 。async context 里调用同步操作 block 整个 runtime 。
    当然还有代码里到处都是 async/await 关键字,不过这个属于个人偏好。
    你要说不同平台的具体实现,还有 future based (do nothing until awaited) 和 task based (spawned after creating)的区别。无运行时的语言没法使用 green thread ,有 gc 的语言(尤其 Python 的 asyncio 选择前者)用 async/await 并不能说是良好的选择。js 因为最开始就是异步单线程降低了复杂度同时也确实优于回调模式。Rust 的实现还相当不完善,实现自己的 future 还需要 unsafe ( pin projection)。Swift 不了解就不评价了。

如果.net 是谷歌出的,那应该会更好用

不知道你看过 EF core 的文档么?都在 github 上,通过开源社区来维护的.
对比 Facebook 和 Google 的文档,微软强太多了...
扯远了...
docs.microsoft.com/en-us/ef/core/

还是先看看 EF core 打印出来的日志,确认一下是不是查询慢。

大多数开发平台都是同步异步都有,当然要实现 2 种,否则怎么区分,这也能算缺点?异步上下文调用同步方法会阻塞和 async/await 有什么关系?前面我强调了,async await 是用来简化异步编程模型,使源代码的逻辑简明清晰,易于阅读和维护,并没有改变开发平台实现异步编程的控制调度,你非要把编程模型随意扩大解释,把语言特性和背后的实现混为一谈。各个开发平台实现的效果各有好坏,但同步异步都有的开发平台里,async/await 显然是最好的异步编程模型,否则不会在 C#首创后被多个新旧语言学了去。你只是看着好像知道很多知识点,但实际连基本概念都搞不清,只会翻来覆去扯几个名词。

比如我要写一个 redis client ,需要用同步 IO 的 API 和异步 IO 的 API 分别实现一遍,代码逻辑完全一样,为什么不能只写一遍?除了底层语言没有必要同步异步都有,像 go 、erlang 可以全都是异步,异步不增加任何额外的复杂度和学习成本。green thread 也没有这个问题。
至于真正需要全都有的,可以了解一下 zig 的 color blind async ,只需要实现一遍,同步 context 下 async 自动变成 noop 。

异步 context 调用同步方法导致阻塞就是 async/await 模型的缺陷。你要说是实现的问题,你到说说哪个 async/await 的实现可以调用同步方法而不阻塞?

贴里讨论的一直都是 programming pattern ,根本没有提到过 scheduler/runtime 具体实现的问题,上面讨论的也是 task 具有 structural 的特性(await task will resume after all sub task awaited finished)从而可以更细粒度的控制不同的 task 由不同的 executor (单线程 /多线程)执行。这确实是 async/await 比 goroutine 的优点,但如果 green thread 引入了 structural concurrency 也是一样可以实现的。

async/await 在过去一段时间确实非常流行,但没有任何范式可以适用于所有的需求,没有银弹,自然没有哪个能成为是“最好”。
何况 async/await 有很多问题和批评, 如 journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ ,hn 上每几个月都上一次头条,没看过可以看一下。

建议多了解一些不同的语言和范式,C#是一门优秀的语言,但它也有历史的局限性。很多当时的设计但若干年后发现有问题的(比如 void 不能作为泛型参数)。erlang/elixir 可以写出天然分布式的程序,actor model 广泛在 OOP 中使用,green thread 极大的简化了异步 IO 的复杂度,直接编写同步代码并获得异步 IO 的并发性能提升 nearly for free 。structural concurrency 是 trio 的作者提出的,要求任何函数 spawn 的 task 必须在函数返回前结束,希望像现代编程语言的控制流取代 goto 一样取代当前 spawn 的 task 可以自由运行 for any time ,详细阅读 vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
go 里的 errgroup 和 rust 的 scoped_thread 都是该范式的体现,并且被 kotlin 作为了官方的协程范式。

EF 需要 .AsNoTracking() 否则极大影响性能,不过就算跟踪了也不至于才 4000 条数据要 2 分钟,我更倾向于数据库配置有误、数据库机器性能受限、EF 或框架甚至 SqlServer 本身有 BUG ;事实上 dotnet framework 4.5.2 曾出现过严重的方法重载 BUG 。

在 csharp 板块吹 golang 的建议直接屏蔽,连 java 都不如的语言赶紧去 c/cpp 社区吹。

命中友军

我觉得某些人吧,不懂就不要乱说。。。
asp.net core 现在默认是没有同步上下文的。。。
"async context 里调用同步操作 block 整个 runtime"...完全看不懂。。。阻塞大概是在当前有同步上下文的情况下,调用 Task 的同步获取结果的方法,而这个 Task 的同步上下文和当前是同一个才会阻塞。。。
而且阻塞也只是当前线程,阻塞整个 runtime ????????????
另外,太长不看,没有任何参考价值

简单的定位方法:

1 ,打开数据库,show processlist ,看看 db 语句的运行状态,是不是在 send data 。如果是那么就是数据库到 web 服务器慢,或者数据集大。(带宽,走了外网)
2 ,如果 sql 数据库没问题,那么可能是转对象 json 比较大,对象转 json 比较慢(可以缓存下再测试);
3 ,如果只查 100 条,还是那么慢吗?
4 ,是不是时间花在 dnslookup 之类的了?

#12
#35

打印了 sql ,确实不是查询卡的,查询 2ms 就执行完了,在这个 api 的 return 语句执行中,回去遍历模型结构导致的?因为的 F10 看了,一直在循环执行模型属性中的 get 方法。

我改了一下代码,用一个 list 对象暂存查询返回对象,获取列表对象的语句瞬间完成,但是下一句 return 语句是会卡住的,请问这个问题怎么排查呢?

[HttpGet]
public async Task<ActionResult<IEnumerable<ZLJBXXItem>>> GetZLJBXXItem()
{
 List<ZLJBXXItem> lst = await _context.ZLJBXXItem.ToListAsync(); //这句瞬间返回 lst 值
 return lst; // 卡顿在这句
}

楼主是不是做医疗的?不会是杭州的吧?

IEnumerable 改成 list 试试看,你这样返回的是一个可以迭代的对象,

感觉是 ActionResult 的问题?

 [HttpGet(Name = "GetWeatherForecast")]
 public async Task<ActionResult<IEnumerable<WeatherForecast>>> Get()
 {
 await Task.CompletedTask;
 var list = Enumerable.Range(1, 1000).Select(index => new WeatherForecast
 {
 Date = DateTime.Now.AddDays(index),
 TemperatureC = Random.Shared.Next(-20, 55),
 Summary = Summaries[Random.Shared.Next(Summaries.Length)]
 }).ToList();
 return list;
 }

这样的代码也会卡一会

sorry 提供错了

你这样返回只是为了 swagger 可以找到返回类型,可以展示出来
我一般都是这样写的

[HttpGet("List")]
[ProducesResponseType(typeof(List),StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task List()
{
var list = new List();
return Ok(list);
}

你可以参考一下 。

Swagger 的两分钟有没有可能是 Swaggerui 渲染比较慢? 实际的网络请求也同样的慢吗?

JSON 序列化方法,默认用的是微软官方的 。虽然按照微软官方的文档,序列化确实比 Newtonsoft.Json 快,但是不排除你有什么特殊的地方, 所以,替换一下默认 Json 序列化方法 用 Newtonsoft.Json 试试看 。

“block 整个 runtime”
我的理解是 async 其实只是 task 的语法糖,最终还是会追溯到一个线程。
用 await 的话就是把异步变成同步的,并且不会导致因为等待线程执行而阻塞当前线程(等待时会释放 cpu 资源)。
所以说最后不应该是阻塞当前的线程吗,而且同步方法本身就应该阻塞线程啊。。。

为啥你非要往 GC 上拐呢,自定义调度和 GC 到底有什么关系。你要场景,那一个很简单的,房间类游戏,我希望房间内的请求都是单线程处理,但是房间内在处理耗时的异步操作的时候,我任然希望这个房间能处理只读请求。这不就是并发不并行,允许重入。其它的场景挺多的,再比方说 actor 模型那就是典型的这个样的场景。

erlang go 都是面向并发的编程语言,和同步异步都有的开发平台比较,你就像在拿 OLTP 和 OLAP 比较,说 OLTP 为什么没有 OLAP 读性能好。

关于异步同步阻塞,只说.net ,以前的 .net 版本有同步上下文设计,但 .net core 以后的版本就不是这个设计了,前面有人说了,异步任务调用同步方法只阻塞当前任务,哪有阻塞整个应用的,而且实现变了 async/await 编程模型却没有变。即使其他开发平台有你说的这种情况,也正说明和实现有关,和 async/await 没有关系。

另外,你认可的 Kotlin 也有 async/await ,但不影响它有不同的调度器,并且不同的编译目标还用不同的实现。明显,好坏不谈,async/await 编程模型可以有各种实现,但你始终模糊 编程模型和实现 的界限,将它们混在一起。

建议教育别人前先自己厘清概念,还有你的回复中那么多名词不是专有词汇都是有中文翻译的,我和香港客户谈技术问题都不会夹这么多英文词汇,是不是你中文不太好?

看了一下资料,原来是在一个只能单线程的上下文中,以阻塞的方式调用异步方法,会导致异步方法中的线程结束后无法返回结果,因为返回结果需要当前的上下文继续运行,而上下文又因为等待结果被阻塞了,从而陷入死锁。如果上下文允许多线程则不会发生这个事情,所以说最好就是不要使用阻塞方式调用异步方法。

我怀疑你实体里有循环引用。不要返回整个实体,试试用 DTO ,只返回用到的数据。

他们写的时候一定觉得自己知识很渊博

我想给你 1000000 个感谢,强行夹英文的中文看的想吐,有本事全用英文啊,这里的人又不是看不懂。

我说 gc 是因为 gc 语言的应用场景主要是写业务和应用,只要在该执行的时候执行就好,不关心具体如何调度。同时 gc 语言也不在意一些小小的 overhead ,所以可能并不值得 async/await 造成的过度的复杂度。我倒是不了解确实有这种需求,所以请教一下你这个例子中如果并行了会怎样?或者说为什么不能简单的用 task 处理所有的任务?

go 太多狂教徒,和普通人不在一个频道,就喜欢扯一些专有名词左顾右盼,不回答具体问题,基本没有交流价值

同步异步都有不外乎:

  1. 该平台早期就没有异步,异步是后来加的。
  2. 异步有较大的复杂度,不值得总是使用。
  3. 底层无运行时的语言,当然需要都支持。

async/await 无论哪个实现形式都是一样的:函数转换成一个状态机,内部的 await 作为显式的 suspend point ,当执行到的时候将函数挂起,状态保存,然后控制流返回给 runtime 。runtime 在某个时间点再次 schedule 该 task ,直到结束。
异步 context 中调用阻塞操作会使 runtime 无法拿回控制流,整个线程就被阻塞了,而不单单是一个 task 。而这个线程是 runtime 的 worker 线程。如果 runtime 有 10 个 worker 线程并发处理一万个 task ,那么阻塞一个就只剩 9 个线程可以执行其他 task 了,并且由于 task 都是写并发的,代码里有阻塞部分大概率不止会阻塞一个线程,这样就会导致整个系统的吞吐量大幅下降,也就是阻塞整个 runtime 。

kotlin 则是完全不同的模式,虽然和 async/await 有一定的相似性,但这和调度器有啥关系?

你可以考虑下系统防火墙之类原因,GetWeatherForecast 这段代码刚我测试了下返回是很快的,没有问题

调度和 GC 完全是两回事,外加这和性能压根没关系。你是完全没了解过 actor 相关的东西么,要是这样的话你去看看 dotnet 组的 Orleans 项目,看看他们的编程模型如果不能自定义调度器的话,要怎么优雅的实现。

await 不会阻塞线程哈,线程在等待 task 完成时候,会去执行其它 task

当然,我只是说既然接受了 GC 更容易接受其他具有一定 overhead 但降低复杂度的方式。
orleans 是分布式的东西么,那就理解了,分布式有什么需求我都不奇怪。erlang/elixir 就是 green thread 和 actor model ,需不需要自定义调度器我不清楚,不过显然与 async/await 没啥关系。

await 会把函数挂起控制流还给 runtime ,当然不会阻塞。我说的是调用同步操作,如 Thread.sleep 。

async/await 只是一个叫法,准确的说是需要能够自定义执行流的暂停和恢复。但是目前主流语言里能做到这点的基本都叫 async/await 所以说强依赖也当然没错。另外这个需求和分布式其实关系不大,单纯的只是和编程模型有关系。还有我想说的是,GC 不代表就一定要有开销但是降低复杂度。你看看最近微软对 dotnet 做的很多更新,显然是相反的操作,包括 Span 、Memory 等等这类东西。

哎呀。头疼

async/await 是显式标明 suspend point 的一种方式,相对应的是隐式插入 suspend point 。你要具体控制调度只要 spawn 的时候能提供参数就好了,不依赖显式或者隐式。

我觉得语言设计还是要尽量专注,不应该追求满足所有需求。开一些 unsafe 的后门倒也没啥,有需要可以用,不用时也不会有影响。但整体设计上有开销时同时导致不必要的复杂性,就不算良好的设计了。

如果隐式插桩你还要控制调度,自定义调度实现起来会比显示调度更加麻烦,而且隐式调度还会有难以区分同步和异步调用的问题。

插桩(构造 task )只是插桩,和调度(执行 task )没有关系的啊。
关于区分,你可以隐式 suspend(await),但显式 async ,kotlin 就是这样子。go 则是只能写“同步”的代码。

构造 task 和调度 task 当然没关系,但是隐式也就意味着你不会显示把 Task 当返回值写上,这样调用方是不能区分这个方法到底是想让你异步调用还是同步调用。kt 那个异步不管是用还是自定义调度,都比 C#麻烦的多,是个很好的例子。

同步还是异步是由调用方决定的,方法本身只要实现好自己的逻辑,不用关心自己被如何调用啊。
kotlin 的方式更直白,suspend 表明该函数内部存在 suspend point ,要同步调用那就直接调用,要异步调用那就 async 包一层再调用。从另一个角度来说,可以看作是 async 每次被调用时自动添加了 await ,平时 async 紧跟 await 的是最常见的,这样反而减少了很多输入。scope 的存在也让细粒度的控制(包括调度、取消等)变得更加容易。zig 也采用了类似的方式,同时创造性地解决了 colored function 问题( async 在同步环境中自动变成 noop )。
像 go 那样干脆把异步调用砍掉,只能写“同步”代码,也不失为一种不错的方式,在其目标领域表现良好。

kt 那种模式和 await 没有什么区别,我反而觉得变得麻烦而且不直观。我认为良好的方式需要满足两个条件,一个是显示告诉调用者这个方法内是否有异步调用,第二个是显示进行异步等待。所以我并不太喜欢 kt 那个模式。以及我之前尝试给 kt 定制调度器,觉得比 C#麻烦。

长篇大论文字讨论不如写代码来明示各种方式的优劣。

懒,这玩意一写就是几百行还得加注释和两份代码对比,不想写,又不是做教程。