이 시리즈의 결론 부분인 이 기사에서는 두 가지 사항을 살펴본다. 첫째는 뮤텍스 기반의 동시성 목록을 구현할 때 선택해야 할 설계 방법이고 둘째는
뮤텍스를 사용하지 않고 동시성 데이터 구조를 설계하는 방법이다. 두 번째 주제에서는 동시성 스택을 구현하고 이러한 데이터 구조를 설계하는 과정에서 발생하는
문제점을 일부 강조한다. 플랫폼에 독립적인 C++로 뮤텍스가 없는 데이터 구조를 설계하는 것은 아직 현실성이 없으므로 GCC 버전 4.3.4를 컴파일러로 선택하고
GCC에 특정된 __sync_* 함수를 코드에서 사용한다. Windows® C++ 개발자라면 비슷한 작업을 수행하는 데 필요한 상호 잠금식* 함수 그룹을 고려하도록 한다.
동시성 단일 연결 목록을 구현할 때 선택해야 할 설계 방법
목록 1에는 가장 기본적인 동시성 단일 연결 목록 인터페이스가 표시되어 있다. 분명히 무엇인가 누락되어 있지 않은가?
목록 1. 동시성 단일 연결 목록 인터페이스
template <typename T>
class SList {
private:
typedef struct Node {
T data;
Node *next;
Node(T& data) : value(data), next(NULL) { }
} Node;
pthread_mutex_t _lock;
Node *head, *tail;
public:
void push_back(T& value);
void insert_after(T& previous, T& value); // insert data after previous
void remove(const T& value);
bool find(const T& value); // return true on success
SList( );
~SList( );
};
|
예상되는 행을 처리하기 위해 목록 2에서는 push_back 메소드를 정의한다.
목록 2. 동시성 연결 목록에 데이터 밀어넣기
void SList<T>::push_back(T& data)
{
pthread_mutex_lock(&_lock);
if (head == NULL) {
head = new Node(data);
tail = head;
} else {
tail->next = new Node(data);
tail = tail->next;
}
pthread_mutex_unlock(&_lock);
}
|
이제 push_back을 호출하여 연속해서 n 개의 정수를 이 목록에 밀어 넣는 스레드를 생각해 보자. 인터페이스 자체에는
처음에 잠금을 획득하기 전에 삽입될 모든 데이터가 알려진 경우에도 뮤텍스를 n 번 획득하고 릴리스하도록 되어 있다. 훨씬 더 나은 방법은
정수 목록을 받는 또 다른 메소드를 정의하여 뮤텍스를 한 번만 획득하고 릴리스하는 것이다. 목록 3에는 이 메소드가 정의되어 있다.
목록 3. 연결 목록에 지능적으로 추가
void SList<T>::push_back(T* data, int count) // or use C++ iterators
{
Node *begin = new Node(data[0]);
Node *temp = begin;
for (int i=1; i<count; ++i) {
temp->next = new Node(data[i]);
temp = temp->next;
}
pthread_mutex_lock(&_lock);
if (head == NULL) {
head = begin;
tail = head;
} else {
tail->next = begin;
tail = temp;
}
pthread_mutex_unlock(&_lock);
}
|
이제, 연결 목록의 검색 요소, 즉 find 메소드를 최적화하도록 하자. 다음과 같은 몇 가지 상황이 발생할 수 있다.
- 일부 스레드가 연결 목록 전체를 반복하는 동안 삽입 또는 삭제 요청이 들어온다.
- 일부 스레드가 연결 목록을 반복하는 동안 반복 요청이 들어온다.
- 일부 스레드가 데이터를 연결 목록에 삽입하거나 삭제하는 동안 반복 요청이 들어온다.
분명히, 다수의 반복 요청을 동시에 서비스할 수 있어야 한다. 삽입/삭제 비율이 아주 작고 기본 활동이 검색으로 구성된 시스템에서는 단일한 잠금 기반 방식을
사용하지 않는 것이 좋다. 이러한 맥락에서 읽기/쓰기 잠금, 즉 pthread_rwlock_t를 알아보도록 하자.
이 기사의 예제에서는 pthread_mutex_t를 사용하지 않고 SList의 pthread_rwlock_t를 사용하게 된다. 이렇게 하면 다수의 스레드가 연결 목록을 동시에
검색할 수 있다. 데이터를 삽입하거나 삭제할 때 여전히 전체 목록을 잠그게 되지만 별다른 문제는 없다. 목록 4에는 pthread_rwlock_t를 사용하여 구현한
일부 연결 목록이 표시되어 있고 목록 5에는 find 메소드의 코드가 표시되어 있다.
목록 4. 읽기/쓰기 잠금을 사용하는 동시성 단일 연결 목록
template <typename T>
class SList {
private:
typedef struct Node {
// … same as before
} Node;
pthread_rwlock_t _rwlock; // Not pthread_mutex_t any more!
Node *head, *tail;
public:
// … other API remain as-is
SList( ) : head(NULL), tail(NULL) {
pthread_rwlock_init(&_rwlock, NULL);
}
~SList( ) {
pthread_rwlock_destroy(&_rwlock);
// … now cleanup nodes
}
};
|
목록 5에는 연결 목록을 검색하는 코드가 표시되어 있다.
목록 5. 읽기/쓰기 잠금을 사용하여 연결 목록 검색
bool SList<T>::find(const T& value)
{
pthread_rwlock_rdlock (&_rwlock);
Node* temp = head;
while (temp) {
if (temp->value == data) {
status = true;
break;
}
temp = temp->next;
}
pthread_rwlock_unlock(&_rwlock);
return status;
}
|
목록 6에는 읽기/쓰기 잠금을 사용하는 push_back이 표시되어 있다.
목록 6. 읽기/쓰기 잠금을 사용하여 동시성 연결 목록에 데이터 밀어넣기
void SList<T>::push_back(T& data)
{
pthread_setschedprio(pthread_self( ), SCHED_FIFO);
pthread_rwlock_wrlock(&_rwlock);
// … All the code here is same as Listing 2
pthread_rwlock_unlock(&_rwlock);
}
|
몇 가지 사항을 검토해 보도록 하자. 동기화를 위해 두 가지 잠금 함수(pthread_rwlock_rdlock 및 pthread_rwlock_wrlock)를 사용했으며
쓰기 스레드의 우선순위를 설정하기 위해 pthread_setschedprio를 호출했다. 쓰기 스레드가 이러한 잠금으로 인해 차단되지 않는 경우,
즉 삭제/삽입 요청이 없는 경우에는 읽기 스레드가 또 다른 읽기 스레드를 차단하지 않기 때문에 다수의 읽기 스레드가 동시에 목록 검색을
요청할 수 있다. 쓰기 스레드가 이러한 잠금을 대기하고 있는 경우에는 새로운 읽기 스레드가 잠금을 획득할 수 없으며 이 읽기 스레드는
기존의 읽기 스레드가 완료된 후, 쓰기 스레드가 완료될 때까지 대기한다. 이러한 방식을 사용하여 pthread_setschedprio로 쓰기 스레드의
우선순위를 지정하지 않는 경우에는 읽기/쓰기 잠금의 특성을 감안하면 쓰기 스레드가 어떻게 작동하는지 쉽게 파악할 수 있다.
이러한 방식을 사용할 때는 다음과 같은 몇 가지 사항을 기억해야 한다.
- 최대 읽기 잠금 수(정의된 구현)를 초과하면
pthread_rwlock_rdlock이 실패한다. - 동시성 읽기 잠금이 n 개 있는 경우에는
pthread_rwlock_unlock을 n 번 호출해야 한다.
마지막으로 살펴볼 메소드는 insert_after이다. 다시 한 번, 예상되는 사용 패턴에 따라 데이터 구조를 어떻게 조정할 것인지 의사결정을 한다. 삽입과 검색 수는
거의 동일하지만, 삭제는 최소로 이루어지는 사전 제공된 연결 목록으로 애플리케이션이 시작하는 경우에는 삽입 과정에서 전체 목록을 잠그지
않는 것이 좋다. 이러한 경우에는 연결 목록의 불연속 지점에서 동시성 삽입을 허용하고 읽기/쓰기 잠금을 기반으로 하는 방식을 다시
사용하는 것이 좋다. 연결 목록을 구성하는 방법은 다음과 같다.
- 잠금이 두 가지 레벨에서 발생(목록 7 참조): 연결 목록에는 읽기/쓰기 잠금이 있는 반면에 개별 노드에는 뮤텍스가 있다. 공간을 절약하고자 하는 경우에는 뮤텍스를 공유(노드-뮤텍스 맵 유지보수)하는 것을 고려하도록 한다.
- 삽입은 쓰기 스레드가 연결 목록에 읽기 잠금을 설정하고 난 후에 진행된다. 그 후에는 새 데이터가 추가될 개별 노드가 데이터가 삽입되기 전에 잠겼다가 데이터가 삽입된 후에 해제되며 그 다음에 읽기/쓰기 잠금이 해제된다.
- 삭제 과정에서는 연결 목록에 쓰기 잠금이 설정된다. 특정 노드에 잠금을 설정할 필요는 없다.
- 검색은 앞서와 같이 동시에 수행될 수 있다.
목록 7. 두 가지 레벨의 잠금을 사용하는 동시성 단일 연결 목록
template <typename T>
class SList {
private:
typedef struct Node {
pthread_mutex_lock lock;
T data;
Node *next;
Node(T& data) : value(data), next(NULL) {
pthread_mutex_init(&lock, NULL);
}
~Node( ) {
pthread_mutex_destroy(&lock);
}
} Node;
pthread_rwlock_t _rwlock; // 2 level locking
Node *head, *tail;
public:
// … all external API remain as-is
}
};
|
목록 8에는 연결 목록에 데이터를 삽입하는 코드가 표시되어 있다.
목록 8. 이중 잠금을 사용하여 연결 목록에 데이터 삽입
void SList<T>:: insert_after(T& previous, T& value)
{
pthread_rwlock_rdlock (&_rwlock);
Node* temp = head;
while (temp) {
if (temp->value == previous) {
break;
}
temp = temp->next;
}
Node* newNode = new Node(value);
pthread_mutex_lock(&temp->lock);
newNode->next = temp->next;
temp->next = newNode;
pthread_mutex_unlock(&temp->lock);
pthread_rwlock_unlock(&_rwlock);
return status;
}
|
이제까지는 동기화를 구현하기 위해 하나의 뮤텍스나 다수의 뮤텍스를 데이터 구조의 일부로 포함하여 사용했다. 그러나 이 방식에는 문제점이 있다. 다음과 같은 상황을 생각해 보자.
- 뮤텍스를 대기하는 과정에서 때로는 많은 시간이 소요된다. 이러한 지연은 시스템을 확장하는 데 부정적인 영향을 준다.
- 우선순위가 더 낮은 스레드가 뮤텍스를 획득할 수 있기 때문에 우선순위가 더 높은 스레드를 정지하려면 동일한 뮤텍스를 생성해야 한다. 이러한 문제점을 우선순위 역전이라고 한다(자세한 정보를 확인할 수 있는 링크는 참고자료 참조).
- 시간이 종료되었기 때문에 뮤텍스를 소유하고 있는 스레드의 스케줄이 취소될 수 있다. 동일한 뮤텍스를 대기하는 다른 스레드의 경우에는 대기 시간이 훨씬 더 길어지기 때문에 이점이 부정적인 영향을 미칠 수 있다. 이러한 문제점을 lock convoying이라고 한다. (자세한 정보를 가리키는 링크는 참고자료를 참조한다.)
뮤텍스의 문제점은 여기서 끝나지 않는다. 최근에는 뮤텍스를 사용하지 않는 솔루션이 발표되었다. 뮤텍스가 사용하기 어렵다고 하더라도 성능을 강화하려면 뮤텍스에 관심을 갖는 것이 좋다.
뮤텍스를 사용하지 않는 솔루션을 살펴보기 전에 80486으로 시작하는 모든 Intel® 프로세서에서 사용 가능한 CMPXCHG 어셈블리 명령어를 잠시 살펴보도록 하자. 목록 9에는 이 명령어가 수행하는 작업이 개념적 관점에서 표시되어 있다.
목록 9. 비교 및 스왑 명령어 작동
int compare_and_swap ( int *memory_location, int expected_value, int new_value)
{
int old_value = *memory_location;
if (old_value == expected_value)
*memory_location = new_value;
return old_value;
}
|
이 코드에서는 이 명령어가 메모리 위치에 예상된 값이 있는지 확인하여, 메모리 위치에 예상된 값이 있으면 새 값을 메모리 위치로 복사한다. 목록 10에는 어셈블리어 관점에서 작성된 의사 코드가 표시되어 있다.
목록 10. 비교 및 스왑 명령어의 어셈블리어 의사 코드
CMPXCHG OP1, OP2
if ({AL or AX or EAX} = OP1)
zero = 1 ;Set the zero flag in the flag register
OP1 = OP2
else
zero := 0 ;Clear the zero flag in the flag register
{AL or AX or EAX}= OP1
|
CPU는 피연산자(8, 16 또는 32비트)의 크기에 따라 AL, AX 또는 EAX 레지스터를 선택한다. AL, AX 또는 EAX 레지스터의 내용이 피연산자 1의 내용과 일치하면 피연산자 2의 내용이 첫 번째 레지스터에 복사된다. 그렇지 않으면 AL, AX 또는 EAX 레지스터가 피연산자 2의 값으로 업데이트된다. Intel Pentium® 64비트 프로세서에는 64비트 비교 및 교환을 지원하는 비슷한 명령어(CMPXCHG8B)가 있다. CMPXCHG 명령어는 원자적이다. 따라서 이 명령이 완료되기 전에는 중간에 시스템의 상태를 볼 수 없다. 이 명령어는 완전히 실행되었거나 아직 시작하지 않은 것이다. 다른 플랫폼에도 동일한 명령어가 있다. 예를 들면, Motorola MC68030 프로세서에는 시맨틱이 비슷한 명령어, 즉 CAS(Compare And Swap)가 있다.
CMPXCHG가 중요한 이유는 무엇일까? 이것이 필자가 어셈블리어로 코드를 작성할 것이라는 것을 의미하는 것일까?
CMPXCHG와 관련 명령어 CMPXCHG8B를 이해해야 한다. 이러한 명령어가 잠금을 사용하지 않는 솔루션의 핵심이기 때문이다. 그러나 어셈블리어로 코드를 작성하지 않아도 할 수 있다. 다행히도 GCC(GNU Compiler Collection, 버전 4.1 이후부터)에는 x86과 x86-64 플랫폼의 CAS 조작을 구현하는 데 사용할 수 있는 원자적 내장 함수가 있다(참고자료 참조). 이를 위해 헤더 파일을 삽입할 필요는 없다. 이 기사에서는 GCC 내장 함수를 사용하여 잠금을 사용하지 않는 구조를 구현한다. 다음과 같은 내장 함수를 살펴보도록 하자.
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...) type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...) |
__sync_bool_compare_and_swap 내장 함수는 oldval과 *ptr을 비교한다. 이 값이 서로 일치하면 newval을 *ptr로 복사한다. oldval과 *ptr이 일치하는
경우에는 리턴 값이 True이고 그렇지 않은 경우에는 False이다. 언제나 이전 값을 리턴한다는 점을 제외하면 __sync_val_compare_and_swap 내장 함수의 작동도 비슷하다.
목록 11에는 샘플 사용법이 표시되어 있다.
목록 11. GCC CAS 내장 함수의 샘플 사용법
#include <iostream>
using namespace std;
int main()
{
bool lock(false);
bool old_value = __sync_val_compare_and_swap( &lock, false, true);
cout >> lock >> endl; // prints 0x1
cout >> old_value >> endl; // prints 0x0
}
|
이제까지 CAS를 일부 살펴보았으므로 이제 동시성 스택을 설계해 보도록 하자. 잠금은 사용하지 않는 이러한 동시성 데이터 구조는 비블로킹 데이터 구조라고도 한다. 목록 12에는 코드 인터페이스가 표시되어 있다.
목록 12. 연결 목록을 기반으로 구현된 비블로킹 스택
template <typename T>
class Stack {
typedef struct Node {
T data;
Node* next;
Node(const T& d) : data(d), next(0) { }
} Node;
Node *top;
public:
Stack( ) : top(0) { }
void push(const T& data);
T pop( ) throw (…);
};
|
목록 13에는 밀어넣기(push) 조작이 표시되어 있다.
목록 13. 비블로킹 스택에 데이터 밀어넣기
void Stack<T>::push(const T& data)
{
Node *n = new Node(data);
while (1) {
n->next = top;
if (__sync_bool_compare_and_swap(&top, n->next, n)) { // CAS
break;
}
}
}
|
밀어넣기 조작은 어떻게 수행될까? 단일 스레드 관점에서는 새 노드가 작성되면 다음 포인터는 스택의 맨 위를 가리킨다. 다음에는 CAS를 호출하고 새 노드를 스택의 최상위 위치로 복사한다.
다중 스레드의 관점에서는 두 개 이상의 스레드가 동시에 데이터를 스택에 밀어 넣을 수 있다. 스레드 A와 스레드 B가 각각 20과 30을 스택에 밀어 넣으려 한다고
가정하면 스레드 A가 먼저 시간을 할당 받는다. 또한 스레드 A는 n->next = top 명령이 완료되면 스케줄이 취소된다. 다행히도 이 경우에는 스레드 B가
활동을 개시하기 때문에 CAS를 완료할 수 있으며 스레드 B는 스택에 30을 밀어 넣고 완료된다. 다음에는 스레드 A가 재개된다. 이 경우에는
스레드 B가 스택 맨 위에 있는 위치의 내용을 수정했기 때문에 *top과 n->next가 분명히 일치하지 않는다. 따라서 이 코드는 역순환되어 적절한
최상위 포인터(스레드 B 때문에 변경된)를 가리키고 CAS를 호출한 후, 스택에 20을 밀어 넣고 완료된다. 이러한 모든 과정은 잠금을 사용하지 않고 수행된다.
목록 14에는 스택에서 요소를 꺼내는 조작을 하는 코드가 표시되어 있다.
목록 14. 비블로킹 스택에서 데이터 꺼내기
T Stack<T>::pop( )
{
if (top == NULL)
throw std::string(“Cannot pop from empty stack”);
while (1) {
Node* next = top->next;
if (__sync_bool_compare_and_swap(&top, top, next)) { // CAS
return top->data;
}
}
}
|
밀어넣기 조작의 비슷한 행을 따라서 꺼내기 조작의 시맨틱을 정의한다.
스택의 맨 위에는 결과가 저장되므로 CAS를 사용하여 최상위 위치를 top-<next로 업데이트하고 적절한 데이터를 리턴한다. CAS가 수행되기 바로 전에
스레드 선점이 있었던 경우에는 스레드가 재개되고 나서 CAS가 실패하고 유효한 데이터가 사용 가능할 때까지 루프가 계속해서 순환된다.
불행히도 스택에서 데이터를 꺼내는 조작을 구현하는 데는 문제점(분명한 것과 분명하지 않은 것)이 있다. 분명한 문제점은 while 루프에서 널을 확인해야
한다는 점이다. 스레드 P와 스레드 Q는 모두 요소가 하나만 남아 있는 스택에서 데이터를 꺼내려고 하는데, 스레드 P가 CAS가 수행되기 바로 전에 스케줄이 취소된
경우에는 스레드 P가 다시 제어를 얻을 때까지 스택에는 꺼낼 데이터가 아무 것도 남아 있지 않다. 이는 스택의 top이 널이기 때문이며
이 &top을 액세스하면 분명히 오류가 발생한다. 이는 피할 수 없는 버그이다. 이러한 문제점을 통해
병렬 데이터 구조를 다룰 때에는 코드가 언제나 순차적으로 실행될 것이라고 생각하지 말라는 기본적인 설계 원칙을 확인할 수 있다.
목록 15에는 버그를 수정한 코드가 표시되어 있다.
목록 15. 비블로킹 스택에서 데이터 꺼내기
T Stack<T>::pop( )
{
while (1) {
if (top == NULL)
throw std::string(“Cannot pop from empty stack”);
Node* next = top->next;
if (top && __sync_bool_compare_and_swap(&top, top, next)) { // CAS
return top->data;
}
}
}
|
다음 문제점은 조금 더 복잡하지만, 메모리 관리자가 작동하는 방식(자세한 정보를 가리키는 링크는 참고자료 참조)을 이해한다면 그다지 어렵지 않을 것이다. 목록 16에는 이 문제점이 표시되어 있다.
목록 16. 메모리를 재활용하면 CAS에 심각한 문제점을 일으킬 수 있음
T* ptr1 = new T(8, 18);
T* old = ptr1;
// .. do stuff with ptr1
delete ptr1;
T* ptr2 = new T(0, 1);
// We can't guarantee that the operating system will not recycle memory
// Custom memory managers recycle memory often
if (old1 == ptr2) {
…
}
|
이 코드에서는 old와 ptr2의 값이 다르다는 것을 보장할 수 없다. 운영 체제와 사용자 정의 애플리케이션 메모리 관리 시스템에 따라서는
삭제된 메모리가 재활용될 가능성이 있다. 다시 말해서 필요한 경우에는 애플리케이션에서 재사용하기 위해 삭제된 메모리를 시스템에 반환하지 않고
특수한 풀에 저장한다. 이렇게 하면 추가 메모리를 요청하기 위해 시스템 호출을 거쳐야 할 필요가 없기 때문에 분명히 성능이 개선된다. 이렇게 하는 것이
일반적으로 좋지만, 비블로킹 스택에는 좋지 않은 영향을 미친다. 그 이유를 살펴보도록 하자.
두 개의 스레드 A와 B가 있다고 가정하자. 스레드 A는 pop을 호출하지만, CAS가 수행되기 바로 전에 스케줄이 취소된다. 스레드 B는 pop을
호출한 후, 데이터를 밀어 넣으며, 이 과정에서 이전의 꺼내기 조작에서 재활용된 메모리가 사용된다. 목록 17에는 의사 코드가 표시되어 있다.
목록 17. 시퀀스 다이어그램
Thread A tries to pop Stack Contents: 5 10 14 9 100 2 result = pointer to node containing 5 Thread A now de-scheduled Thread B gains control Stack Contents: 5 10 14 9 100 2 Thread B pops 5 Thread B pushes 8 16 24 of which 8 was from the same memory that earlier stored 5 Stack Contents: 8 16 24 10 14 9 100 2 Thread A gains control At this time, result is still a valid pointer and *result = 8 But next points to 10, skipping 16 and 24!!! |
수정하는 방법은 매우 간단하다. 다음 노드를 저장하지 않으면 된다. 목록 18에는 수정된 코드가 표시되어 있다.
목록 18. 비블로킹 스택에서 데이터 꺼내기
T Stack<T>::pop( )
{
while (1) {
Node* result = top;
if (result == NULL)
throw std::string(“Cannot pop from empty stack”);
if (top && __sync_bool_compare_and_swap(&top, result, result->next)) { // CAS
return top->data;
}
}
}
|
이렇게 정리하면 스레드 A가 꺼내기 조작을 시도하는 동안 스레드 B가 스택의 최상위 위치를 수정하는 경우에도 스택에 있는 요소를 건너 뛰지 않게 된다.
이 시리즈에서는 동시성 액세스를 따르는 데이터 구조를 설계하는 방법을 자세하게 살펴보았다. 또한, 뮤텍스를 기반으로 하는 방법이나 잠금을 사용하지 않는 방법을
선택하여 설계하는 방법을 살펴보았다. 어떤 방법을 사용하든지 이러한 데이터 구조의 전통적인 기능을 뛰어 넘는 개념을 고려해야 하며, 특히
스레드가 재스케줄될 때 스레드가 어떻게 재개되는지 그리고 선점에 대해 언제나 기억해야 한다. 특히 잠금을 사용하지 않는 솔루션은 현재 플랫폼과 컴파일러에
약간 특정적이다. 스레드와 잠금을 구현하는 데 필요한 Boost 라이브러리와 잠금을 사용하지 않는 연결 목록에 관한 John Valois의 논문을
살펴보도록 하자. (링크는 참고자료를 참조한다.) C++0x 표준에는 std::thread 클래스가 있지만, 이 클래스에 대한 지원은 매우 제한되어 있어
오래된 컴파일러에는 대부분 이 클래스가 분명히 존재하지 않는다.
교육
-
MSDN에는 스레드의 우선순위 역전에 대한 우수한 정보가 있다.
-
lock convoying에 관해 자세히 배우자.
-
캠브리지 대학교의 practical lock-free data structures 페이지를 확인하자.
-
John Valois의 잠금을 사용하지 않는 연결 목록 관련 논문을 확인하자.
-
memory manager for
C++(Arpan Sen 및 Rahul Kumar Kardam, developerWorks, 2008년 2월): 이 기사에서 C++의 메모리 관리에 관해 자세히 배우자. -
AIX와 UNIX developerWorks 영역: AIX와 UNIX 영역에서는 AIX 시스템 관리와 UNIX 스킬 확장의 모든 측면과 관련된 풍부한 정보를 제공한다.
-
AIX와 UNIX 입문
AIX와 UNIX 입문 페이지에서 자세한 정보를 볼 수 있다.
-
기술 서점: 다양한 기술 주제와 관련된 서적을 살펴볼 수 있다.
제품 및 기술 얻기
-
GCC 원자적 내장 함수를 확인하자.
-
Boost 스레드 라이브러리를 다운로드하여 자세히 배우자.
토론
-
Twitter의 developerWorks 페이지를 팔로우하자.
-
developerWorks 블로그: 블로그를 읽어 보고 developerWorks community에 참여하자.
-
다음과 같은 AIX 및 UNIX 포럼에 참여하자.
- AIX 5L—기술 포럼
- 개발자용 AIX 포럼
- Cluster Systems Management
- IBM Support Assistant
- Performance Tools—기술적
- 기타 AIX 및 UNIX 포럼
Arpan Sen은 선임 엔지니어로 전자 설계 자동화 분야의 소프트웨어 개발을 담당한다. 여러 해 동안 Solaris, SunOS, HP-UX 및 IRIX를 포함한 UNIX는 물론 Linux와 Microsoft Windows의 여러 제품과 관련된 업무를 맡아 왔다. 소프트웨어 성능 최적화 기법, 그래프 이론 및 병렬 컴퓨팅에 많은 관심을 가지고 있다. Arpan은 대학원에서 소프트웨어 시스템 분야를 전공했다. 이메일 주소는 arpansen@gmail.com이다.