[C#] 병렬 프로그래밍 실습: 병렬 및 비동기 코드에 대한 단위 테스트 케이스 작성

        이 장에서는 병렬 및 비동기 코드에 대한 단위 테스트를 작성하는 방법을 설명합니다 . 단위 테스트 작성은 대규모 프로젝트에서 중요한 부분이며, 강력하고 안정적이며 유지 관리가 쉬운 코드를 위한 불가피한 요구 사항입니다. 하지만 이 장의 코드는 기본적으로 Unity와 관련이 없으며 적용할 수 없습니다. 하지만 Unity에서는 여전히 단위 테스트가 필요합니다.여기에서는 Unity 기반의 단위 테스트에 대해 설명합니다.

        이 튜토리얼로 엔지니어링 배우기: Magician Dix/HandsOnParallelProgramming · GitCode


1. 안전하게 작업 시작

        이전 연구에 따르면 Task.Run을 직접 사용하면 예외가 발생하지 않는다는 것을 배웠습니다. 모든 메소드에서 Try-Catch를 직접 작성하는 것은 너무 번거로우므로 일반적인 Task 안전 작성 방법을 사용하는 것이 더 편리합니다.

        이전 장에서 언급했듯이 Try-Catch는 스레드를 교차할 수 없으므로 스레드는 대기하고 모든 오류를 수집하여 발생시켜야 하며, 이 대기는 중단되는 것을 방지하기 위해 메인 스레드에서 기다릴 수 없습니다.

        public static void RunTaskSafe(Func<Task> function)
        {
            Task.Run(() =>
            {
                try
                {
                    var task = Task.Run(function);
                    task.Wait();
                }
                catch (AggregateException ex)
                {
                    Debug.LogError(ex.Message);
                    Debug.LogError(ex.InnerException);
                }
            });
        }

        위의 예제 코드에서 볼 수 있듯이 메인 스레드를 차단하지 않고도 오류가 올바르게 발생될 수 있습니다.

 

2. 작업 수행 테스트

        일반적인 접근 방식은 Task.Wait를 사용하는 것이며, 그러면 시간을 직접 확인할 수 있습니다.

Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();

var task = Task.Run(function);
task.Wait();

stopwatch.Stop();
Debug.Log($"【{function.Method.Name} 】耗时:{stopwatch.Elapsed.TotalMilliseconds}");

        위와 같이 정상적으로 동작하며, 기능 소요 시간을 출력할 수 있습니다.

        그러나 분명히 이런 종류의 인쇄는 시간이 많이 걸리고 불편하며 GC 및 성능 오버헤드도 있습니다.핵심 노드 디버깅 중에만 성능을 확인할 수 있지만 실제로 대규모로 사용할 수는 없습니다.

        가장 좋은 해결책은 당연히 Unity의 프로파일러 창에서 성능 보고서를 보는 것입니다.

https://docs.unity3d.com/cn/2022.2/ScriptReference/Profiling.Profiler.BeginThreadProfiling.html icon-default.png?t=N6B9https://docs.unity3d.com/cn/2022.2/ScriptReference/Profiling.Profiler.BeginThreadProfiling.html         실제로 가능합니다 그것은 할 수 있습니다. Unity 2017.3.0 이후 버전에서는 Unity가 멀티스레드 성능 분석에 적합한 인터페이스인 Profiling.BeginThreadProfiling을 추가합니다. 따라서 Task 시작 쓰기 방식을 다음과 같이 수정하겠습니다.

        public static Task Run(Action action)
        {
            if (action == null)
                return Task.CompletedTask;

            var task = Task.Run(() =>
            {
                try
                {
#if TASK_PROFILE
                    var method = action.Method;
                    if (method == null)
                        return;

                    Profiler.BeginThreadProfiling("GYTask", $"GYTask_{Task.CurrentId}");
                    Profiler.BeginSample(method.GetMethodName());
#endif
                    //在子线程运行全部任务,并等待完成以收集错误
                    var task = Task.Run(action);
                    task.Wait();

#if TASK_PROFILE
                    Profiler.EndSample();
                    Profiler.EndThreadProfiling();
#endif
                }
                catch (AggregateException ex)
                {
                    UnityEngine.Debug.LogError(ex.Message);
                    UnityEngine.Debug.LogError(ex.InnerException);
                }
            });

            return task;
        }

        시간이 많이 걸리는 기능을 실행한 후 프로파일러에서 그 효과를 확인할 수 있습니다.

         물론, 메인 스레드에서는 그 효과를 볼 수 없지만, 타임라인을 보면 이 코드 부분이 평행하다는 것을 알 수 있습니다. 마찬가지로 계층 구조에서 해당 스레드를 선택하여 실행 상태를 볼 수도 있습니다.

         VS에서 제공하는 도구보다 이게 훨씬 좋은 것 같아요!

3. 일부 성능 오버헤드 문제

        성능 테스트 도구를 사용하면 일부 TPL의 성능을 테스트할 수 있습니다. 여기서는 제가 더 관심을 두는 몇 가지 영역을 선택했습니다.

3.1. 태스크 생성 비용

        이는 테스트하기가 더 쉽고 메인 스레드에서 직접 측정할 수 있습니다.

         이는 주로 GC 문제임을 알 수 있습니다.

  • Task.Run 자체의 구성으로 인해 GC(~ 0.8KB)가 발생합니다.

  • 익명 함수의 구성 자체도 GC(~ 0.12KB)를 생성합니다.

        시간 소모 측면에서 한 번에 Task를 생성하는 데 약 0.1ms가 소요됩니다(위 그림에서 10개 시작은 약 1ms 소요). 요약하면 작업을 시작하는 데 0.1ms가 걸리고 1KB GC 발생합니다 .

        익명 함수를 생성하는데 소요되는 GC와 시간 소모적인 비용은 어떤 방법으로든 소모될 수 있지만, 본 장에서는 이에 대해 논의하지 않고, 후속 연구에서 해결책이 있다면 활용 가능하다.

3.2 Task.Delay의 오버헤드

        위의 성능 테스트 예시를 보면 Task.Delay가 실제로 스레드를 채우는 특수 함수라는 것을 알 수 있는데, GC는 없지만 매 프레임마다 꽉 차는 걸 확인하는 데 시간이 걸린다.

        Delay만으로는 CPU 작동 압력에 큰 영향을 미치지 않을 수 있지만 성능 테스트에서는 상당한 피크를 볼 수 있습니다.

3.3 신호등 비용

        지연과 같은 세마포어는 프로파일러에 오버헤드를 표시할 수 있습니다.

         결국 이것과 Delay는 모두 하위 스레드를 차단하는 것으로 이해될 수 있습니다.

        물론 이 테스트는 실제로 성능 테스트 코드를 작성하는 방식과 관련이 있는데, 작업이 완료될 때까지 기다리는 시간을 계산하기 때문에 이 메서드가 계속 실행되는 한 오버헤드가 발생하게 됩니다.


4. 이 장의 섹션

        이 장에서는 멀티스레딩의 두 가지 주요 문제점, 즉 오류 발생 불가와 성능 오버헤드 확인 불가능을 해결하는 Unity의 일반적인 TPL 작성 방법의 예를 소개합니다. 개인적으로는 상당히 유용하다고 생각하며, 코드를 참고자료로 활용해도 좋을 것 같습니다.

        물론, 모든 API에 적응하려면 여전히 다양한 후속 추가가 필요합니다.

        이 튜토리얼로 엔지니어링 배우기: Magician Dix/HandsOnParallelProgramming · GitCode

추천

출처blog.csdn.net/cyf649669121/article/details/131911948