비동기 코드 작성(Task 클래스)

00·2025년 1월 11일

C#

목록 보기
143/149

Task 클래스

System.Threading.Tasks 네임스페이스의 Task 클래스는 C#에서 비동기 코드를 쉽게 작성할 수 있도록 도와준다.

동기 코드는, 검사가 검으로 공격할 때처럼 동작한다. 검사가 검으로 상대를 찌른 뒤에 다시 뽑아야 칼을 쓸 수 있는 것처럼, 동기코드는 메서드를 호출한 뒤에 이 메서드의 실행이 완전히 종료(반환)되어야만 다음 메서드를 호출할 수 있다.

비동기 코드는, 궁수가 활을 쏠 때처럼 동작한다. 궁수는 화살을 쏘고 나면 바로 다음 화살을 쏠 준비를 할 수 있다. 이미 쏜 화살에 대해서는 잊고서 말이다. 즉,
비동기 코드는 궁수가 화살을 쏘는 것처럼 메서드를 호출한 뒤에 메서드의 종료를 기다리지 않고 바로 다음 코드를 실행한다. 작업을 시작한 후 결과를 기다리지 않고 다른 작업을 계속 수행할 수 있는 방식인 것이다.
따라서 인터넷에서 큰 파일을 다운로드하는 동안 다른 작업을 할 수 있도록 하거나, 복잡한 계산을 백그라운드에서 수행하면서 사용자 인터페이스가 멈추지 않도록 할 수 있다.

Task 클래스는 C#에서 비동기 프로그래밍을 위한 핵심적인 클래스이며, 다양한 기능을 제공하여 비동기 작업을 효율적으로 관리할 수 있도록 해준다.

Task 클래스의 주요 기능

  • 비동기 작업 생성: Task 클래스의 생성자 또는 Task.Run() 메서드를 사용하여 비동기 작업을 생성할 수 있다.
    (Task 클래스는 인스턴스를 생성할 때 Action 대리자를 인수로 넘겨받는다. 즉 Task() 생성자의 인수로, 반환형을 갖지 않는 메서드, 익명 메서드, 무명 함수 등을 넘겨받는다.)

  • 작업 시작: Start() 메서드를 사용하여 비동기 작업을 시작할 수 있다.

  • 결과 대기: Wait() 메서드를 사용하여 비동기 작업이 완료될 때까지 기다릴 수 있다.

  • 결과 가져오기: Result 프로퍼티를 사용하여 비동기 작업의 결과를 가져올 수 있다.

  • 예외 처리: Exception 프로퍼티를 사용하여 비동기 작업에서 발생한 예외를 처리할 수 있다.

  • 취소: CancellationToken을 사용하여 비동기 작업을 취소할 수 있다.

  • 연속: ContinueWith() 메서드를 사용하여 비동기 작업이 완료된 후 실행할 작업을 지정할 수 있다.

Task 클래스 사용 예시

using System;
using System.Threading;
using System.Threading.Tasks;

class TaskTest
{
    static void Main(string[] args)
    {
        Task task = new Task(() =>
        {
            Thread.Sleep(1000); // 1초 동안 대기
            Console.WriteLine("비동기 작업 완료!");
        });

        task.Start(); // 작업 시작

        Console.WriteLine("메인 스레드 계속 실행...");

        task.Wait(); // 작업 완료 대기

        Console.WriteLine("모든 작업 완료!");
    }
}

Task 클래스의 장점

  • 간편한 비동기 프로그래밍: 스레드를 직접 관리하지 않고도 비동기 작업을 쉽게 구현할 수 있다.
  • 향상된 성능: 스레드 풀을 사용하여 스레드를 효율적으로 관리하고, 비동기 작업의 성능을 향상시킬 수 있다.
  • 응답성 향상: UI 스레드를 차단하지 않고 비동기 작업을 수행하여 애플리케이션의 응답성을 향상시킬 수 있다.

병렬 처리, 비동기 처리, 멀티 스레드
병렬 처리, 비동기 처리, 멀티 스레드는 모두 프로그램의 성능을 향상시키는 데 사용되는 기술이지만, 각각의 개념과 적용 방식은 다르다.

