여러 클라이언트가 하나의 서버를 통해 통신하는 구조를 나타내면 아래 그림과 같다.
위 구조에서 클라이언트는 서버와 통신을 위한 Session을 가지고 있고 Session을 통해 다른 클라이언트와 데이터를 주고 받을 수 있다. 하지만, 서버는 특정 클라이언트에 대한 정보가 오면 다른 클라이언트에게 Broadcast해주기 위해 연결된 클라이언트들의 Session 목록을 가지고 있어야 한다.
예를들어, 그룹채팅 시스템에서 한 클라이언트가 서버에게 채팅을 보내는 상황이라고 하자. 채팅을 보내는 상황순서는 아래그림과 같이 나타낼 수 있다.
여기서 주의해야할 점은 Server에 등록된 Client와의 Session목록은 멀티쓰레드환경이기 때문에 각각의 Session에 채팅내용을 전송하는과정은 lock
을 이용해 한번에 한 쓰레드만 접근가능한 환경이라는 것이다.
만약, N개의 클라이언트가 동시에 채팅을 입력한다면 서버는 N개의 채팅내용을 N개의 Client에게 전송해야 한다. 이러한 상황에서 한 쓰레드만 전송하도록하면 밀려오는 채팅내용을 전송하는데 대기하는 쓰레드는 점점 쌓이게 될 것이다.
한 쓰레드가 [2]를 진행하는 동안 다른 쓰레드는 대기하는 상황을 해결하기 위해 JobQueue를 사용하는 방법이 있다. JobQueue는 앞으로 해야할 일들을 Queue형식으로 저장하는 자료구조로 사용하며 대기하는 쓰레드는 JobQueue에 [2]라는 Job을 넣어주고 다른작업을 할 수 있다.
구현하고자하는 JobQueue는 일반 Queue와 다르게 Push
작업만 있다. JobQueue는 "~한 일을 할 수 있으면 하고 다른 쓰레드가 사용중이라면 Queue에 넣는 기능"을 수행하기 때문에 Pop
은 필요가 없다. 물론 JobQueue에 존재하는 Queue에 진입할 때는 lock을 통해 하나의 쓰레드만 Enqueue하도록 해야한다.
public interface IJobQueue
{
void Push(Action Job);
}
JobQueue는 Action을 저장한다. 즉, Queue에는 실행할 함수를 넣으며 위의 예제에서는 클라이언트에게 채팅내용을 전송하는 함수를 넣는다. JobQueue에 접근하는 쓰레드의 실행 순서는 다음과 같다.
Queue<Action> _jobQueue = new Queue<Action>();
object _lock = new object();
bool _flush = false;
_flush
같은 경우 값을 변경할 때 멀티쓰레드환경을 고려해서 lock으로 잠긴 영역에서 값을 변경해야 다른 쓰레드의 진입을 막을 수 있다.
public void Push(Action Job)
{
bool flush = false;
lock (_lock)
{
_jobQueue.Enqueue(Job);
if (_flush == false)
flush = _flush = true;
}
if (flush)
Flush();
}
공유자원인 _jobQueue
에 Enqueue할 때는 _lock
을 이용해 임계구역을 형성해 작업을 해야한다. 그리고 Push
함수는 Queue에 Enqueue만 하는것이 아닌 다른 쓰레드가 작업을 처리하고 있지 않으면 직접 처리해야한다. 지역변수와 임계구역을 사용해 현재 다른 쓰레드가 사용하고 있는지를 판별한 뒤 flush == true
이면 직접 처리하도록 Flush
함수를 호출한다.
void Flush()
{
while (true)
{
Action action = Pop();
if (action == null)
{
_flush = false;
return;
}
action.Invoke();
}
}
작업을 직접 처리하는 함수로 Queue가 빌 때까지 Action을 Invoke한다. 여기서 "Queue에서 Pop하는데 lock을 안해도 되는건가?"라고 생각할 수 있다. 하지만 위의 Push
함수와 함께 생각하면 Flush
함수에 접근하는 쓰레드는 단 하나만 존재한다. 즉, Flush
함수에는 lock이 필요없다.
Action Pop()
{
if (_jobQueue.Count == 0)
{
return null;
}
return _jobQueue.Dequeue();
}
Queue에 있는 값을 Pop하는 함수로 여기 역시 Flush
와 같은 이유로 lock이 필요없다.
public interface IJobQueue
{
void Push(Action Job);
}
public class JobQueue : IJobQueue
{
Queue<Action> _jobQueue = new Queue<Action>();
object _lock = new object();
bool _flush = false;
public void Push(Action Job)
{
bool flush = false;
lock (_lock)
{
_jobQueue.Enqueue(Job);
if (_flush == false)
flush = _flush = true;
}
if (flush)
Flush();
}
void Flush()
{
while (true)
{
Action action = Pop();
if (action == null)
{
_flush = false;
return;
}
action.Invoke();
}
}
Action Pop()
{
if (_jobQueue.Count == 0)
{
return null;
}
return _jobQueue.Dequeue();
}
}