본문 바로가기
ZAION/C++

[이것이 C#이다]Ch.19 스레드와 태스크 - 2

by 우기37 2024. 2. 2.

## 목차

2) Task와 Task<TResult> 그리고 Parallel

 1. 

 2. 

 3. 

3) async 한정자와 await 연산자로 만드는 비동기 코드

 

 

 

#2 Task와 Task<TResult> 그리고 Parallel

C#이 처음 발표된 2000년대 초, CPU 스피드가 수 기가헤르츠에 이르면서, CPU의 성능 향상에 대한 물리적 한계에 부딪히기 시작했습니다. 그래서 코어를 증가시키는쪽으로 CPU 개발 방향을 선회했습니다. 싱글코어 시절에는 클럭 스피드가 높은 버전으로 CPU를 업그레이드하면 소프트웨어의 성능도 자연스레 좋아졌지만, 멀티코어 CPU에서는 소프트웨어의 '성능 무임 승차'서비스를 기대할 수 없게 되었습니다.

 

고성능 소프트웨어를 만들기 위해서는 병렬 처리 기법과 비동기 처리 기법이 필수가 되었습니다. 그러나 프로그래머 입장에서는 일일이 구현하려면 숨이 턱까지 차오를 판이었습니다.

 

마이크로소프트는 이 변화에 편안하게 적응하도록 더 쉽게 비동기 코드를 작성할 수 있도록 하는 도구와 장치를 준비했습니다.

 

System.Threading.Tasks 네임스페이스의 클래스들과 다음 절에서 다룰 async 한정자와 await 연산자가 바로 그것입니다.

병행성 코드나 비동기 코드를 손쉽게 작성할 수 있도록 돕는 여러 클래스 중에 Task와 Task<Result>, 그리고 Parallel 클래스를 정리해보겠습니다.

 

잠깐! 멀티 스레드가 있지 않나요?

System.Threading.Tasks 네임스페이스의 클래스들은 하나의 작업을 쪼갠 뒤 쪼개진 작업들을 동시에 처리하는 코드와 비동기 코드를 위해 설계됐습니다.

반면에, Thread 클래스는 여러 작업을 (나누지 않고) 각각 처리해야 하는 코드에 적합합니다.

 

*병렬처리 : 하나의 작업을여러 작업자가 나눠서 수행한 뒤 다시 하나의 결과로 만드는 것

*비동기 처리 : 작업 A를 시작한 후 A의 결과가 나올 때까지 마냥 대기하는 대신 곧이어 다른 작업 B,C,D...를 수행하다가 작업 A가 끝나면 그 때 결과를 받아내는 처리 방식을 말합니다.

 

 

1. System.Threading.Tasks.Task 클래스

이 클래스는 우리가 비동기 코드를 손쉽게 작성할 수 있도록 도와줍니다.

 

비동기 코드 : 비동기 코드는 궁수가 활을 쏠 때처럼 동작합니다.(Shoot(Fire) And Forget) 메소드를 호출한 뒤에 메소드의 종료를 기다리지 않고 바로 다음 코드를 실행합니다.

 

동기 코드 : 동기 코드는 검술사가 검으로 공격할 때처럼 동작합니다. 메소드를 호출한 뒤에 이 메소드의 실행이 완전히 종료되어야만(즉, 반환되어야만) 다음 메소드를 호출할 수 있습니다.

 

Task 클래스는 인스턴스를 생성할 때 Action 대리자를 넘겨받습니다. 다시 말해 반환형을 갖지 않는 메소드와 익명 메소드, 무명 함수 등을 넘겨받습니다.

 

다음 실습예제에는 세 대의 Task를 이용해서 세 개의 파일을 복사합니다. 앞의 두 Task는 비동기로 파일을 복사하고, 세 번째 Task는 동기로 파일을 복사합니다.

 

[실습]

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

