[ 공 부 ]/[ C# ]

19장. Task

HiStar__ 2025. 1. 30. 16:11

[2025-01-30] - 게시글 최초 작성


1. Task / Task<TResult> / Parallel 란?

병렬 처리 : 하나의 작업을 여러 작업자가 나눠서 수행한 뒤 다시 하나의 결과로 만드는 것
비동기 처리 : 작업 A를 시작한 후 A 결과가 나올 때까지 B, C, D ... 를 수행하다가 작업 A가 끝나면 그 결과를 받아냄.

 

 .NET 에서 지원하는 System.Threading.Tasks 네임스페이스의 클래스들,
 async 한정자 await 연산자

Task 등 클래스들도 내부적으로는 Thread를 이용하여 구현.


2. T a s k

2.1 System.Threading.Tasks.Task 클래스

 동기 코드 : 메소드를 호출한 뒤에 이 메소드의 실행이 완전히 종료되어야만 다음 메소드를 호출 가능
 비동기 코드 : 메소드를 호출한 뒤에 메소드의 종료를 기다리지 않고 다음 코드를 실행.

 

1. Action 대리자로 사용

2. Task.Run() 생성과 시작을 단번에 한다. ( 무명 함수 사용 )

 

[msdn] System.Threading.Tasks.Task

 

Task 클래스 (System.Threading.Tasks)

비동기 작업을 나타냅니다.

learn.microsoft.com

 

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

namespace ThisIsCSharp
{
    class MainApp
    {
        static 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($"Task:{Task.CurrentId}, ThreadID : {Thread.CurrentThread.ManagedThreadId}," +
                    $" {paths[0]} was copied to {paths[1]}");
            };

            Task t1 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy1" });

            Task t2 = Task.Run(() =>
            {
                FileCopyAction(new string[] { srcFile, srcFile + ".copy2" });
            });

            t1.Start();

            Task t3 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy3" });

            // 동기 실행을 위한 메소드
            t3.RunSynchronously();

            t1.Wait();
            t2.Wait();
            t3.Wait();
        }
    }
}


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

 1. 코드의 비동기 실행 결과를 얻을 수 있음

 2. Func 대리자로 사용

 3. Task 클래스를 사용 후 Wait() 를 호출하는 습관을 들이면 좋다!

 

[msdn] Task<TResult>

 

Task<TResult> 클래스 (System.Threading.Tasks)

값을 반환할 수 있는 비동기 작업을 나타냅니다.

learn.microsoft.com

 

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

namespace ThisIsCSharp
{
    class MainApp
    {
        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;
        }


        static void Main(string[] args)
        {
            long from = Convert.ToInt64("0");
            long to = Convert.ToInt64("100000");
            int taskCount = Convert.ToInt32("5");

            // Func 대리자 인스턴스 생성
            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[{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;

            // Task 시작
            foreach(Task<List<long>> task in tasks)
            {
                task.Start();
            }

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

            // 결과를 Task의 Result로 받아 올 수 있다.
            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 {from} and {to} : {total.Count}");
            Console.WriteLine($"Elapsed Time : {elapsed}");

        }
    }
}


2.3 Parallel 클래스

 1. 손쉬운 병렬 처리를 지원. ( System.Threading.Tasks.Parallel 클래스 )
 2. For(), Foreach() 등의 메소드를 제공 함으로써 Task<TResult> 보다 더 쉬운 구현을 제공한다.
 3. Parallel.For() 메소드 : 등록한 메소드를 병렬로 호출 할 때,  몇 개의 스레드를 사용할지는 Parallel 클래스가 
                                        내부적으로 판단하여 최적화.

 

[msdn] System.Threading.Tasks.Parallel

 

Parallel 클래스 (System.Threading.Tasks)

병렬 루프 및 영역에 대한 지원을 제공합니다.

learn.microsoft.com

 

// Parallel 클래스 사용!
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ThisIsCSharp
{
    class MainApp
    {
        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;
        }


        static void Main(string[] args)
        {
            long from = Convert.ToInt64("0");
            long to = Convert.ToInt64("100000");
            
            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 {from} and {to} : {total.Count}");
            Console.WriteLine($"Elapsed Time : {elapsed}");

        }
    }
}

2.4 async 한정자 / await 연산자

 C# ~ 5.0 : BeginInoke() / EndInvoke()

 C# 5.0 ~ : async 한정자 / await 연산자2025-01-30] - 게시글 최초 작성

async 한정자
: 메소드, 이벤트 처리기, 태스크, 람다식 등을 수식함으로써 C# 컴파일러가 호출하는 코드를 만날 때 
호출 결과를 기다리지 않고 바로 다음 코드로 이동하도록 실행 코드 생성.
* async로 한정하는 메소드의 경우, 반환 형식이 Task, Task<TResult>, void 형식이여야 한다.

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

await 연산자

해당 함수가 끝날때까지 기다리면서 제어권은 호출자에게 넘겨준다.

 

Task.Delay() 
: 함수가 하는 일은 인수로 입력받은 시간이 지나면 Task 객체를 반환
 스레드를 블록시키지 않는다.

Thread.Sleep()
: 스레드 전체를 블록 시킨다.
using System;
using System.Threading.Tasks;

namespace ThisIsCSharp
{
    class MainApp
    {
        // async 한정자로 함수 선언
        async static private void MyMethodAsync(int count)
        {
            Console.WriteLine("C");
            Console.WriteLine("D");

            // 1. await
            await Task.Run(async () =>
            {
                for (int i = 1; i <= count; i++)
                {
                    Console.WriteLine($"{i}/{count} ...");
                    await Task.Delay(100);      // Thread.Sleep()의 비동기 버전
                }
            });

            Console.WriteLine("G");
            Console.WriteLine("H");
        }

        static void Caller()
        {
            Console.WriteLine("A");
            Console.WriteLine("B");

            MyMethodAsync(3);

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


        static void Main(string[] args)
        {
            Caller();
            Console.ReadLine();
        }
    }
}

2.5 .NET이 제공하는 비동기 API

System.IO.Stream 클래스가 제공하는 ~ Async() 메소드

System.IO.Stream
Read  <-> ReadAsync 
Write <-> WriteAsync

 

 ~ Async 함수의 경우 await 연산자 처리를 해야 한다.

 

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

namespace ThisIsCSharp
{
    class MainApp
    {
       // 파일 복사 후 복사한 파일 용량 반환
       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;
                    while((nRead = await fromStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                    {
                        await toStream.WriteAsync(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} Bytes.");
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Usage : AsyncFileIO <Source> <Destination>");
            DoCopy("a.dat", "b.dat");
            Console.ReadLine();
        }
    }
}


4. 추가 정보

I/O 바운드 
: 컴퓨터가 어떤 작업을 할 때 CPU 보다는 입출력에 더 많은 시간을 사용하는 상황


5. 참고자료