비 기능 언어에서 영구 데이터 구조 사용


17

순전히 기능적이거나 거의 순전히 기능적인 언어는 불변적이고 상태 비 저장 (stateless) 스타일의 함수형 프로그래밍에 잘 맞기 때문에 지속적인 데이터 구조의 이점을 얻습니다.

그러나 때때로 우리는 Java와 같은 (상태 기반, OOP) 언어에 대한 영구 데이터 구조 라이브러리를 볼 수 있습니다. 지속적인 데이터 구조를 선호하는 주장은 종종 불변이기 때문에 스레드로부터 안전하다는 주장 입니다.

그러나 영구 데이터 구조가 스레드로부터 안전하다는 이유는 하나의 스레드가 영구 컬렉션에 요소를 "추가"하는 경우 작업 은 원본과 같지만 요소가 추가 된 컬렉션을 반환하기 때문 입니다. 따라서 다른 스레드는 원본 컬렉션을 참조하십시오. 두 컬렉션은 물론 많은 내부 상태를 공유하므로 이러한 지속적인 구조가 효율적입니다.

그러나 스레드마다 데이터의 상태가 다르기 때문에 한 스레드가 다른 스레드에서 볼 수있는 변경을 수행하는 시나리오를 처리하는 데 영구 데이터 구조 자체로는 충분 하지 않은 것 같습니다 . 이를 위해서는 원자, 참조, 소프트웨어 트랜잭션 메모리 또는 클래식 잠금 및 동기화 메커니즘과 같은 장치를 사용해야합니다.

그렇다면 왜 PDS의 불변성이 "스레드 안전성"에 유리한 것으로 선전됩니까? PDS가 동기화 또는 동시성 문제 해결에 도움이되는 실제 사례가 있습니까? 또는 PDS는 단순히 함수형 프로그래밍 스타일을 지원하는 객체에 상태 비 저장 인터페이스를 제공하는 방법입니까?


3
당신은 "지속적"이라고 계속 말합니다. "프로그램을 다시 시작해도 살아남을 수있다"또는 "생성 후 변경되지 않음"과 같이 "불변"인 것처럼 "지속적"을 의미합니까?
Kilian Foth

17
@KilianFoth 영구 데이터 구조는 잘 정의정의를 가지고 있습니다 . "영구 데이터 구조는 수정 될 때 항상 이전 버전 자체를 유지하는 데이터 구조입니다". 따라서 "프로그램을 다시 시작해도 살아남을 수있는 것"과 같이 지속성이 아니라 새로운 구조를 기반으로 새 구조를 만들 때 이전 구조를 재사용하는 것입니다.
Michał Kosmulski 2016 년

3
귀하의 질문은 비 기능적 언어에서 영구 데이터 구조를 사용하는 것과 패러다임에 관계없이 동시성 및 병렬 처리 부분이 해결 하지 못하는 부분에 관한 것입니다.

내 실수. "지속적인 데이터 구조"가 단순한 지속성과는 다른 기술적 용어라는 것을 몰랐습니다.
Kilian Foth

@delnan 네 맞습니다.
Ray Toal 2016 년

답변:


15

지속적 / 불변 데이터 구조는 자체적으로 동시성 문제를 해결하지는 않지만 훨씬 쉽게 해결할 수 있습니다.

세트 S를 다른 스레드 T2로 전달하는 스레드 T1을 고려하십시오. S가 변경 가능하면 T1에 문제가 있습니다 .S에서 발생하는 제어력을 잃습니다. 스레드 T2가이를 수정할 수 있으므로 T1이 S의 내용에 전혀 의존 할 수 없으며 그 반대도 마찬가지입니다. -T2는 T1을 확신 할 수 없습니다 T2가 작동하는 동안 S를 수정하지 않습니다.

하나의 솔루션은 스레드 중 하나만 S를 수정할 수 있도록 T1 및 T2의 통신에 일종의 계약을 추가하는 것입니다. 이는 오류가 발생하기 쉽고 설계 및 구현에 부담이됩니다.

또 다른 해결책은 T1 또는 T2가 데이터 구조 (또는 조정되지 않은 경우 둘 다)를 복제하는 것입니다. 그러나 S가 지속적이지 않으면 비싼 O (n) 연산입니다.

영구적 인 데이터 구조가 있다면이 부담이 없습니다. 구조를 다른 스레드로 전달할 수 있으며 그 구조로 신경 쓰지 않아도됩니다. 두 스레드 모두 원래 버전에 액세스 할 수 있으며 임의의 작업을 수행 할 수 있습니다. 다른 스레드가 보는 내용에는 영향을 미치지 않습니다.

영구 데이터 구조와 불변 데이터 구조 도 참조하십시오 .


