cpu 집약적(cpu intensive)과 쓰레드(thread)

cpu 집약적(cpu intensive)과 쓰레드(thread)

이철우

하나의 작업이 시작해서 마무리 될 때까지 수 초(second) 또는 더 오래 걸린다면, 대개는 이 작업을 비동기로 수행한다. 이렇게 시간을 소비하는 작업(time-consuming operation)도 cpu 집약적인 것과 아닌 것으로 나눈다. [참고 1]의 예제 두 개에서 앞 예제 - 기다려 서비스 - 는 cpu 집약적 아닌 것, 뒤 예제 - 再歸 피보나치 수열 - 은 cpu 집약적인 것이다.

[참고 2]의 問 - cpu 집약적과 입출력 집약적의 다른 점은 무엇인가? - 에 대한 答에 따르면, cpu 집약적을

cpu intensive is code that uses a lot of cpu cycles.

cpu 집약적이란 cpu 주기를 많이 사용하는 코드입니다.

라고 설명한다. 조금 다르게 본다면, ‘쓰레드에 들러붙는(thread-sticked)’이라고 볼 수 있지 않을까?

이 글에서는 [참고 1]의 앞 예제를 조금 바꾸어 ’기다려 서비스(DelayService)’를 DotNet 7 Console 프로젝트로 만들고, 약간의 변수 조정으로 이 서비스가 조금 더 cpu 집약적인 것으로 바꾸어 보겠다. 그리고 ‘再歸 피보나치 서비스(FibonacciService)’를 만들고 기다려 서비스와 비교해 보겠다.

먼저 ‘기다려 서비스’를 추상화 하고 구현하자.

// IService.cs

    public interface IService
    {
        Task Request();
        void Cancel();

        bool IsRunning { get; }

        event EventHandler<RequestCompletedArgs>? RequestCompleted;
        event EventHandler<RequestCanceledArgs>? RequestCanceled;

        public class RequestCompletedArgs : EventArgs
        {
            public ulong Result { get; init; }
            public RequestCompletedArgs(ulong result)
            {
                Result = result;
            }

            public override string ToString() => $"{Completed}{':'}{Result}";
        }

        public class RequestCanceledArgs : EventArgs
        {
            public string Message { get; init; }
            public RequestCanceledArgs(string remains)
            {
                Message = remains;
            }

            public override string ToString() => $"{Canceled}{':'}{Message}";
        }

        private static readonly string Completed = "Completed";
        private static readonly string Canceled = "Canceled";
    }
// ServiceAbstract.cs

    public abstract class ServiceAbstract : IService
    {
        public bool IsRunning { get; protected set; }
        protected bool CancelRequired { get; set; }

        protected ServiceAbstract()
		{
            IsRunning = false;
            CancelRequired = false;
		}

        public event EventHandler<IService.RequestCompletedArgs>? RequestCompleted;
        public event EventHandler<IService.RequestCanceledArgs>? RequestCanceled;

        public virtual async Task Request()
        {
            if (IsRunning)
            {
                await Task.Run(() => Console.WriteLine(AlreadyRun));
                return;
            }
        }

        public virtual void Cancel()
        {
            if (IsRunning)
            {
                CancelRequired = true;
            }
            else
            {
                Console.WriteLine(NotRunning);
            }
        }

        protected virtual void OnRequestCompleted(IService.RequestCompletedArgs args)
        {
            RequestCompleted?.Invoke(this, args);
        }

        protected virtual void OnRequestCanceled(IService.RequestCanceledArgs args)
        {
            RequestCanceled?.Invoke(this, args);
        }

        private static readonly string AlreadyRun = "Already run.";
        private static readonly string NotRunning = "Not running.";
    }