namespace ThisIsCSharpExam.Ch._19.TaskParallelExam
{
    class UsingTask
    {
        public void Main(string[] args)
        {
            string srcFile = args[0];

	// Action 대리자 기반의 무명 함수 선언 방법
            //Action<object> FileCopyAction = (object state) =>   // 인수를 받음
            //{
            //    string[] paths = (string[])state;
            //    File.Copy(paths[0], paths[1]);

            //    Console.WriteLine("TaskID:[{0}], ThreadID:[{1}], [{2}] was copied to [{3}]",
            //        Task.CurrentId, Thread.CurrentThread.ManagedThreadId, paths[0], paths[1]);
            //};

            // t1 -> 생성자에서 넘겨받은 무명 함수를 비동기로 호출합니다.
            Task t1 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy1" });

            /*
            t2 -> Task의 생성과 기작은 단번에 합니다.
            덤으로 Task가 실행할 Action 대리자도 무명 함수로 바꿔보았습니다.
            */
            Task t2 = Task.Run(() =>
            {
                FileCopyAction(new string[] { srcFile, srcFile + ".copy2" });
            });

            t1.Start();

            // 두 번째 인수는 FileCopyAction의 인수로 상요됩니다.
            Task t3 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy3" });

            /*
            Task는 코드의 비동기 실행을 위한 Start() 메소드뿐 아니라 동기 실행을 위한
            RunSynchronously() 메소드도 제공합니다.
            이 메소드는 실행이 끝나야 반환지만, 나쁜 습관을 방지하기 위해 Wait()는 호출해주는 것이 좋습니다.
            */
            t3.RunSynchronously();

            // t1, t2, t3 비동기 호출이 완료될 때까지 기다립니다.
            t1.Wait();
            t2.Wait();
            t3.Wait();

            Console.ReadLine();
        }

        // FileCopyAction 메소드를 따로 분리한 방법
        private void FileCopyAction(object state)
        {
            string[] paths = (string[])state;
            File.Copy(paths[0], paths[1]);

            Console.WriteLine("TaskID:[{0}], ThreadID:[{1}], [{2}] was copied to [{3}]",
                Task.CurrentId, Thread.CurrentThread.ManagedThreadId, paths[0], paths[1]);

        }
    }
}

 

 

*처음에 인자가 없어서 인덱스 범위를 초과했다는 오류가 계속해서 발생했는데, 프로젝트의 속성에서 아래와 같이 경로를 입력해주니 정상적으로 동작하였습니다.

 

 

 

 

2. 코드의 비동기 실행 결과를 주는 Task<TResult> 클래스

코드의 비동기 실행 결과를 손쉽게 얻게 해줍니다.

예를 들어 15개의 비동기 작업을 실행한 후 그 결과를 취합해야 한다고 한다면 일이 굉장히 복잡해집니다. Tast<TResult>는 코드의 비동기 실행 결과를 손쉽게 취합할 수 있도록 도와줍니다.

 

Task 클래스의 사용법은 Action 대리자로 받는 대신 Finc 대리자로 받는다는점과 결과를 반환받을 수 있다는점 입니다.

 

다음은 프로그램 인수로 입력받은 두 수 사이에 존재하는 모든 소수의 목록 반환합니다. 소수 찾기는 문제 특성상 각 수를 일일이 검사해야 합니다. 만약 하나의 Task만으로 n개의 수에 대해 소수 찾기를 한다면 '소수 판정 시간 x n'만큼의 시간이 소요될 것입니다. 하지만 m개의 CPU가 있는 시스템에서 m개의 Task가 n개의 수를 m으로 나눈 범위만큼 각각 소수 찾기를 한다면 작업 시간이 훨씬 줄어들것입니다.

 

