Entity Framework 쿼리 가능 비동기


98

Entity Framework 6을 사용하여 일부 웹 API 관련 작업을하고 있으며 컨트롤러 메서드 중 하나는 내 데이터베이스에서 테이블의 내용을 IQueryable<Entity>. 내 저장소에서 비동기로 EF를 사용하는 것이 처음이므로 비동기 적으로 수행하는 유리한 이유가 있는지 궁금합니다.

기본적으로 그것은

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

vs

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

비동기 버전은 실제로 여기에서 성능 이점을 얻을 수 있습니까? 아니면 먼저 목록에 투영하고 (비 동기화를 사용하여) IQueryable로 이동하여 불필요한 오버 헤드가 발생합니까?


1
context.Urls는 IQueryable <URL>을 구현하는 DbSet <URL> 유형이므로 .AsQueryable ()이 중복됩니다. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx EF에서 제공하는 패턴을 따르거나 컨텍스트를 생성하는 도구를 사용했다고 가정합니다.
Sean B

답변:


225

문제는 Entity Framework에서 async / await가 작동하는 방식을 오해 한 것 같습니다.

Entity Framework 정보

따라서이 코드를 살펴 보겠습니다.

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

그리고 그것의 사용 예 :

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

거기에서 무슨 일이 일어나나요?

  1. 우리는 IQueryable객체 를 얻습니다 (아직 데이터베이스에 접근하지 않음)repo.GetAllUrls()
  2. 우리는 새로운 IQueryable 사용하여 지정된 조건 개체를.Where(u => <condition>
  3. 우리는 새로운 IQueryable 지정된 페이징 제한이 개체를.Take(10)
  4. 를 사용하여 데이터베이스에서 결과를 검색합니다 .ToList(). 우리의 IQueryable객체는 (같은 select top 10 * from Urls where <condition>) sql로 컴파일됩니다 . 그리고 데이터베이스는 인덱스를 사용할 수 있으며 SQL Server는 데이터베이스에서 10 개의 개체 만 보냅니다 (데이터베이스에 저장된 10 억 개의 URL 모두는 아님).

좋아, 첫 번째 코드를 보자.

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

동일한 사용 예를 통해 다음을 얻었습니다.

  1. 을 사용하여 데이터베이스에 저장된 수십억 개의 URL을 모두 메모리에로드하고 await context.Urls.ToListAsync();있습니다.
  2. 메모리 오버플로가 발생했습니다. 서버를 죽이는 올바른 방법

async / await 정보

async / await가 선호되는 이유는 무엇입니까? 이 코드를 살펴 보겠습니다.

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

여기서 무슨 일이 일어나나요?

  1. 라인 1에서 시작 var stuff1 = ...
  2. 우리는 몇 가지 물건을 얻고 싶은 SQL 서버에 요청을 보냅니다. userId
  3. 우리는 기다립니다 (현재 스레드가 차단됨)
  4. 우리는 기다립니다 (현재 스레드가 차단됨)
  5. .....
  6. SQL 서버는 우리에게 응답을 보냅니다.
  7. 2 호선으로 이동 var stuff2 = ...
  8. 우리는 무언가를 얻고 싶은 SQL 서버에 요청을 보냅니다. userId
  9. 우리는 기다립니다 (현재 스레드가 차단됨)
  10. 다시 한번
  11. .....
  12. SQL 서버는 우리에게 응답을 보냅니다.
  13. 뷰 렌더링

따라서 비동기 버전을 살펴 보겠습니다.

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

여기서 무슨 일이 일어나나요?

  1. stuff1을 얻기 위해 SQL Server에 요청을 보냅니다 (1 행).
  2. stuff2를 얻기 위해 SQL Server에 요청을 보냅니다 (2 행).
  3. SQL 서버의 응답을 기다리지 만 현재 스레드가 차단되지 않고 다른 사용자의 쿼리를 처리 할 수 ​​있습니다.
  4. 뷰 렌더링

그것을하는 올바른 방법

여기에 좋은 코드 :

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

IQueryable에 대한 using System.Data.Entity메서드를 사용 하려면 추가해야합니다 ToListAsync().

필터링 및 페이징 등이 필요하지 않은 경우 IQueryable. await context.Urls.ToListAsync()materialized를 사용 하고 작업 할 수 있습니다 List<Url>.


3
사진을보고 @Korijn i2.iis.net/media/7188126/... 에서 IIS 아키텍처 소개 나는 IIS의 모든 요청이 비동기 방식으로 처리되는 것을 말할 수있다
빅토르 Lova

7
GetAllUrlsByUser메서드 의 결과 집합에 대해 작업하지 않기 때문에 비동기로 만들 필요가 없습니다. 태스크를 반환하고 컴파일러에 의해 생성되는 불필요한 상태 머신을 저장하십시오.
Johnathon Sullinger 2016

1
@JohnathonSullinger 그것이 행복한 흐름에서 작동하지만, 여기에 예외가 나타나지 않고 기다리고있는 첫 번째 장소로 전파되는 부작용이 없습니까? (그것이 꼭 나쁘지는 않지만 행동의 변화입니까?)
Henry Been

9
흥미롭게도 "About async / await"의 두 번째 코드 예제가 전혀 의미가 없다는 사실을 아는 사람은 없습니다. EF와 EF Core가 스레드로부터 안전하지 않기 때문에 예외가 발생하므로 병렬로 실행하려고하면 예외가 발생합니다.
Tseng

1
이 답변이 정확하지만, 내가 사용하지 않는 것이 좋습니다 것입니다 asyncawait당신이 목록 작업을 수행하지 않는 경우. 발신자에게 맡기 await십시오. 이 단계에서 호출을 기다릴 return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();때 어셈블리를 디 컴파일하고 IL을 볼 때 추가 비동기 래퍼를 생성합니다.
Ali Khakpouri

10

게시 한 예제 인 첫 번째 버전에는 큰 차이가 있습니다.

var urls = await context.Urls.ToListAsync();

이것은 나쁘다 , 기본적으로 select * from table모든 결과를 메모리에 반환 한 다음 where이를 수행하는 대신 메모리 컬렉션에 적용합니다.select * from table where... 데이터베이스에 대해.

두 번째 방법은 쿼리가 IQueryable(아마도 .Where().Select()쿼리와 일치하는 db 값만 반환 하는 linq 스타일 작업을 통해) 쿼리가 적용될 때까지 실제로 데이터베이스에 도달하지 않습니다 .

예제가 비교 가능한 경우 async컴파일러가 async기능 을 허용하기 위해 생성하는 상태 머신에 더 많은 오버 헤드가 있으므로 요청 당 버전이 일반적으로 약간 느려집니다 .

그러나 가장 큰 차이점 (및 이점)은 asyncIO가 완료되기를 기다리는 동안 처리 스레드를 차단하지 않기 때문에 버전이 더 많은 동시 요청을 허용 한다는 것입니다 (db 쿼리, 파일 액세스, 웹 요청 등).


7
쿼리가 IQueryable에 적용될 때까지 .... IQueryable.Where 및 IQueryable.Select 모두 쿼리를 강제 실행합니다. 사전은 술어를 적용하고 후자는 프로젝션을 적용합니다. ToList, ToArray, Single 또는 First와 같은 구체화 연산자가 사용될 때까지 실행되지 않습니다.
JJS

0

간단히 말해서,
IQueryableRUN 프로세스를 연기하고 먼저 다른 IQueryable표현 과 함께 표현을 구축 한 다음 표현 전체를 해석하고 실행 하도록 설계되었습니다 .
그러나 ToList()방법 (또는 이와 유사한 몇 가지 방법)은 "있는 그대로"식을 즉시 실행해야합니다.
첫 번째 메서드 ( GetAllUrlsAsync)는 메서드 가 IQueryable뒤 따르기 때문에 즉시 실행 됩니다 ToListAsync(). 따라서 즉시 (비동기) 실행되고 IEnumerables를 반환합니다 .
한편 두 번째 방법 ( GetAllUrls)은 실행되지 않습니다. 대신 표현식을 리턴하고이 메소드의 CALLER가 표현식을 실행합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.