알고리즘 문제 사이트들이다.

UVa Online Judge

내가 요즘 가는 곳. 그런데 엄청 페이지 로딩 속도가 느리다. 심각할 정도.

가입하고 Online Judge의 Browse Problems에서 문제를 골라 소스를 작성한 뒤 제출하면 정답 여부를 확인할 수 있다. 

서버의 컴파일러는 gcc이므로 Visual C++ 컴파일러로 잘 돌아가는 것도 업로드하면 컴파일 에러 나는 경우가 있으니 주의한다.
예를 들면 템플릿 사용할 때 꺾쇠를 연이어서 사용하면 - << 나 >> - VC++에서는 잘 인식되지만 gcc에서는 스트림 연산자로 인식되므로 한 칸 띄어써야 한다. 


USACO

대학교 다닐 때 ACM 모임 들어가서 처음으로 알게 된 알고리즘 문제 사이트. UVa에 비해 페이지가 좀 촌스럽지만 속도가 잘 나오는 편이다.

2010-11-04 
한번 방문해봤는데, 2007년 3월에 다섯 문제 풀고 한 문제 본 상태이다. 좀 부끄럽다. 당시 몇 번 나가다가 학업이 바쁘다는 핑계로 나가지 않았는데 지금 생각하니 좀 후회가 된다. 계속 했으면 지금의 내가 어떻게 변해 있었을까?



알고리즘 문제는 공학이라기 보다는 수학의 영역에 가까워서, 코드를 보기 좋게 한다거나 구조나 설계 같은걸 잘 만드는 것 보다는 문제를 빨리, 그리고 정확하게 푸는 게 더 중요하다.

풀다 보면 두 가지 상황이 발생하는데, 하나는 공학 - 그러니까 프로그래밍 - 스킬의 부족으로, 문제를 푸는 방법은 남에게 말할 수 있는데 코드로 작성하는 못하는 상황이다.

다른 하나는 흔한 상황으로, 그냥 푸는 방법을 찾지 못하는 경우이다.


문제를 풀다 보면 지루하고 귀찮고 짜증나서 집중하지 못하고 웹서핑을 한다거나 게임을 켜버린다거나 하는데, 어떻게 해야 극복할 수 있을 지 생각해보고 있는 중이다. 마치 숙제를 하는 기분이다.

사실 산수 문제 푸는걸 어렸을 때부터 별로 좋아하지 않았다. 과학은 아주 좋아했지만, 수학은 곱셈 배울 때 부터 나를 힘들게 했다. 어쩌면 구구단을 맞으면서 배워서 그럴지도 모르겠다.



반응형
결론부터 말하자면
 
1. 링크드 리스트 당 하나의 동기화 객체를 사용할 것.

2. 성능을 위해 Read Write Lock(또는 Shared Exclusive Lock이라고도 함)을 사용할 것.
 (동시에 여러명이 Read 가능, 한명만 Write 가능)

3. 리스트가 길어져서 탐색 시간과 대기 시간이 증가할 것 같으면 해시 테이블을 사용하고, 각 버킷별로 하나의 동기화 객체를 사용할 것.



멀티스레드 프로그래밍
에서 링크드 리스트(Linked List)를 다루다 보면 마주치는 문제가 있다. 바로 동기화이다.

이게 접근할 수 있는 스레드가 한개면 상관이 없는데, 여러개일 경우에는 push와 pop이 동시에 진행되면서 잘못된 메모리 참조를 하고 프로그램이 죽어버리는 상황이 발생할 수 있다. 실제로 이 점을 전혀 염두하지 않고 프로그램을 만들었다가 프로그램이 피를 토하며 죽고 나서야 실수를 깨달았던 경우가 있었다. 그 후부터는 항상 리스트를 만들 때 동기화가 필요한지에 대해 생각한다.