// DelayService.cs

    public class DelayService : ServiceAbstract
    {
        private TimeSpan DelayTime { get; init; }
        private TimeSpan Tolerance { get; init; }

        public DelayService(TimeSpan delayTime, TimeSpan tolerance)
            : base()
        {
            DelayTime = delayTime;
            Tolerance = tolerance;
        }

        public override async Task Request()
        {
            await base.Request().ConfigureAwait(false);

            IsRunning = true;
            CancelRequired = false;
            await Delay().ConfigureAwait(false);
        }

        private async Task Delay()
        {
            await Task.Run(async () =>
            {
                var tolerance = (ulong)Tolerance.TotalMilliseconds;
                var delaySlice = TimeSpan.FromMilliseconds((tolerance / 2) > 1 ? (tolerance / 2) : 1);
                var total = (ulong)DelayTime.TotalMilliseconds;
                var start = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds();
                ulong current = 0;
                var isTimeout = false;
                while (true)
                {
                    current = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds() - start + tolerance;
                    if (total < current)
                    {
                        isTimeout = true;
                        break;
                    }
                    if (CancelRequired)
                    {
                        break;
                    }

                    await Task.Delay(delaySlice).ConfigureAwait(false);
                }

                if (isTimeout)
                {
                    OnRequestCompleted(new IService.RequestCompletedArgs(current - tolerance));
                }
                else
                {
                    OnRequestCanceled(new IService.RequestCanceledArgs($"{Remains} {total - current}"));
                }

                IsRunning = false;
            }).ConfigureAwait(false);
        }

        protected override void OnRequestCompleted(IService.RequestCompletedArgs args)
        {
            base.OnRequestCompleted(args);
        }

        protected override void OnRequestCanceled(IService.RequestCanceledArgs args)
        {
            base.OnRequestCanceled(args);
        }

        private readonly static string Remains = "Remains:";
    }

이제 이를 실행할 코드를 만들자.

// Program.cs

Console.WriteLine("Hello, World!");

IService[]? services = null;
var exitRequired = false;
while (!exitRequired)
{
    var input = Console.ReadLine();
    var commands = input!.Split(' ');

    var command = commands[0];
    var argument1 = (commands.Length > 1) ? int.Parse(commands[1]) : 0;
    var argument2 = (commands.Length > 2) ? int.Parse(commands[2]) : 0;
    var argument3 = (commands.Length > 3) ? int.Parse(commands[3]) : 0;

    switch (command)
    {
        case "exit":
            exitRequired = true;
            break;
        case "delay":
            if (commands.Length != 4)
            {
                Console.WriteLine("Usage: delay serviceCount delayTime(msec) tolerance(msec)");
            }
            var delayServiceCount = argument1;
            var delayTime = TimeSpan.FromMilliseconds(argument2);
            var tolerance = TimeSpan.FromMilliseconds(argument3);

            services = new DelayService[delayServiceCount];
            var delayTasks = new Task[delayServiceCount];

            for (int i = 0; i < delayServiceCount; i++)
            {
                services[i] = new DelayService(delayTime, tolerance);
                services[i].RequestCompleted += IService_RequestCompleted;
                services[i].RequestCanceled += IService_RequestCanceled;
                delayTasks[i] = services[i].Request();
            }

            //var start = DateTimeOffset.Now.ToUnixTimeMilliseconds();
            _ = Task.WhenAll(delayTasks).ConfigureAwait(false);
            //var final = DateTimeOffset.Now.ToUnixTimeMilliseconds();

            //Console.WriteLine($"Elapsed: {final - start}msec");
            break;
        case "cancel":
            if (services is not null)
            {
                for (int i = 0; i < services.Length; i++)
                {
                    services[i].Cancel();
                }
            }
            break;
         default:
            if (!string.IsNullOrEmpty(input))
            {
                Console.WriteLine(input);
            }
            break;
    }
}

Console.WriteLine("Bye.");
await Task.Delay(500).ConfigureAwait(false);
return;

void IService_RequestCanceled(object? sender, IService.RequestCanceledArgs e)
{
    OnServiceEvent(sender, e);
}

void IService_RequestCompleted(object? sender, IService.RequestCompletedArgs e)
{
    OnServiceEvent(sender, e);
}

void OnServiceEvent(object? sender, EventArgs e)
{
    Console.WriteLine(e);

    if (sender is IService service)
    {
        service.RequestCompleted -= IService_RequestCompleted;
        service.RequestCanceled -= IService_RequestCanceled;
    }
}

