C# 유니티에서 코루틴을 사용하면서 동시성과 병렬성관련 내용까지 찾아보게 되었습니다.
코루틴은 동시성 프로그래밍을 지원합니다.
제가 코루틴에 대한 이해도가 거의 없을 때는 병렬로 처리를 해주는 줄 알았습니다.
스레드를 하나 더 생성하거나 불러와서 해당 태스크(Task)를 처리하도록 하는 줄 알았던 것입니다.
병렬성 프로그래밍에 고려할 점과 위험성에 대해서 잘 모르고 단순하게 생각했던 결과입니다.
그래서 이번에 우선 동시성과 병렬성에 대한 개념을 정리하면서 이해하기 위해 작성하게 되었습니다.
동시성(Concurrency)
병렬성(Parallelism)
지연(Latency)
락(Lock)
공유 자원에 동시 접근 문제점
- 동시성은 동시에 두 가지 이상의 일을 하는 것
- 동시성은 멀티태스킹
- 동시에 실행되는 것 같이 보인다
위 글은 파이썬 격월 세미나 영상(링크)에서 나온 내용입니다.
정말 간결하게 동시성을 설명하는 글이라고 생각해서 가져와봤습니다.
동시성은 실행하는 궁극적인 주체(코어 혹은 스레드)가 하나인데 두 가지 이상의 일을 수행하는 것입니다.
하지만 컴퓨터가 처리하는 속도가 사람이 인지하기 힘들 정도로 빠르기 때문에 동시에 실행되는 것처럼 보입니다.
- 병렬성은 동시에 두 가지 이상의 일을 하는 것
- 실제로 동시에 여러 작업이 처리되는 것
병렬성은 동시성과 반대로 실제로 해당 태스크(Task)를 수행하는 주체가 다수인 것입니다.
대부분 OS 관련 책에는 싱글 코어와 멀티 코어의 차이로 설명을 합니다.
병렬성은 여기서 멀티 코어에서 각 태스크 하나 당 하나의 코어가 작업을 진행하는 것을 예시로 듭니다.
위 자료를 보면 동시성(Concurrent)는 병렬성과 차이를 한눈에 볼 수 있습니다.
여기서 조금 더 눈여겨 봐야 할 것은 동시성에서는 Context Switch가 발생한다는 것입니다.
문맥 교환(Context switch)는 하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(문맥)을 보관하고 새로운 프로세스의 상태를 적재하는 작업을 말한다.
한 프로세스의 문맥은 그 프로세스의 프로세스 제어 블록(PCB)에 기록되어 있다.
<위키피디아 문맥 교환>
이런 문맥 교환이 빈번하게 발생하면 많은 오버헤드가 생길 수 있습니다.
만약에 하나의 코어에 하나의 태스크(T1)만 실행 시키면 Context Switch는 발생하지 않을 것입니다.
하지만 사진에는 2개 이상의 태스크를 실행 시킬 때 차이를 보여주고 있습니다.
싱글 스레드 보다 멀티 스레드가 작업을 실행하는 주체가 많기 때문에 이점이 훨씬 많다고 생각했습니다.
하지만 멀티 스레드를 공부하면서 고려해야 할 부분이 상당히 많다는 것을 알게되었습니다.
성능적 이점과 같은 부분 때문에 병렬 프로그래밍을 무턱대고 도입하다가,
위와 같은 치명적인 문제가 발생할 수 있습니다.
공유 자원의 접근을 해결하기 위해 Lock을 거는 방법이 있지만,
무분별한 Lock을 걸게 되면 지연(Latency) 생기는 문제가 발생할 수 있습니다.
Lock을 사용해서 공유 자원에 대한 접근을 하나의 스레드만 접근 하도록 보장 하지만,
그 만큼 다른 스레드가 대기 시간이 길어지면 생기는 문제입니다.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
class Exercise
{
const string PASSWORD = "password";
const int MAX = 10000000;
static SHA256 SHA256 = SHA256.Create();
static void Main()
{
List<string> passwordList = new List<string>(MAX);
for (int i = 0; i < MAX; i++)
passwordList.Add($"{PASSWORD}{i}");
SequentialEncryt(passwordList);
ParallelEncryt(passwordList);
}
static void SequentialEncryt(List<string> passwordList)
{
DateTime start = DateTime.Now;
for (int i = 0; i < passwordList.Count; i++)
{
byte[] array = Encoding.Default.GetBytes(passwordList[i]);
byte[] hashValue = SHA256.ComputeHash(array);
};
DateTime end = DateTime.Now;
TimeSpan ts = (end - start);
Console.WriteLine($"순차적 : {ts.TotalMilliseconds}");
}
static void ParallelEncryt(List<string> passwordList)
{
DateTime start = DateTime.Now;
Parallel.For(0, passwordList.Count, i =>
{
byte[] array = Encoding.Default.GetBytes(passwordList[i]);
byte[] hashValue = SHA256.ComputeHash(array);
});
DateTime end = DateTime.Now;
TimeSpan ts = (end - start);
Console.WriteLine($"병렬적 : {ts.TotalMilliseconds}");
}
}
위 코드는 문자열로 되어있는 패스워드 데이터를 SHA256으로 암호화 하는 코드입니다.
순차 처리되는 암호화와 병렬로 처리되는 암호화 로직의 성능을 비교해봤습니다.
대량의 데이터를 가용한 CPU를 최대한 활용해 여러 쓰레드가 나눠서 처리하는 병렬 처리가 순차적 처리보다 빠를 가능성이 높다는 것을 알게 되었습니다.
C#에서 Parallel 클래스가 있어서 쉽게 구현해보고 테스트 할 수 있었습니다.
좋은 글이네요. 공유해주셔서 감사합니다.