대개 이런 상황에서는 리스트 한개당 하나의 크리티컬 섹션을 사용해서 동기화하는 방법을 사용했다. 그런데 리스트가 길어지다 보면 탐색 시간이 오래 걸리기 마련인데, 만약 여러 스레드가 하나의 리스트를 '읽으려' 할 때 동시에 읽어도 되지만 크리티컬 섹션에 막혀서 하나씩 대기를 타고 있는 비효율적인 상황이 발생한 적이 있다.


이 문제를 해결하기 위해서 처음에 생각해 낸 방법은 '각 노드마다 동기화 객체 사용'이었다. 리스트 전체를 잠그는 대신 하나의 노드만 잠그고 읽거나 쓰면, 잠기지 않은 다른 노드는 다른 스레드가 접근할 수 있기 때문이다.

그런데 생각하다보니 문제가 하나 생겼는데, 일단 노드들이 주루룩 있는 상태에서는 각 노드를 잠가서 읽거나 쓰는(노드의 내용을 수정하는)데는 문제가 없었다. 하지만 링크를 끊거나 연결하는 과정에서 문제가 발생할 것이 분명했다.

동기화라는 것은 원자적(아토믹, Atomic)이지 않은 여러 개의 명령을 원자성을 가지도록 해주는 것이다. 원자적인 명령 단위는 멀티스레드 환경에서도 안전하다. 자세한 내용은 생략.

위에서 말한 그 문제는, 링크드 리스트에서 노드를 추가하거나 삭제할 때 연결을 맺고 끊는 과정이 원자적이지 않아서 발생한 것이다. 앞 노드의 연결을 수정하고, 뒤 노드의 연결을 수정하는 두개의 과정이 있기 때문이다. 그럼 락 걸고 앞뒤로 수정한 다음에 언락하면 되는거 아냐? 말은 쉽지만 1번 노드에 락 건다고 2번 노드까지 잠기는게 아니니까.

고민하던 끝에... 그래! 그냥 락 앞뒤로 걸어버리자! 하고 생각했다.

이론적으로 될 거라고 생각했다. 하지만 데드락과의 사투 끝에... 결국 내린 결론은 리스트 통째로 락 거는게 가장 낫다는 것이었다. 동기화 로직에 들어가는 코드의 양이 비대해지고 복잡해지면서 과연 다른 사람이 이 코드를 보고 이해할까 하는 생각이 드는 것은 물론이었다. 어찌어찌 해서 사실 완성은 했지만, 결국 이 코드를 버렸다.

쓰기를 할 때 뿐만이 아니라 읽기를 할 때도 필요한 노드를 포함해서 앞뒤 3개의 노드에 락을 걸어야 했다. 물론 데드락이 생기지 않게 왼쪽 오른쪽 자신 순서로 걸도록 했지만...

이제 여러 개의 스레드가 읽는다고 쳐보자. 락과 언락에 걸리는 시간때문에 오히려 퍼포먼스가 떨어질 지경이다.

그래서 버렸다. 사실 성능 측정으로 비교해보지는 않았다. 하지만 그렇게 직감이 들었다.


그 대안으로 해시 테이블을 선택했다. 말하자면 리스트를 쪼개서 분산 처리하는 것이다. 테이블의 각 버킷을 링크드 리스트로 만들면 된다. 그리고 각 버킷별로 동기화 객체를 사용하면 된다. 동시에 읽을 수 있게 Read Write Lock을 사용하면 더 좋다(Read Write Lock은 Interlock을 사용해서 직접 구현했다).

말은 마치 해시 테이블을 교묘하게 이용한 링크드 리스트처럼 했지만, 그냥 해시 테이블을 쓰는거다. 충돌이 발생하면 링크드 리스트로 이어가는 것일 뿐이지.

해시 테이블은 링크드 리스트보다 조금 더 메모리를 소모한다. 하지만 많은 데이터가 있어도 검색과 삽입, 삭제가 빠르므로 현업에서 자주 사용하고 있다.

반응형

'C, C++ 일반 > 알고리즘' 카테고리의 다른 글

알고리즘 문제 사이트  (0) 2010.11.04

+ Recent posts