프로그램을 실행한 뒤에,

delay 10 45000 1

를 입력하고 ‘엔터’ 단추를 누른다. 위 예에서 delay 다음 첫 매개변수는 ‘기다려 서비스’의 수, 두 번째는 기다리는 시간(45000 msec), 세 번째는 기다리는 시간 측정 오차(1 msec)이다. 그러므로 이는 0.001 초 오차로 45 초 기다리는 서비스를 10 개 실행한다.

delay 100 60000 1000

이라고 하면, 1 초 오차로 60 초 기다리는 서비스를 100 개 실행한다. delay 뒤 세 매개변수를 여러가지로 바꾸면서 실행해보기 바란다. 2015년 맥북 i5 칩으로 위의 45 초 기다려는 cpu 점유율이 30% 정도, 60 초 기다려는 2% 정도 나온다. delay 다음 세 번째 매개변수 - 시간 측정 오차(Tolerance) - 가 1 에 가까울 수록 cpu 집약적이 되고 1 보다 커지면 cpu 집약적이 아닌 것이 된다. 그래도 ‘기다려 서비스’ 여러 개는 병렬 처리에 문제가 없다.

이제 확실하게 cpu 집약적인 ‘再歸 피보나치 서비스’를 만들고 기다려 서비스와 비교해 보자.

// FibonacciService.cs

    public class FibonacciService : ServiceAbstract
    {
        public ulong N { get; private set; }
        private ulong _currentN;

		public FibonacciService(ulong n)
		{
            N = n;
		}

        public override async Task Request()
        {
            await base.Request().ConfigureAwait(false);

            await Task.Factory.StartNew(() =>
            {
                try
                {
                    IsRunning = true;
                    CancelRequired = false;
                    var result = Recursive(N);
                    OnRequestCompleted(new IService.RequestCompletedArgs(result));
                    if (CancelRequired)
                    {
                        Console.WriteLine("CancelRequired is bypass.");
                    }
                }
                catch (OverflowException oe)
                {
                    OnRequestCanceled(new IService.RequestCanceledArgs($"{OVERFLOW}{':'}{oe.Message}"));
                }
                catch (Exception e)
                {
                    if (e.Message.Equals(CANCEL))
                    {
                        OnRequestCanceled(new IService.RequestCanceledArgs($"{COMPUTING}{':'}{_currentN}"));
                    }
                    else
                    {
                        OnRequestCanceled(new IService.RequestCanceledArgs($"{EXCEPTION}{':'}{e.Message}"));
                    }
                }
                finally
                {
                    IsRunning = false;
                }
            }, TaskCreationOptions.LongRunning).ConfigureAwait(false);
        }

        private ulong Recursive(ulong n)
        {
            if (CancelRequired)
            {
                _currentN = n;
                throw new Exception(CANCEL);
            }

            return checked((n < 2) ? n : Recursive(n - 1) + Recursive(n - 2));
        }


        protected override void OnRequestCompleted(IService.RequestCompletedArgs args)
        {
            base.OnRequestCompleted(args);
        }

        protected override void OnRequestCanceled(IService.RequestCanceledArgs args)
        {
            base.OnRequestCanceled(args);
        }


        private class ExceptionIssuedArgs : EventArgs
        {
            public Exception Exception { get; init; }
            public ExceptionIssuedArgs(Exception exception)
            {
                Exception = exception;
            }

            public override string ToString() => $"Exception Issued. {Exception}";
        }

        private static readonly string CANCEL = "Cancel";
        private static readonly string OVERFLOW = "Overflow";
        private static readonly string EXCEPTION = "Exception";
        private static readonly string COMPUTING = "Computing";
    }
// Program.cs

Console.WriteLine("Hello, World!");