[실습]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThisIsCSharpExam.Ch._19.TaskParallelExam
{
    class TaskResult
    {
        public void Main(string[] args)
        {
            long from = Convert.ToInt64(args[0]);
            long to = Convert.ToInt64(args[1]);
            int taskCount = Convert.ToInt32(args[2]);

            Func<object, List<long>> FindPrimeFunc =
                (objRange) =>
                {
                    long[] range = (long[])objRange;
                    List<long> found = new List<long>();

                    for (long i = range[0]; i < range[1]; i++)
                    {
                        if (IsPrime(i))
                            found.Add(i);
                    }
                    return found;
                };
            Task<List<long>>[] tasks = new Task<List<long>>[taskCount];
            long currentFrom = from;
            long currentTo = to / tasks.Length;
            for (int i = 0; i < tasks.Length; i++)
            {
                Console.WriteLine("Task[{0}] : {1} ~ {2}", i, currentFrom, currentTo);
                tasks[i] = new Task<List<long>>(FindPrimeFunc, new long[] { currentFrom, currentTo });
                currentFrom = currentTo + 1;

                if (i == tasks.Length - 2)
                    currentTo = to;
                else
                    currentTo = currentTo + (to / tasks.Length);
            }
            Console.WriteLine("Please press enter to start...");
            Console.ReadLine();
            Console.WriteLine("Started...");

            DateTime startTime = DateTime.Now;

            foreach (Task<List<long>> task in tasks)
                task.Start();

            List<long> total = new List<long>();

            foreach(Task<List<long>> task in tasks)
            {
                task.Wait();
                total.AddRange(task.Result.ToArray());
            }
            DateTime endTime = DateTime.Now;

            TimeSpan elapsed = endTime - startTime;

            Console.WriteLine("Prime number count between {0} ans {1} : {2}",
                from, to, total.Count);
            Console.WriteLine("Elapsed time : {0}", elapsed);
        }
        static bool IsPrime(long number)
        {
            if (number < 2)
                return false;
            if (number % 2 == 0 && number != 2)
                return false;
            for (long i = 2; i < number; i++)
            {
                if (number % i == 0)
                    return false;
            }
            return true;
        }
    }
}

 

 

 

 

3. 손쉬운 병렬 처리를 가능케 하는 Parallel 클래스

System.Threading.Tasks.Parallel 클래스는 For(), Foreach() 등의 메소드를 제공함으로써 우리가 이전 절에서 Task<TResult>를이용해 직접 구현했던 병렬 처리를 더 쉽게 구현할 수 있게 해줍니다.

 

[실습] 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThisIsCSharpExam.Ch._19.TaskParallelExam
{
    class ParallelLoop
    {
        public void Main(string[] args)
        {
            long from = Convert.ToInt64(args[0]);
            long to = Convert.ToInt64(args[1]);

            Console.WriteLine("Please press enter to start...");
            Console.ReadLine();
            Console.WriteLine("Started...");

            DateTime startTime = DateTime.Now;
            List<long> total = new List<long>();

            Parallel.For(from, to, (long i) =>
            {
                if (IsPrime(i))
                    lock (total)
                        total.Add(i);
            });
            DateTime endTime = DateTime.Now;

            TimeSpan elapsed = endTime - startTime;

            Console.WriteLine("Prime number count between {0} and {1} : {2}",
                from, to, total.Count);
            Console.WriteLine("Elapsed time : {0}", elapsed);
        }
        static bool IsPrime(long number)
        {
            if (number < 2)
                return false;

            if (number % 2 == 0 && number != 2)
                return false;

            for (long i = 2; i < number; i++)
            {
                if (number % i == 0)
                    return false;
            }
            return true;
        }
    }
}

 

 

 

 

#3 async 한정자와 await 연산자로 만드는 비동기 코드

async 한정자는 메소드, 이벤트 처리기, 태스크, 람다식 등을 수삭함으로써 C# 컴파일러가 이들을 호출하는 코드를 만날 때 호출 결과를 기다리지 않고 바로 다음 코드로 이동하도록 실행 코드를 생성하게 합니다. 또한, 한정자이므로 메소드 또는 이벤트 처리기를 선언할 때 다음과 같이 다른 한정자들과 함께 사용하면 됩니다.

 

