TL;DR
- Rust에서 여러 스레드가 데이터 하나를 함께 읽고 수정할 때
Mutex와Arc를 함께 사용해 한 번에 하나의 스레드만 데이터에 접근하게 함. - 데이터는
Mutex로 감싼 뒤 원자적 참조 카운팅(atomic reference counting)을 제공하는Arc로 감싸며, 복제된Arc는 같은Mutex와 벡터를 참조함. - 각 스레드는
lock()으로 잠금을 얻고, 잠금을 얻지 못하면 접근 가능해질 때까지 대기하며, 잠금이 유지되는 동안 벡터 값을 수정함. - 메인 스레드는 두 스레드에
.join()을 호출해 작업을 마칠 때까지 기다린 뒤 잠금을 얻어 갱신된 데이터를 출력함. - 잠금 획득 순서는 정해져 있지 않아 최종 값은
[3, 5, 7]또는[4, 6, 8]이 될 수 있으며, 임계 구역 밖에서도 작업해야 스레드 간 대기가 줄어듦.
데이터 공유와 `Mutex`·`Arc`
my_data는 여러 스레드가 갱신하거나 읽을 수 있는 공유 데이터이며, 복사본 하나만 유지하므로 상호 배제(mutual exclusion)를 통해 한 번에 하나의 주체만 접근하게 함.Mutex자체를 여러 스레드가 안전하게 공유할 수 없으므로, 원자적 참조 카운팅을 제공하는Arc로 감싸 잠금에 대한 공유 접근을 관리함.- 구조는 벡터 데이터 한 개, 이를 감싸는
Mutex한 개, 그리고 비교적 저렴한Arc여러 개로 구성됨. Arc문서에 따르면 원자적 연산은 일반 메모리 접근(Rc)보다 비용이 크지만,Arc사용 자체가 지나치게 비싼 것은 아님.
데이터 구성과 스레드 실행
- 예제는
[1, 2, 3]으로 벡터를 만들고,Mutex로 감싼 다음Arc로 감싸며, 변수 섀도잉(variable shadowing)으로 각 단계를 같은 이름에 다시 바인딩함. - 두 스레드를 생성하고 각각
Arc<Mutex<Vec<i32>>>를 복제한 뒤, 새 스레드로 이동시킴. 복제는Arc를 복제하는 것이며, 동일한Mutex와 벡터를 가리키는 참조를 제공함. - 새 스레드는 데이터 잠금 획득을 시도함. 즉시 얻으면 데이터를 수정하고, 얻지 못하면 접근 가능해질 때까지 차단됨.
- 첫 번째 스레드는 벡터의 각 값을 1씩 더하고, 잠금을 유지한 채 750밀리초 동안 대기함. 이후 잠금을 놓고 900밀리초 동안 별도 작업을 수행함.
- 두 번째 스레드는 각 값을 2배로 만들고, 잠금을 유지한 채 1,250밀리초 동안 대기함. 이후 잠금을 놓고 1,100밀리초 동안 별도 작업을 수행함.
스레드 종료 대기와 결과
- 두 스레드가 실행되는 동안 메인 스레드는 각 스레드에
.join()을 호출해 종료를 기다림. 둘 다 끝나기 전에는 다른 작업을 하지 않으므로.join()호출 순서는 중요하지 않음. .join()은 차단 작업이며, 두 스레드가 끝난 뒤 메인 스레드는 잠금을 얻고Result를 언래핑해 갱신된 데이터를 확인함..join()을 호출하지 않으면 스레드가 시작하기 전에 메인 스레드가 잠금을 얻을 가능성이 있고, 스레드가 실행될 기회가 생기기 전에 프로그램이 종료될 수 있음.- 두 스레드는 거의 동시에 시작하므로 어느 스레드가 먼저 실행될지는 정해져 있지 않음. 첫 번째 스레드가 먼저 처리되면 최종 값은
[4, 6, 8]이고, 두 번째 스레드가 먼저 처리되면[3, 5, 7]임.
임계 구역 밖의 작업
- 처음 예제는 두 스레드가 잠금을 얻고 작업을 흉내 낸 뒤 반환하는 구조라, 두 스레드는 사실상 차례로 실행되며 진정한 동시 실행은 이루어지지 않음.
- 실제 작업에서는 임계 구역에 들어가기 전 공유 자료 구조에 넣을 데이터를 준비하거나, 값을 갱신한 뒤 그 결과로 계산하는 등 잠금 밖에서도 작업할 수 있음.
- 스레드가
Mutex의 임계 구역 안에서만 작업하면 한 스레드가 잠금을 사용하는 동안 나머지 스레드는 차례를 기다리게 됨. - 코드가
Mutex의 임계 구역 안에서만 동작한다면 멀티스레딩이 적절한 선택이 아닐 수 있음.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요