신경 쓰지 않았는데 몰랐던 것

여러 task를 한번에 실행하고 개별적으로 await하려고 하는 상황에서

var tasks = behaviors.Select(x => x.ExecuteAsync());

foreach (var task in tasks)
{
await task;
}

와 같이 코드를 구현하다 병렬 실행이 아닌 순차적 실행이 되길래
찾아보니 IEnumerable은 Lazy평가해서 await하는 순간 task를 실행하는 것을
알았네요.
또 ToArray()와 같이 모든 원소가 평가 되게 끔 하면 병렬로 실행이 되구요…

IEnumerable을 크게 신경 쓰지 않았었는데, 지연 평가하여 효율적인 사용을 하는 것은 처음 알았네요…

8개의 좋아요

IEnumerable이 lazy evaluation한다는 특징은 정말 중요한 것 같아요.
Task관련 아니더라도 디버깅 할 때 헤매기도 하더라고요.

2개의 좋아요

저도 코드를 보는 순간 헷갈렸음을 시인합니다.

1개의 좋아요

제가 아는 선에서 조금 살을 붙이면 tasks의 평가는 await하는 순간이 아니라 tasks 객체를 처음 사용하게 될 때 됩니다.

그러므로 실제 tasks의 평가는 foreach문의 tasks입니다.

적어주신 코드를 보면 평가된 tasks를 하나씩 await하고 계셔서 순차 실행이 되는 것이고

foreach문 안에서 await를 하지 않으면 말씀하신 ToList()ToArray()로 즉시평가하는 것처럼 동작합니다.

var tasks = behaviors.Select(x => x.ExecuteAsync());

foreach (var task in tasks) // tasks의 평가는 여기서 되고
{
  // 각각의 tasks를 await하지 않으면 마치 병렬로 실행 된 것처럼 결과가 나온다.
  //await task;
}

그리고 지연평가가 좋은 것이긴 한데 멀티 쓰레드 작업을 하다보면 각 쓰레드끼리 아직 평가되지 않은 IEnumerable을 공유하게 되면서 원하는 결과가 나오지 않을 때도 있습니다. 이럴 때는 말씀하신 것 처럼 linq작성 후 즉시 ToList()등으로 평가해서 사용해야합니다.

1개의 좋아요


테스트 해봤는데 .Net 8.0에서는 (var task in tasks) 에서
var task 행으로 넘어갈 때 0번 인덱스가 평가됩니다.
tasks는 그전에 평가되지 않았구요…
저 상황에서 콘솔에 0번이 표시된 라인만 출력되는데, 뭔가 잘못 테스트한게 있을까요?

아니요 없습니다.
딱! 정상이네요!
foreach 돌 때 tasks (IEnumerable)에 있는 것 중 첫 번째 것을 꺼내오려고 시도해요.
foreach 내부가 어떻게 구현되어 있을까 생각해보시면 도움이 됩니다.

다르게 생각하면 Select함수가 어떤 동작을 하는지 보는 것도 좋습니다.
결국 Iterator를 반환할 뿐입니다 :slight_smile:
https://source.dot.net/#System.Linq/System/Linq/Select.cs

Select.cs 코드를 보시면 selector(x, i) => x.RunAsync(i) 인데요.
selector가 언제 실행되시는지를 봐야 합니다.
같은 파일의 IEnumerableSelectIterator를 보시면 MoveNext함수에서 _selector를 호출하는 것을 볼 수 있습니다.
누가 언제 MoveNext를 호출하는가?
이제 foreach의 구현을 볼 차례입니다.
그것은 직접…ㅋㅋ (저는 여기까지만 보겠습니다.)

1개의 좋아요

아~ 평가라는 말에서 약간에 오해가 있던 것 같습니다.

제가 말한 평가라는 것은 linq로 만들어진 메서드 호출 체인이 확정 되는 것을 말한 것이였습니다.

foreachtasks에서 linq식이 평가(확정) 되고 실제 평가 된 식으로 실행되는 것은 직접 시퀀스에 접근했을 때가 맞습니다.

저도 잘 모르고 썼다가 나중에 대공사 하는 경우가 종종 있어요

Linq 가 반환한는 IEnumerable의 Lazy 실행의 정확한 시점을 알고 싶다면, foreach 를 while 문으로 변경하면 됩니다.

var enumerator = tasks.GetEnumerator();
while(true)
{
    Console.WriteLine("MoveNext() 호출");

    if (enumerator.MoveNext() is false) break;
   
    Console.WriteLine("Current get");
    var task = enumerator.Current;

    Console.WriteLine("await task");
    await task;
    // ..
}
2개의 좋아요