Saya punya situs MVC yang menggunakan Entity Framework 6 untuk menangani database, dan saya telah bereksperimen dengan mengubahnya sehingga semuanya berjalan sebagai pengontrol async dan panggilan ke database dijalankan sebagai rekan async mereka (mis. ToListAsync () alih-alih ToList ())
Masalah yang saya alami adalah bahwa hanya mengubah pertanyaan saya ke async telah menyebabkan mereka menjadi sangat lambat.
Kode berikut mendapatkan koleksi objek "Album" dari konteks data saya dan diterjemahkan ke dalam basis data yang cukup sederhana:
// Get the albums
var albums = await this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToListAsync();
Inilah SQL yang dibuat:
exec sp_executesql N'SELECT
[Extent1].[ID] AS [ID],
[Extent1].[URL] AS [URL],
[Extent1].[ASIN] AS [ASIN],
[Extent1].[Title] AS [Title],
[Extent1].[ReleaseDate] AS [ReleaseDate],
[Extent1].[AccurateDay] AS [AccurateDay],
[Extent1].[AccurateMonth] AS [AccurateMonth],
[Extent1].[Type] AS [Type],
[Extent1].[Tracks] AS [Tracks],
[Extent1].[MainCredits] AS [MainCredits],
[Extent1].[SupportingCredits] AS [SupportingCredits],
[Extent1].[Description] AS [Description],
[Extent1].[Image] AS [Image],
[Extent1].[HasImage] AS [HasImage],
[Extent1].[Created] AS [Created],
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134
Seperti yang terjadi, ini bukan permintaan yang sangat rumit, tapi butuh hampir 6 detik untuk menjalankan server SQL. SQL Server Profiler melaporkannya untuk menyelesaikan 5742 ms.
Jika saya mengubah kode saya ke:
// Get the albums
var albums = this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToList();
Kemudian SQL yang sama persis dihasilkan, namun ini berjalan hanya dalam 474ms menurut SQL Server Profiler.
Basis data memiliki sekitar 3500 baris dalam tabel "Album", yang tidak terlalu banyak, dan memiliki indeks pada kolom "Artist_ID", jadi itu harus cukup cepat.
Saya tahu bahwa async memiliki overhead, tetapi membuat segalanya menjadi sepuluh kali lebih lambat sepertinya agak curam bagi saya! Di mana saya salah di sini?
sumber
Jawaban:
Saya menemukan pertanyaan ini sangat menarik, terutama karena saya menggunakan di
async
mana-mana dengan Ado.Net dan EF 6. Saya berharap seseorang memberikan penjelasan untuk pertanyaan ini, tetapi itu tidak terjadi. Jadi saya mencoba mereproduksi masalah ini di pihak saya. Saya harap beberapa dari Anda akan menemukan ini menarik.Berita baik pertama: Saya mereproduksinya :) Dan perbedaannya sangat besar. Dengan faktor 8 ...
Pertama saya mencurigai sesuatu yang berhubungan dengan
CommandBehavior
, karena saya membaca artikel menarik tentangasync
Ado, mengatakan ini:"Karena mode akses non-sekuensial harus menyimpan data untuk seluruh baris, itu dapat menyebabkan masalah jika Anda membaca kolom besar dari server (seperti varbinary (MAX), varchar (MAX), nvarchar (MAX) atau XML) ). "
Saya mencurigai
ToList()
panggilan menjadiCommandBehavior.SequentialAccess
asyncCommandBehavior.Default
(tidak berurutan, yang dapat menyebabkan masalah). Jadi saya mengunduh sumber-sumber EF6, dan meletakkan breakpoints di mana-mana (diCommandBehavior
mana digunakan, tentu saja).Hasil: tidak ada . Semua panggilan dibuat dengan
CommandBehavior.Default
.... Jadi saya mencoba masuk ke kode EF untuk memahami apa yang terjadi ... dan ... ooouch ... Saya tidak pernah melihat kode pendelegasian seperti itu, semuanya tampak malas dijalankan ...Jadi saya mencoba membuat profil untuk memahami apa yang terjadi ...
Dan saya pikir saya punya sesuatu ...
Inilah model untuk membuat tabel yang saya benchmark, dengan 3.500 baris di dalamnya, dan masing-masing 256 Kb data acak
varbinary(MAX)
. (EF 6.1 - CodeFirst - CodePlex ):Dan inilah kode yang saya gunakan untuk membuat data uji, dan benchmark EF.
Untuk panggilan EF biasa (
.ToList()
), pembuatan profil tampak "normal" dan mudah dibaca:Di sini kita menemukan 8,4 detik yang kita miliki dengan Stopwatch (membuat profil memperlambat perf). Kami juga menemukan HitCount = 3500 di sepanjang jalur panggilan, yang konsisten dengan 3500 baris dalam tes. Di sisi parser TDS, hal-hal mulai menjadi lebih buruk sejak kita membaca 118 353 panggilan pada
TryReadByteArray()
metode, yang merupakan loop buffering terjadi. (rata-rata 33,8 panggilan untuk masing-masingbyte[]
256kb)Untuk
async
kasus ini, ini benar-benar sangat berbeda .... Pertama,.ToListAsync()
panggilan dijadwalkan di ThreadPool, dan kemudian ditunggu. Tidak ada yang luar biasa di sini. Tapi, sekarang, inilah yang adaasync
di ThreadPool:Pertama, dalam kasus pertama kami hanya memiliki 3500 hitungan hit di sepanjang jalur panggilan penuh, di sini kami memiliki 118 371. Selain itu, Anda harus membayangkan semua panggilan sinkronisasi yang saya tidak lakukan pada screenshoot ...
Kedua, dalam kasus pertama, kami memiliki panggilan "hanya 118 353" untuk
TryReadByteArray()
metode ini, di sini kami memiliki 2 050 210 panggilan! Ini 17 kali lebih banyak ... (pada tes dengan array 1Mb besar, 160 kali lebih banyak)Apalagi ada:
Task
instance dibuatInterlocked
panggilanMonitor
panggilanExecutionContext
instance, dengan 264 481 CaptureSpinLock
panggilanDugaan saya adalah buffering dibuat dengan cara async (dan bukan yang bagus), dengan Tugas paralel mencoba membaca data dari TDS. Terlalu banyak tugas yang dibuat hanya untuk mem-parsing data biner.
Sebagai kesimpulan awal, kita dapat mengatakan Async hebat, EF6 hebat, tetapi penggunaan async EF6 dalam penerapannya saat ini menambahkan overhead yang besar, di sisi kinerja, sisi Threading, dan sisi CPU (penggunaan CPU 12% di
ToList()
case dan 20% dalamToListAsync
case untuk kerja 8 sampai 10 kali lebih lama ... Saya menjalankannya pada i7 920 lama).Sambil melakukan beberapa tes, saya memikirkan artikel ini lagi dan saya perhatikan sesuatu yang saya lewatkan:
"Untuk metode asinkron baru di. Net 4.5, perilaku mereka persis sama dengan metode sinkron, kecuali untuk satu pengecualian penting: ReadAsync dalam mode non-sekuensial."
Apa ?!!!
Jadi saya memperluas tolok ukur saya untuk memasukkan Ado.Net dalam panggilan biasa / async, dan dengan
CommandBehavior.SequentialAccess
/CommandBehavior.Default
, dan inilah kejutan besar! :Kami memiliki perilaku yang sama persis dengan Ado.Net !!! Telapak tangan...
Kesimpulan definitif saya adalah : ada bug dalam implementasi EF 6. Seharusnya beralih
CommandBehavior
keSequentialAccess
ketika panggilan async dilakukan atas tabel yang berisibinary(max)
kolom. Masalah membuat terlalu banyak Tugas, memperlambat proses, ada di sisi Ado.Net. Masalah EF adalah tidak menggunakan Ado.Net sebagaimana mestinya.Sekarang Anda tahu daripada menggunakan metode async EF6, Anda lebih baik harus memanggil EF dengan cara non-async biasa, dan kemudian menggunakan a
TaskCompletionSource<T>
untuk mengembalikan hasilnya dengan cara async.Catatan 1: Saya mengedit posting saya karena kesalahan yang memalukan .... Saya telah melakukan tes pertama saya melalui jaringan, bukan secara lokal, dan bandwidth terbatas telah mengubah hasil. Berikut adalah hasil yang diperbarui.
Catatan 2: Saya tidak memperluas pengujian saya ke kasus kegunaan lain (mis:
nvarchar(max)
dengan banyak data), tetapi ada kemungkinan perilaku yang sama terjadi.Catatan 3: Sesuatu yang biasa untuk
ToList()
kasus ini, adalah CPU 12% (1/8 dari CPU saya = 1 inti logis). Sesuatu yang tidak biasa adalah maksimum 20% untukToListAsync()
kasus ini, seolah-olah Penjadwal tidak dapat menggunakan semua Tapak. Itu mungkin karena terlalu banyak Tugas yang dibuat, atau mungkin hambatan dalam TDS parser, saya tidak tahu ...sumber
.ToListAsync()
dan.CountAsync()
... Untuk siapa pun yang menemukan utas komentar ini , permintaan ini dapat membantu. Hasil positif.Karena saya mendapat tautan ke pertanyaan ini beberapa hari yang lalu saya memutuskan untuk mengirim pembaruan kecil. Saya dapat mereproduksi hasil dari jawaban asli menggunakan, saat ini, versi terbaru EF (6.4.0) dan .NET Framework 4.7.2. Anehnya masalah ini tidak pernah diperbaiki.
Ini menimbulkan pertanyaan: Apakah ada peningkatan dalam dotnet core?
Saya menyalin kode dari jawaban asli ke proyek dotnet core 3.1.3 baru dan menambahkan EF Core 3.1.3. Hasilnya adalah:
Anehnya ada banyak perbaikan. Tampaknya masih ada jeda waktu karena threadpool dipanggil tetapi ini sekitar 3 kali lebih cepat daripada implementasi .NET Framework.
Saya harap jawaban ini membantu orang lain yang dapat mengirim cara ini di masa depan.
sumber