2
아, 따라서이 문맥에서 "스레드 안전"은 단지 하나의 스레드가 다른 스레드가 자신이 보는 데이터를 파괴하는 것에 대해 걱정할 필요는 없지만 동기화 및 스레드간에 공유 하고자 하는 데이터를 처리하는 것과는 아무런 관련이 없음을 의미 합니다. 그것은 내가 생각한 것과 일치하지만, "자신의 동시성 문제를 해결하지 마십시오."
Ray Toal 2016 년

2
@RayToal 예,이 문맥에서 "스레드 안전"은 정확히 의미합니다. 스레드간에 데이터를 공유 하는 방법 은 다른 문제이며, 언급 한 바와 같이 많은 솔루션이 있습니다 (개인적으로는 STM을 사용하여 작곡 가능성을 좋아합니다). 스레드 안전성을 통해 공유 후 데이터가 어떻게되는지 걱정할 필요가 없습니다. 스레드는 데이터 구조에서 작업하는 사람과시기를 동기화 할 필요가 없기 때문에 실제로 큰 문제입니다.
Petr Pudlák 2016 년

@RayToal 이것은 액터 와 같은 우아한 동시성 모델을 허용 하여 개발자가 명시적인 잠금 및 스레드 관리를 처리 할 필요가없고 메시지의 불변성 을 의존 하지 않도록합니다. 전달 된 액터.
Petr Pudlák 2016 년

고마워 Petr, 나는 배우에게 또 다른 모습을 줄 것이다. 나는 모든 Clojure 메커니즘에 익숙하며 Rich Hickey 는 적어도 Erlang에서 예시 한 것처럼 액터 모델을 사용하지 않기로 명시 적으로 언급했습니다 . 그래도 더 많이 알수록 좋습니다.
Ray Toal 2016 년

@RayToal 재미있는 링크입니다. 감사합니다. 액터를 예제로만 사용했지만 최고의 솔루션이라고 말하는 것은 아닙니다. Clojure를 사용하지는 않았지만 STM이 선호되는 솔루션 인 것 같습니다. 액터보다 확실히 선호합니다. STM은 또한 지속성 / 불변성에 의존합니다. 데이터 구조를 취소 할 수없이 수정하면 트랜잭션을 다시 시작할 수 없습니다.
Petr Pudlák 2016 년

5

그렇다면 왜 PDS의 불변성이 "스레드 안전성"에 유리한 것으로 선전됩니까? PDS가 동기화 또는 동시성 문제 해결에 도움이되는 실제 사례가 있습니까?

이 경우 PDS의 주요 이점은 모든 것을 독창적으로 만들지 않고도 (모든 것을 깊게 복사하지 않고) 데이터의 일부를 수정할 수 있다는 것입니다. 복사 및 붙여 넣은 데이터 인스턴스화, 사소한 실행 취소 시스템, 게임의 사소한 재생 기능, 사소한 비파괴 편집, 사소한 예외 안전 등 부작용없이 저렴한 기능을 쓸 수있게하는 것 외에도 많은 잠재적 인 이점이 있습니다.


2

영구적이지만 변경 가능한 데이터 구조를 상상할 수 있습니다. 예를 들어, 첫 번째 노드에 대한 포인터로 표시되는 링크 된 목록과 새 헤드 노드 + 이전 목록으로 구성된 새 목록을 리턴하는 선행 조작을 사용할 수 있습니다. 여전히 이전 헤드에 대한 참조가 있으므로이 목록에 액세스하고 수정할 수 있으며,이 목록은 새 목록에도 포함됩니다. 가능하지만 이러한 패러다임은 지속적이고 불변의 데이터 구조의 이점을 제공하지 않습니다. 예를 들어 기본적으로 스레드 안전하지 않습니다. 그러나 개발자가 공간 효율성과 같이 자신이하는 일을 알고있는 한 사용이 가능할 수 있습니다. 또한 언어 수준에서 구조를 변경할 수는 있지만 코드가 코드를 수정하는 것을 방해하는 요소는 없습니다.

불변성없이 (언어 또는 규칙에 의해 시행되는) 긴 스토리 짧게 지속성 데이터 구조는 이점 (스레드 안전성)을 잃지 만 다른 것은 아닙니다 (일부 시나리오의 공간 효율성).

비 기능적 언어의 예와 같이 Java String.substring()는 영구 데이터 구조라고 부르는 것을 사용합니다. 문자열은 문자 배열과 실제로 사용되는 배열 범위의 시작 및 끝 오프셋으로 표시됩니다. 부분 문자열이 작성되면 새 객체는 시작 및 종료 오프셋이 수정 된 경우에만 동일한 문자 배열을 재사용합니다. String불변 이기 때문에 (substring() 다른 것들이 아니라 연산 ) 불변의 영구 데이터 구조입니다.

