개발/컴퓨터사이언스

RCU (Read-Copy Update)

kwony 2018. 10. 30. 21:41

공유중인 값을 읽는 쓰레드와 업데이트하는 쓰레의 개수가 각각 하나인 경우에는 경우에는 spin_lock을 이용해도 별 어려움 없이 동기화 작업을 수행 할 수 있다. 그런데 값을 읽는 프로세스가 두개 이상이 될 때부터는 동기화 작업이 골치가 아파진다. 아래의 코드처럼 두개 이상의 쓰레드가 공유중인 val 값을 읽으려고 하고 한개의 쓰레드가 값을 업데이트 하고 있다고 해보자. 본래의 목적은 reader를 호출하는 thd[0]와 thd[1] 쓰레드가 writer를 호출하는 thd[2] 쓰레드가 업데이트 하는 값을 동일하게 읽는 것이었다.


reader 두개를 writer보다 앞서서 생성한 덕분에 초반에는 어느 정도 동기화가 유지가 되겠지만 시간이 지나면서 쓰레드의 priority 값이 달라지게되고 어떤 기점에서 thd[2] 쓰레드가 thd[0]와 thd[1] 사이에 실행될 수도 있다. 이런 경우 thd[0]와 thd[1]은 같은 값을 읽지못하게 된다.


pthread_spinlock_t lock;
int val;

void *reader() {
    int read;
    while (1) {
        pthread_spin_lock(&lock);
        read = val;
        printf("reader1: %d\n", val);
        pthread_spin_unlock(&lock);
    }
}

void *writer() {
    while(1) {
        pthread_spin_lock(&lock);
        val++;
        pthread_spin_unlock(&lock);
    }
}

int main() {
    pthread_t thd[3];

    pthread_spin_init(&lock, PTHREAD_PROCESS_SHARED);
    pthread_create(&thd[0], NULL, reader, NULL);
    pthread_create(&thd[1], NULL, reader, NULL);
    pthread_create(&thd[2], NULL, writer, NULL);

    pthread_join(thd[0], NULL);
    pthread_join(thd[1], NULL);
    pthread_join(thd[2], NULL);

    printf("DDD\n");

    return 1;
}


이런 경우를 위해 리눅스에서는 RCU(Read-Copy Update)라는 라이브러리를 제공한다. RCU를 사용하면 여러 개의 읽기 작업과 한 개의 쓰기 작업의 동기화를 구현 할 수 있다.  단, 여러 개의 쓰기 작업의 동기화는 지원이 안된다. LWN에서 소개한 예시코드로 간단히 사용 방법을 익혀보자.


 struct foo {
   struct list_head list;
   int a;
   int b;
   int c;
 };
 LIST_HEAD(head);

 /* . . . */

 p = kmalloc(sizeof(*p), GFP_KERNEL);
 p->a = 1;
 p->b = 2;
 p->c = 3;
 list_add_rcu(&p->list, &head);

// Thread1
 rcu_read_lock();
 list_for_each_entry_rcu(p, head, list) {
   do_something_with(p->a, p->b, p->c);
 }
 rcu_read_unlock();

// Thread2
 q = kmalloc(sizeof(*p), GFP_KERNEL);
 *q = *p;
 q->b = 2;
 q->c = 3;
 list_replace_rcu(&p->list, &q->list);
 synchronize_rcu();
 kfree(p);


11~15 라인에서는 `foo`구조체 포인터로 선언된 `p`변수에 메모리 공간을 할당하고 값을 대입한 후 `list_add_rcu`라는 함수를 이용해 리스트에 넣는 작업이다. `list_head` 변수가 확장성이 좋아 RCU에서도 list를 이용해 여러 개의 변수를 관리할 수 있도록 했다.


Thread1 은 읽는 작업을 수행하는 reader다. `p`값을 읽어오기 전에 `rcu_read_lock()` 함수를 호출해서 접근 권한을 얻고 작업을 수행한 다음에는 `rcu_read_unlock()`으로 lock을 풀어준다. 다른 쓰레드도 읽는 작업을 수행한다면 위와 동일한 코드를 탄다.


Thread2는 rcu 리스트의 값을 업데이트하는 쓰레드다. `list_replace_rcu()` 함수는 첫번째 인자의 값을 두번째 인자의 값으로 바꾸는 함수다. 그리고 바로 아래 `synchronize_rcu()`는 다른 쓰레드에서 rcu 리스트의 값을 읽는 작업이 모두 종료될 때 까지 기다린 후 수정한 정보를 업데이트 한다. 이 함수가 호출되지 않으면 RCU 리스트 수정 함수를 호출해도 reader쪽에서 읽는 값은 동일하다. 이 함수를 이용해 읽는 작업과 쓰는 작업의 동기화를 맞춰 줄 수 있다.


* 더 상세한 동작 메커니즘을 알고 싶으신 분은 LWN 을 참조하면 좋을 것 같다.