.NET Tools
Essential productivity kit for .NET and game developers
JetBrains Rider에서 Tasks(작업) 뷰를 사용하는 방법
Task Parallel Library(TPL)는 프레임워크가 멀티스레드 병렬 코드를 작성하고 실행할 수 있도록 하므로 모든 .NET 애플리케이션에 필수적입니다. 또한, 리소스를 최대한 활용하려는 개발자라면 System.Threading
및 System.Threading.Tasks
에 있는 타입을 활용하여 맞춤형 코드를 작성하고 싶을 것입니다. 이를 위해서는 잠금, 교착 상태, await 및 스케줄링과 같이 빠르고 확장 가능한 솔루션을 작성할 때 필요한 동시성 및 스레드의 기초를 이해해야 합니다. 이러한 개념에 대한 이해를 확장하려면 혼란스럽게 실행되는 작업을 시각화하고 이해하는 데 도움을 주는 탁월한 도구가 필요합니다.
좋은 소식이 있습니다! 최근 Tasks(작업) 뷰의 첫 번째 신규 버전이 출시되었습니다. Tasks 뷰는 현재 애플리케이션 프로세스 내 작업을 이해할 수 있도록 도와주는 강력한 도구입니다.
이 글에서는 새로운 도구 창을 살펴보고, 필수 UI 요소를 논의한 다음, 일반적인 시나리오를 몇 가지 설명합니다. 다 읽고 난 후 여러분은 자신의 코드 베이스를 살펴보며 최적화 가능성을 찾을 수 있게 될 것입니다.
.NET 애플리케이션에서 작업 실행
.NET에서 Tasks(작업)는 동시성 및 멀티스레딩과 같은 개념에 대한 추상화를 제공합니다. 목적은 CPU 코어나 스레드에 대한 걱정을 줄이고 스케줄링이나 작업 동시 실행과 같은 더 상위의 개념에 집중하도록 하는 것입니다. 이는 개발자들이 시스템 리소스를 효율적으로 활용하면서 명령형 코드를 작성하는 데 집중하도록 도와주기 때문에 보통은 매우 유용합니다.
그러나 그 유용성에도 불구하고 완벽한 추상화란 없으며 특정 시점에는 여러 작업을 동시에 실행했을 때의 위험을 관리해야 합니다. 여기에는 교착 상태, 경합 조건 및 부하를 야기하는 비효율적인 스케줄링 등이 포함됩니다. 추상화를 사용하는 것과 별도로 그 작동 방식 및 원리도 이해하면 좋습니다.
일반적으로 사용자는 작업의 스케줄링을 처리하지만 작업이 언제 어떻게 끝날지는 알 수 없습니다. 실행은 .NET 런타임이 담당합니다. 문제가 발생할 징후를 보이는 특정 코드 패턴을 식별할 수도 있지만, 가장 좋은 방법은 런타임에 작업의 문제를 진단하는 것입니다. 그러면 코드 베이스에서 일반적으로 발견될 수 있는 몇몇 시나리오를 살펴보고 Tasks 뷰로 애플리케이션을 이해하는 방법을 알아보겠습니다.
공통 작업
작업을 다룰 때 가장 가능성이 높은 시나리오는 async
나 await
키워드가 필요한 작업을 반환하는 API를 사용하는 것입니다. 이러한 비동기 API는 ASP.NET Core, MAUI 및 Entity Framework Core에 있습니다. 여러 오픈 소스 프로젝트도 async를 우선하는 API로 전환하여 개발자의 요구 사항에 대응하고 있습니다.
간단한 예시를 살펴보겠습니다.
<span class="keyword">await</span> <span class="method-name">BasicWork</span>();
<span class="keyword">async</span> <span class="class-name">Task</span> <span class="method-name">BasicWork</span>()
{
<span class="keyword">await</span> <span class="class-name">Task</span><span class="operator">.</span><span class="method-name static-symbol">Delay</span>(<span class="struct-name">TimeSpan</span><span class="operator">.</span><span class="method-name static-symbol">FromSeconds</span>(<span class="number">1</span>));
<span class="static-symbol class-name">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"👋 Hello Tasks View!"</span>);
}
이 예시에는 두 개의 작업이 있습니다. JetBrains의 프로그램에서 가져온 메인 작업과 BasicWork
메서드가 있습니다. 이를 새로운 Tasks(작업) 뷰를 사용하여 확인할 수 있습니다. 세션을 디버그하는 동안 Task 탭을 눌러 다음의 테이블을 표시합니다.
또한, 우측 상단에서 Graph(그래프) 뷰를 선택하여 전환할 수 있습니다.
Graph 뷰에서 스택 항목에 마우스 커서를 올려 놓으면 행의 정보를 확인할 수 있습니다.
Tasks로 작업할 때 작업의 상태는 다음 다섯 가지 중 하나입니다.
- Active(활성화): 현재 실행 중인 상태입니다.
- Scheduled(예약됨): 작업이 생성되었으나 아직 실행되지 않은 상태입니다.
- Awaiting(대기 중): 작업이 대기 중인 상태이나 다른 작업을 기다리고 있는 중일 수 있습니다.
- Blocked(차단됨): 작업이 스택의 상단에 있으며 이 작업을 실행하는 스레드가 차단된 상태입니다(휴면, 잠금 대기 등). 스택상에서 더 높이 위치한 다른 작업이 있을 수도 있습니다.
- Deadlocked(교착): 작업이 하나의 리소스를 두고 경합하고 있으며, 중대한 문제가 있는 상태입니다.
가장 일반적인 시나리오를 살펴보았으니 이제 상위 작업을 살펴보겠습니다.
상위 작업
상위 작업을 생성하면 논리적으로 작업을 그룹화할 수 있습니다. 작업 내에 작업을 생성할 때는 TaskCreationOptions.AttachedToParent
를 사용해서 새로운 작업을 이를 포함하는 작업에 연결할 수 있습니다.
<span class="keyword">await</span> <span class="method-name">ParentedTasks</span>();
<span class="class-name">Task</span> <span class="method-name">ParentedTasks</span>()
{
<span class="comment">// Parent task</span>
<span class="keyword">var</span> <span class="local-name">parentTask</span> <span class="operator">=</span> <span class="class-name">Task</span><span class="operator">.</span><span class="property-name static-symbol">Factory</span><span class="operator">.</span><span class="method-name">StartNew</span>(() <span class="operator">=></span>
{
<span class="static-symbol class-name">Console</span><span class="operator">.</span><span class="static-symbol method-name">WriteLine</span>(<span class="string">"Parent task started."</span>);
<span class="comment">// Child task</span>
<span class="keyword">var</span> <span class="local-name">task</span> <span class="operator">=</span> <span class="class-name">Task</span><span class="operator">.</span><span class="property-name static-symbol">Factory</span><span class="operator">.</span><span class="method-name">StartNew</span>(() <span class="operator">=></span>
{
<span class="static-symbol class-name">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"Child task started."</span>);
<span class="class-name">Task</span><span class="operator">.</span><span class="static-symbol method-name">Delay</span>(<span class="number">2000</span>)<span class="operator">.</span><span class="method-name">Wait</span>(); <span class="comment">// Simulating some work</span>
<span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"Child task completed."</span>);
}, <span class="enum-name">TaskCreationOptions</span><span class="operator">.</span><span class="enum-member-name">AttachedToParent</span>);
<span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"Parent task doing some work."</span>);
}, <span class="enum-name">TaskCreationOptions</span><span class="operator">.</span><span class="enum-member-name">AttachedToParent</span>);
<span class="comment">// Wait for parent task to complete, which includes the children</span>
<span class="local-name">parentTask</span><span class="operator">.</span><span class="method-name">Wait</span>();
<span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"Parent task completed."</span>);
<span class="keyword-control">return</span> <span class="class-name">Task</span><span class="operator">.</span><span class="property-name static-symbol">CompletedTask</span>;
}
코드를 실행한 다음 Task(작업) 뷰를 보면 하위 작업이 상위에 성공적으로 추가된 것을 확인할 수 있습니다.
작업이 새로 생성될 때마다 .NET 런타임이 정수 Id
를 할당한다는 점에 유의하세요. 이러한 식별자로 현재 프로세스 내에 있는 작업을 추적할 수 있습니다.
이번에는 Graph(그래프) 뷰에 두 개의 비동기 논리 스택이 표시되어 있습니다. 이 스택은 Wait
및 반환하는 Task.CompletedTask
와 같은 메서드를 사용하는 ParentedTasks
코드의 결과로 발생합니다.
좋습니다. 작업이 서로 관련이 있거나 별도의 논리 스택을 생성했는지 파악하면 잠재적인 경합 조건이 생성되었는지 판단할 수 있습니다.
이제 Tasks 뷰로 작업이 어떻게 예약되었는지 확인하는 방법을 알아보겠습니다.
작업 스케줄링
언제든지 작업을 await
상태로 설정하면 작업이 추후에 실행되도록 예약됩니다. 이는 작업이 예약된 후 즉시 또는 다른 예약 작업이 실행된 후에 일어납니다. 몇몇 작업을 예약하고 해당 작업이 완료될 때까지 기다리는 예시를 살펴보겠습니다.
<span class="keyword">await</span> <span class="method-name">ScheduledWork</span>();
<span class="keyword">async</span> <span class="class-name">Task</span> <span class="method-name">ScheduledWork</span>()
{
<span class="static-symbol class-name">Console</span><span class="operator">.</span><span class="static-symbol method-name">Write</span>(<span class="string">"Let's work..."</span>);
<span class="keyword">var</span> <span class="local-name">tasks</span> <span class="operator">=</span> <span class="class-name static-symbol">Enumerable</span>
<span class="operator">.</span><span class="static-symbol method-name">Range</span>(<span class="number">1</span>, <span class="number">10</span>)
<span class="operator">.</span><span class="extension-method-name">Select</span>((<span class="parameter-name">i</span>) <span class="operator">=></span> <span class="class-name">Task</span><span class="operator">.</span><span class="method-name static-symbol">Run</span>(() <span class="operator">=></span> <span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="method-name static-symbol">Write</span>(<span class="parameter-name">i</span>)));
<span class="keyword">await</span> <span class="class-name">Task</span><span class="operator">.</span><span class="method-name static-symbol">WhenAll</span>(<span class="local-name">tasks</span>);
}
Task.WhenAll
은 제공된 모든 작업을 실행하려고 시도하며, 이 모든 작업을 추후에 실행하도록 예약합니다. 이는 Tasks(작업) 뷰에서 확인할 수 있습니다.
추가적으로 Task.WhenAll
을 사용하면 이러한 연산이 실행되는 구조인 비동기 논리 스택이 생성됩니다.
디버그 세션 중 코드를 단계별로 실행하면 작업이 완료되면서 작업 목록이 줄어드는 것을 볼 수 있습니다. 또한, 여러 작업이 동시에 실행되는 것도 확인할 수 있습니다.
작업이 완료되는 것은 만족스럽지만 너무 오래 지속된다면 좋지 않습니다. 그러면 작업을 다룰 때 가장 무서운 시나리오인 교착 상태로 넘어가겠습니다.
교착 상태
교착 상태를 일으키는 가장 일반적인 이유는 잠금 메커니즘으로 보호되는 공유 리소스를 두고 경합이 발생하기 때문입니다. 공유 리소스를 처리할 때 잠금은 필수적이지만 앱을 손상시키는 문제를 일으킬 수 있습니다.
스릴 있는 삶도 재미는 있으니 교착 상태를 한 번 일으켜 보겠습니다. 더 중요한 목적은 Tasks(작업) 뷰로 교착 상태를 어떻게 식별하는지 알아보는 것입니다. 같은 변수를 잠그려고 시도하는 두 개의 작업을 예약하겠습니다.
<span class="keyword">await</span> <span class="method-name">Deadlock</span>();
<span class="comment">// This method will cause a deadlock</span>
<span class="comment">// proceed with caution, oOOoOOoOo! 👻</span>
<span class="keyword">async</span> <span class="class-name">Task</span> <span class="method-name">Deadlock</span>()
{
<span class="keyword">object</span> <span class="local-name">one</span> <span class="operator">=</span> <span class="keyword">new</span>();
<span class="keyword">object</span> <span class="local-name">two</span> <span class="operator">=</span> <span class="keyword">new</span>();
<span class="keyword">var</span> <span class="local-name">timer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="namespace-name">System</span><span class="operator">.</span><span class="namespace-name">Timers</span><span class="operator">.</span><span class="class-name">Timer</span>(
<span class="struct-name">TimeSpan</span><span class="operator">.</span><span class="method-name static-symbol">FromSeconds</span>(<span class="number">2</span>)
) { <span class="property-name">Enabled</span> <span class="operator">=</span> <span class="keyword">true</span>, <span class="property-name">AutoReset</span> <span class="operator">=</span> <span class="keyword">false</span> };
<span class="local-name">timer</span><span class="operator">.</span><span class="event-name">Elapsed</span> <span class="operator">+=</span> (<span class="keyword">_</span>, <span class="keyword">_</span>) <span class="operator">=></span>
{
<span class="comment">// only see this if we're deadlocked</span>
<span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"💀Deadlock"</span>);
};
<span class="keyword">await</span> <span class="class-name">Task</span><span class="operator">.</span><span class="method-name static-symbol">WhenAll</span>(<span class="class-name">Task</span><span class="operator">.</span><span class="method-name static-symbol">Run</span>(() <span class="operator">=></span>
{
<span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="static-symbol method-name">WriteLine</span>(<span class="string">"Getting lock for one."</span>);
<span class="keyword">lock</span> (<span class="local-name">one</span>)
{
<span class="class-name">Thread</span><span class="operator">.</span><span class="static-symbol method-name">Sleep</span>(<span class="number">1000</span>);
<span class="static-symbol class-name">Console</span><span class="operator">.</span><span class="static-symbol method-name">WriteLine</span>(<span class="string">"Getting lock two in first task."</span>);
<span class="keyword">lock</span> (<span class="local-name">two</span>)
{
}
}
}), <span class="class-name">Task</span><span class="operator">.</span><span class="method-name static-symbol">Run</span>(() <span class="operator">=></span>
{
<span class="static-symbol class-name">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"Getting lock two in second task."</span>);
<span class="keyword">lock</span> (<span class="local-name">two</span>)
{
<span class="class-name">Thread</span><span class="operator">.</span><span class="method-name static-symbol">Sleep</span>(<span class="number">1000</span>);
<span class="class-name static-symbol">Console</span><span class="operator">.</span><span class="method-name static-symbol">WriteLine</span>(<span class="string">"Getting lock one in second task."</span>);
<span class="keyword">lock</span> (<span class="local-name">one</span>)
{
}
}
}));
}
코드를 실행하면 앱이 종료되지 않는다는 것을 확인할 수 있습니다. Run(실행) 툴바에서 일시 중지 버튼을 눌러 애플리케이션을 일시 중지하세요. 이런, 교착 상태네요! 충격적이에요! (물론 그렇게 놀라진 않았습니다)
두 개의 작업이 서로 경쟁하고 있고 어떻게 이 상황이 발생했는지 보여주는 Graph(그래프) 뷰는 더 흥미롭습니다.
교착 상태에 빠진 논리 스택을 두 번 클릭하면 교착 상태가 있는 위치로 이동합니다.
편리한 탐색 기능으로 교착 상태를 손쉽게 찾고 해결할 수 있습니다.
결론
작업 뷰는 현재 JetBrains Rider 2024.2 EAP에서 사용할 수 있습니다. 이 도구의 미래를 만들 수 있도록 피드백을 공유해 주세요. JetBrains는 .NET 개발에서 작업을 처리하기가 상당히 어려울 수 있다는 사실을 잘 알고 있습니다. 이 추가 도구가 어려움을 극복하는 데 도움이 되길 바랍니다. 직접 사용해 보고 기존의 코드를 최적화하거나 코드 베이스에 있던 오래된 문제를 찾는 데 도움이 되는지 확인해 보세요.
읽어주셔서 감사드리며 여러분의 의견과 댓글을 기대하겠습니다.
이미지 출처: Eden Constantino
게시물 원문 작성자