데이터 구조의 불변성은 스레드 안전성과 관련된 부분입니다. 그들의 지속성 (새로운 구조가 생성 될 때 기존 청크의 재사용)은 그러한 콜렉션으로 작업 할 때 효율성과 관련이 있습니다. 항목을 추가하는 것과 같은 작업은 변경할 수 없으므로 기존 구조는 수정하지 않지만 추가 요소가 추가 된 새 구조를 반환합니다. 빈 컬렉션으로 시작하여 1000 요소 컬렉션으로 끝나기 위해 1000 요소를 하나씩 추가하면 전체 구조가 복사 될 때마다 0 + 1 + 2 + ... + 999 = 총 500000 개의 요소는 엄청난 낭비입니다. 영구 데이터 구조를 사용하면 1 요소 컬렉션이 2 요소 요소에서 재사용되고 3 요소 요소 등에서 재사용되므로 피할 수 있습니다.


상태의 한 측면을 제외한 모든 상태가 불변 인 준 불변의 객체를 갖는 것이 유용한 경우가 있습니다. 상태가 주어진 객체와 거의 같은 객체를 만드는 능력. 예를 들어, AppendOnlyList<T>2 개로 증가하는 어레이 의 백업은 각 스냅 샷에 대한 데이터를 복사 할 필요없이 불변 스냅 샷을 생성 할 수 있지만, 스냅 샷의 내용이 포함 된 목록과 재 복사없이 새로운 항목을 생성 할 수는 없습니다. 새로운 배열에 대한 모든 것.
supercat

0

나는 언어와 그 성격, 그리고 내 도메인뿐만 아니라 언어를 사용하는 방식에 따라 C ++로 그러한 개념을 적용하는 것으로 편견이 있습니다. 그러나 이런 것들이 주어지면 불변의 디자인 이라고 생각 합니다 때, 스레드 안전성, 시스템에 대한 추론의 용이성, 함수에 대한 더 많은 재사용 찾기 (및 우리가 할 수있는 발견)와 같은 기능적 프로그래밍과 관련된 많은 이점을 얻을 때 은 가장 흥미로운 측면 합니다. 불쾌한 놀라움없이 순서대로 결합하십시오) 등

이 간단한 C ++ 예제를 보자 (간단히 이미지 처리 전문가 앞에서 당황하지 않도록 단순성에 최적화되지는 않음) :

// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
     Image dst(new_w, new_h);
     for (int y=0; y < new_h; ++y)
     {
         for (int x=0; x < new_w; ++x)
              dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
     }
     return dst;
}

이 함수의 구현은 두 개의 카운터 변수와 출력을위한 임시 로컬 이미지 형태로 로컬 (및 임시) 상태를 변경하지만 외부 부작용은 없습니다. 이미지를 입력하고 새로운 이미지를 출력합니다. 우리는 그것을 마음의 내용에 멀티 스레딩 할 수 있습니다. 추론하기 쉽고 철저한 테스트가 용이합니다. 무언가가 던져지면 새로운 이미지가 자동으로 버려지고 외부 부작용을 롤백 할 염려가 없기 때문에 예외가 안전합니다 (외부 이미지는 함수 범위 밖에서 수정되지 않습니다).

Image위의 함수를 구현하기가 더 어려워지고 아마도 덜 효율적으로 만드는 것을 제외하고는 위의 컨텍스트에서 C ++로 불변 으로 만들면 거의 얻지 못할 가능성이 거의 없습니다.

청정

따라서 외부 기능이없는 순수한 기능 은 매우 흥미 롭습니다. C ++에서도 팀원에게 자주 선호하는 것이 중요합니다. 그러나 문맥과 뉘앙스가 거의없는 상태에서 적용되는 불변의 디자인은 언어의 명령 적 특성을 고려할 때 효율적으로 (일부 두 가지 임시 객체를 변경하는 것이 유용하고 실용적이기 때문에 거의 흥미롭지 않습니다. 순수한 기능을 구현하는 개발자 및 하드웨어).

무거운 구조의 저렴한 복사

내가 찾은 두 번째로 가장 유용한 속성은 엄격한 입력 / 출력 특성으로 함수를 순수하게 만들기 위해 종종 발생하는 것과 같이 비용이 많이 드는 데이터 구조를 저렴하게 복사하는 능력입니다. 이것들은 스택에 맞는 작은 구조가 아닙니다. 그것들 Scene은 비디오 게임 의 전체와 같이 크고 무겁 습니다.