병렬 처리

  • 정의: 하나의 작업을 여러 작업자가 나눠서 수행한 뒤, 다시 하나의 결과로 만드는 것이다.
    여러 명의 요리사가 동시에 요리하는 것과 같다. 각 요리사는 독립적으로 자신의 요리를 만들고, 모든 요리가 완성되면 식사가 준비된다.
  • 사용 이유: 작업 속도를 높이기 위해 사용한다. 여러 개의 CPU 코어를 사용하여 작업을 동시에 처리하면, 전체 작업 시간을 단축할 수 있다.
  • 적합한 상황:
    • CPU 사용량이 많은 작업: 이미지 처리, 동영상 인코딩, 과학 계산 등
    • 독립적인 작업: 각 작업이 서로 영향을 미치지 않는 경우
  • 구현 방법:
    • Parallel 클래스: for, foreach 루프를 병렬로 실행할 수 있도록 지원한다.
    • Task 클래스: 작업을 비동기적으로 실행하고 결과를 가져올 수 있도록 지원한다.
    • PLINQ: LINQ 쿼리를 병렬로 실행할 수 있도록 지원한다.

비동기 처리

  • 정의: 작업 A를 시작한 후, A의 결과가 나올 떄까지 대기하기 않고 다른 작업 B,C,D...를 수행하다가 작업 A가 끝나면 그 때 결과를 받아내는 처리 방식이다. 즉, 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속 수행하는 것이다.
    요리사가 오븐에 음식을 넣고, 음식이 구워지는 동안 다른 요리를 준비하는 것과 같다. 오븐에 음식을 넣은 후, 음식이 다 구워질 때까지 기다리는 대신 다른 요리를 먼저 처리하는 것이다.
  • 사용 이유: 응답성을 높이기 위해 사용한다. 시간이 오래 걸리는 작업을 비동기적으로 처리하면, 프로그램이 멈추지 않고 다른 작업을 계속 수행할 수 있다.
  • 적합한 상황:
    • I/O 작업: 네트워크 통신, 파일 읽기/쓰기, 데이터베이스 쿼리 등
    • 사용자 인터페이스: 사용자 인터페이스가 멈추지 않고 반응하도록 하기 위해
  • 구현 방법:
    • async/await 키워드: 비동기 메서드를 정의하고 호출할 수 있도록 지원한다.
    • Task 클래스: 작업을 비동기적으로 실행하고 결과를 가져올 수 있도록 지원한다.

멀티 스레드

  • 정의: Thread 클래스는 여러 작업을 나누지 않고 각각 처리한다. 하나의 프로세스 내에서 여러 개의 스레드를 생성하여 작업을 동시에 처리하는 것이다.
    (System.Threading.Tasks 네임스페이스의 클래스들은 하나의 작업을 쪼갠 뒤 쪼개진 작업들을 동시에 처리하는 코드비동기 코드를 위해 설계되었다.)
    마치 여러 명의 요리사가 하나의 주방에서 동시에 일하는 것과 같다. 각 요리사는 다른 작업을 수행하지만, 같은 주방(프로세스)에서 협력하여 요리를 완성한다.
    Thread 클래스로도 하나의 작업을 쪼갠 뒤 쪼개진 작업들을 동시에 처리하는 코드비동기 코드를 작성할 수는 있지만(System.Threading.Tasks의 클래스들도 내부적으로는 Thread로 구현되었기 때문), 쉽지 않다.
  • 사용 이유:
    • 병렬 처리: 여러 CPU 코어를 사용하여 작업 속도를 높인다.
    • 비동기 처리: 응답성을 높이고, 사용자 인터페이스를 멈추지 않고 반응하도록 한다.
    • 실시간 처리: 실시간으로 데이터를 처리해야 하는 경우
  • 적합한 상황:
    • CPU 사용량이 많은 작업
    • I/O 작업
    • 실시간 처리
    • 사용자 인터페이스
  • 구현 방법:
    • Thread 클래스: 스레드를 생성하고 관리할 수 있도록 지원한다.
    • ThreadPool 클래스: 스레드 풀을 사용하여 스레드를 효율적으로 관리할 수 있도록 지원한다.

세 가지 기술의 관계

  • 병렬 처리는 멀티 스레드를 사용하여 구현할 수 있다.
  • 비동기 처리는 멀티 스레드를 사용하거나, 단일 스레드에서 비동기적으로 작업을 처리할 수 있다.
  • 멀티 스레드는 병렬 처리와 비동기 처리를 모두 구현하는 데 사용될 수 있다.

요약

병렬 처리, 비동기 처리, 멀티 스레드는 각각 다른 목적과 적용 방식을 가진 기술이다. 이러한 기술들을 적절히 활용하면 프로그램의 성능과 응답성을 향상시킬 수 있다.