다만, async로 한정하는 메소드는 반환 형식이 Task나 Task<TResult> 또는 void 여야 한다는 제약이 있습니다. 실행하고 잊어버릴 작업을 담고 있는 비동기 방식 메소드라면 반환 형식을 void로 선언하고, 작업이 완료될 때까지 기다리는 동기 방식 메소드라면 Task, Task<TResult>로 선언하면 됩니다.

 

"async로 한정한 Task 또는 Task<TResult>를 반환하는 메소드/태스크/람다식은 await 연산자를 만나는 곳에서 호출자에게 제어를 돌려주며, await 연산자가 없는 경우 동기로 실행됩니다."

 

이미지 출처 :&nbsp;https://velog.io/@gkqls813/C-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%99%80-%ED%83%9C%EC%8A%A4%ED%81%AC

 

[실습]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace ThisIsCSharpExam.Ch._19.TaskParallelExam
{
    class AsyncExam
    {
        public void Main(string[] args)
        {
            Caller();
            Console.ReadLine();
        }
        async static private void MyMethodAsync(int count)
        {
            Console.WriteLine("C");
            Console.WriteLine("D");

            await Task.Run(async () =>
            {
                for (int i = 1; i <= count; i++)
                {
                    Console.WriteLine($"{i}/{count} ...");
                    await Task.Delay(100);
                }
            });
            Console.WriteLine("G");
            Console.WriteLine("H");
        }
        static void Caller()
        {
            Console.WriteLine("A");
            Console.WriteLine("B");

            MyMethodAsync(3);

            Console.WriteLine("E");
            Console.WriteLine("F");
        }
    }
}

 

 

 

1. .NET이 제공하는 비동기 API 맛보기

.NET 클래스 라이브러리 곳곳에 추가된 ~Async()라는 이름의 메소드들이 .NET이 제공하는 비동기 API 입니다. 그중에서 System.IO.Stream 클래스가 제공하는 비동기 메소드 두 가지는 살펴보겠습니다.

 

동기 버전 메소드 비동기 버전 메소드 설명
Read ReadAsync 스트림에서 데이터를 읽습니다
Write WriteAsync 스트림에 데이터를 기록합니다.

 

[실습]

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

namespace ThisIsCSharpExam.Ch._19.TaskParallelExam
{
    class AsyncFileIO
    {
        public void Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.WriteLine("Usage : AsyncFileIO <Source> <Destination>");
                return;
            }
            DoCopy(args[0], args[1]);

            Console.ReadLine();
        }
        // async로 한정한 코드를 호출하는 코드도 역시 async로 한정되어 있어야 합니다.
        // 반환 형식은 Task 또는 void 형식이어야 합니다.
        static async Task<long> CopyAsync(string FromPath, string ToPath)
        {
            using (var fromStream = new FileStream(FromPath, FileMode.Open))
            {
                long totalCopied = 0;

                using (var toStream = new FileStream(ToPath, FileMode.Create))
                {
                    byte[] buffer = new byte[1024];
                    int nRead = 0;
                    // ReadAsync()와 WriteAsync() 메소드는 async로 한정되어 있습니다.
                    // 이들을 호출하려면 await 연산자가 필요합니다.
                    while ((nRead = await fromStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                    {
                        await toStream.ReadAsync(buffer, 0, nRead);
                        totalCopied += nRead;
                    }
                }
                return totalCopied;
            }
        }
        static async void DoCopy(string FromPath, string ToPath)
        {
            long totalCopied = await CopyAsync(FromPath, ToPath);
            Console.WriteLine($"Copied Total {totalCopied} Byted.");
        }
    }
}

 

(*현재 결과 값이 오류가 발생해서 추후 오류 해결 후 다시 정리 하겠습니다.)