이 경우 복사 오버 헤드는 효과적인 병렬 처리의 기회를 방지 할 수 있습니다. 물리가 렌더러가 동시에 그리려고하는 장면을 변경하면서 동시에 물리를 깊게하는 경우 서로를 잠그고 병목 현상없이 효과적으로 병렬화하고 렌더링하는 것이 어려울 수 있기 때문입니다. 물리를 적용한 상태에서 하나의 프레임을 출력하기 위해 전체 게임 씬을 복사하면 효과가 동일하지 않을 수 있습니다. 그러나 물리 시스템이 단순히 장면을 입력하고 물리가 적용된 새로운 장면을 출력한다는 의미에서 '순수한'경우, 그러한 순도는 천문학적 복사 오버 헤드 비용으로 오지 않았지만 안전하게 하나는 다른 쪽을 기다리지 않고 렌더러입니다.

따라서 응용 프로그램 상태의 엄청나게 많은 데이터를 저렴하게 복사하고 최소한의 처리 비용과 메모리 사용으로 새로운 수정 버전을 출력하는 기능은 순도 및 효과적인 병렬 처리를위한 새로운 문을 열 수 있습니다. 지속적인 데이터 구조가 구현되는 방식. 그러나 우리가 그러한 교훈을 사용하여 만든 것은 완전히 영구적이거나 불변의 인터페이스를 제공 할 필요가 없습니다 (예를 들어, 복사시 복사 또는 "빌더 / 일시적"을 사용할 수 있음). 함수 / 시스템 / 파이프 라인의 병렬성과 순도 추구에서 메모리 사용과 메모리 액세스를 두 배로 늘리지 않고 복사본의 일부만 복사하고 수정합니다.

불변성

마지막으로 불변성은이 세 가지 중 가장 흥미롭지 않다고 생각하지만 특정 객체 디자인이 순수한 기능에 대한 로컬 임시로 사용되지 않고 넓은 맥락에서 귀중한 경우 철제 주먹으로 시행 할 수 있습니다 모든 방법에서 더 이상 외부 부작용을 일으키지 않기 때문에 "개체 수준 순도"의 종류 (더 이상 메서드의 로컬 범위를 벗어나는 멤버 변수를 더 이상 변경하지 않음).

그리고 C ++과 같은 언어 에서이 세 가지 중에서 가장 흥미 롭지 않다고 생각하지만 사소한 객체의 테스트 및 스레드 안전성 및 추론을 확실히 단순화 할 수 있습니다. 예를 들어 생성자 외부에서 객체에 고유 한 상태 조합을 제공 할 수 없으며, constness와 read- 원래 내용이 변경되지 않도록 (반드시 언어 내에서 가능한 한 많이) 반복자와 핸들 등을 보장합니다.

그러나 나는 이것이 가장 흥미로운 속성이라고 생각한다. 왜냐하면 내가보기에 대부분의 객체가 일시적으로, 가변적 인 형태로 사용되어 순수한 기능 (또는 "순수 시스템"과 같은 더 넓은 개념, 객체 또는 일련의 단순히 무언가를 입력하고 다른 것을 건드리지 않고 새로운 것을 출력하는 궁극적 인 효과를 갖는 기능), 나는 매우 명령적인 언어로 사지에 불변성을 가져 오는 것이 오히려 반 생산적인 목표라고 생각합니다. 실제로 가장 도움이되는 코드베이스 부분에는 거의 적용하지 않았습니다.

드디어:

[...] 영구적 인 데이터 구조 자체는 한 스레드가 다른 스레드에서 볼 수있는 변경을 수행하는 시나리오를 처리하기에 충분하지 않은 것 같습니다. 이를 위해서는 원자, 참조, 소프트웨어 트랜잭션 메모리 또는 클래식 잠금 및 동기화 메커니즘과 같은 장치를 사용해야합니다.

당연히 디자인이 여러 스레드에서 발생하는 수정 (사용자 엔드 디자인 의미)을 동시에 요구하면 동기화 또는 최소한 드로잉 보드로 돌아가서이를 처리 할 수있는 정교한 방법 ( 함수 프로그래밍에서 이러한 종류의 문제를 다루는 전문가가 사용하는 매우 정교한 예제를 보았습니다.)

그러나 일단 일종의 복사 및 일부 수정 된 무거운 구조를 출력하는 기능이 더러워지면 저렴하다는 것을 알았습니다. 예를 들어 지속적인 데이터 구조를 사용하면 종종 많은 문과 기회가 열립니다. 엄격한 I / O 종류의 병렬 파이프 라인에서 서로 독립적으로 실행될 수있는 코드를 병렬화하기 전에는 생각하지 않았습니다. 알고리즘의 일부가 본질적으로 직렬화되어야하더라도, 단일 스레드에 대한 처리를 연기 할 수 있지만 이러한 개념에 기대어 있으면 쉽게 걱정할 필요없이 무거운 작업의 90 %를 병렬화 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.