예제

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace UsingTask
{
    class MainApp
    {
        static void Main(string[] args)
        {
            string srcFile = args[0]; // 첫 번째 명령줄 인수를 srcFile에 저장한다.

            // 파일 복사 작업을 수행하는 Action<object> 델리게이트를 생성한다.
            Action<object> FileCopyAction = (object state) =>
            {
                String[] paths = (String[])state; // state 객체를 string 배열로 변환한다.
                File.Copy(paths[0], paths[1]); // paths[0]에서 paths[1]로 파일을 복사한다.

                // Task ID, Thread ID, 소스 파일 경로, 대상 파일 경로를 출력한다.
                Console.WriteLine("TaskID:{0}, ThreadID:{1}, {2} was copied to {3}",
                    Task.CurrentId, Thread.CurrentThread.ManagedThreadId,
                    paths[0], paths[1]);
            };

            // FileCopyAction 델리게이트를 사용하여 Task 객체를 생성한다.
            Task t1 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy1" });

            // Task.Run() 메서드를 사용하여 Task 객체를 생성한다.
            Task t2 = Task.Run(() =>
            {
                FileCopyAction(new string[] { srcFile, srcFile + ".copy2" });
            });

            t1.Start(); // t1 Task를 시작한다.

            // FileCopyAction 델리게이트를 사용하여 Task 객체를 생성한다.
            Task t3 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy3" });

            t3.RunSynchronously(); // t3 Task를 동기적으로 실행한다.

            t1.Wait(); // t1 Task가 완료될 때까지 기다린다.
            t2.Wait(); // t2 Task가 완료될 때까지 기다린다.
            t3.Wait(); // t3 Task가 완료될 때까지 기다린다.

            // List<int>를 반환하는 Task<List<int>> 객체를 생성한다.
            var myTask = Task<List<int>>.Run(
                () =>
                {
                    Thread.Sleep(1000); // 1초 동안 스레드를 일시 중지한다.

                    List<int> list = new List<int>(); // List<int> 객체를 생성한다.
                    list.Add(3); // list에 3, 4, 5를 추가한다.
                    list.Add(4);
                    list.Add(5);

                    return list; // list를 반환한다.
                }
            );

            myTask.Wait(); // myTask가 완료될 때까지 기다린다.
        }
    }
}

코드 설명

이 C# 코드는 Task 클래스를 사용하여 비동기 작업을 수행하는 방법을 보여주는 예제이다.
세 개의 Task를 이용해서 세 개의 파일을 복사하는 프로그램 코드로, 첫 번째와 두 번째 Task는 비동기로 파일을 복사하고, 세 번쨰 Task는 동기로 파일을 복사한다.
FileCopyAction 델리게이트는 파일 복사 작업을 수행하고, Task 객체는 FileCopyAction 델리게이트를 비동기적으로 실행한다.

  • string srcFile = args[0];: 첫 번째 명령줄 인수를 srcFile에 저장한다.
  • Action<object> FileCopyAction = (object state) => { ... };: 파일 복사 작업을 수행하는 Action<object> 델리게이트를 생성한다.
  • Task t1 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy1" });: FileCopyAction 델리게이트를 사용하여 Task 객체를 생성한다.
  • Task t2 = Task.Run(() => { ... });: Task.Run() 메서드를 사용하여 Task 객체를 생성한다.
  • t1.Start();: t1 Task를 시작한다.
  • Task t3 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy3" });: FileCopyAction 델리게이트를 사용하여 Task 객체를 생성한다.
  • t3.RunSynchronously();: t3 Task를 동기적으로 실행한다.
  • t1.Wait();, t2.Wait();, t3.Wait();: 각 Task가 완료될 때까지 기다린다.
  • var myTask = Task<List<int>>.Run(() => { ... });: List<int>를 반환하는 Task<List<int>> 객체를 생성한다.
  • myTask.Wait();: myTask가 완료될 때까지 기다린다.

출력 결과

TaskID:1, ThreadID:3, test.txt was copied to test.txt.copy1
TaskID:2, ThreadID:4, test.txt was copied to test.txt.copy2
TaskID:3, ThreadID:1, test.txt was copied to test.txt.copy3

참고

  • Task 클래스는 비동기 작업을 나타낸다.
  • Task.Run() 메서드는 Task를 비동기적으로 실행한다.
  • RunSynchronously() 메서드는 Task를 동기적으로 실행한다.
  • Wait() 메서드는 Task가 완료될 때까지 기다린다.
  • Task<TResult> 클래스는 결과 값을 반환하는 Task를 나타낸다.

0개의 댓글