IService[]? services = null;
var exitRequired = false;
while (!exitRequired)
{
    var input = Console.ReadLine();
    var commands = input!.Split(' ');

    var command = commands[0];
    var argument1 = (commands.Length > 1) ? int.Parse(commands[1]) : 0;
    var argument2 = (commands.Length > 2) ? int.Parse(commands[2]) : 0;
    var argument3 = (commands.Length > 3) ? int.Parse(commands[3]) : 0;


    switch (command)
    {
        case "exit":
            exitRequired = true;
            break;
        case "fibo":
            if (commands.Length != 3)
            {
                Console.WriteLine("Usage: fibo serviceCount N");
            }
            var fiboServiceCount = argument1;
            var n = (ulong)argument2;

            services = new FibonacciService[fiboServiceCount];
            var fiboTasks = new Task[fiboServiceCount];

            for (int i = 0; i < fiboServiceCount; i++)
            {
                services[i] = new FibonacciService(n);
                services[i].RequestCompleted += IService_RequestCompleted;
                services[i].RequestCanceled += IService_RequestCanceled;
                fiboTasks[i] = services[i].Request();
            }

            //var start = DateTimeOffset.Now.ToUnixTimeMilliseconds();
            _ = Task.WhenAll(fiboTasks).ConfigureAwait(false);
            //var final = DateTimeOffset.Now.ToUnixTimeMilliseconds();

            //Console.WriteLine($"Elapsed: {final - start}msec");
            break;
        case "delay":
            if (commands.Length != 4)
            {
                Console.WriteLine("Usage: delay serviceCount delayTime(msec) tolerance(msec)");
            }
            var delayServiceCount = argument1;
            var delayTime = TimeSpan.FromMilliseconds(argument2);
            var tolerance = TimeSpan.FromMilliseconds(argument3);

            services = new DelayService[delayServiceCount];
            var delayTasks = new Task[delayServiceCount];

            for (int i = 0; i < delayServiceCount; i++)
            {
                services[i] = new DelayService(delayTime, tolerance);
                services[i].RequestCompleted += IService_RequestCompleted;
                services[i].RequestCanceled += IService_RequestCanceled;
                delayTasks[i] = services[i].Request();
            }

            //var start = DateTimeOffset.Now.ToUnixTimeMilliseconds();
            _ = Task.WhenAll(delayTasks).ConfigureAwait(false);
            //var final = DateTimeOffset.Now.ToUnixTimeMilliseconds();

            //Console.WriteLine($"Elapsed: {final - start}msec");
            break;
        case "cancel":
            if (services is not null)
            {
                for (int i = 0; i < services.Length; i++)
                {
                    services[i].Cancel();
                }
            }
            break;
         default:
            if (!string.IsNullOrEmpty(input))
            {
                Console.WriteLine(input);
            }
            break;
    }
}

Console.WriteLine("Bye.");
await Task.Delay(500).ConfigureAwait(false);
return;

void IService_RequestCanceled(object? sender, IService.RequestCanceledArgs e)
{
    OnServiceEvent(sender, e);
}

void IService_RequestCompleted(object? sender, IService.RequestCompletedArgs e)
{
    OnServiceEvent(sender, e);
}

void OnServiceEvent(object? sender, EventArgs e)
{
    Console.WriteLine(e);

    if (sender is IService service)
    {
        service.RequestCompleted -= IService_RequestCompleted;
        service.RequestCanceled -= IService_RequestCanceled;
    }
}

프로그램을 실행한 뒤에,

fibo 1 45

을 입력하고 ‘엔터’ 단추를 누른다. fibo 다음 첫 매개변수는 ‘再歸 피보나치 서비스’의 수, 두 번째는 피보나치 수열의 순번이다. 글쓴이 노트북으로 Debug 모드에서 cpu 점유율이 99~100% 정도 나온다.

fibo 5 45

이렇게 다섯 개의 서비스를 실행하면 순차적으로 마무리 됨을 알 수 있다. cpu 프로세서 수(코어 수)가 많다면 병렬 처리가 되고 더 빨리 마무리 될 것이라 생각한다.

cpu 집약적인 작업을 여러 개 병렬로 실행하면, 실제 cpu 프로세서 수(코어 수)에 따라 성능이 좌우될 것이다. 그 작업이 쓰레드에 들러붙어(thread-sticked)있기 때문에.

[참고 1] BackgroundWorker Class

[참고 2] What is the difference between CPU intensive and I/O intensive?

4 Likes