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?