불변성이 다중 프로세서 프로그래밍에서 잠금의 필요성을 완전히 제거합니까?


39

1 부

분명히 불변성 은 다중 프로세서 프로그래밍에서 잠금의 필요성을 최소화 하지만 불필요 함만으로는 불충분 한 경우가 있습니까? 대부분의 프로그램이 실제로 무언가를해야하기 (데이터 저장소 업데이트, 보고서 생성, 예외 발생 등)하기 전에 처리를 지연하고 상태를 캡슐화 할 수있는 것 같습니다. 이러한 조치를 항상 잠금없이 수행 할 수 있습니까? 원본을 변경하는 대신 각 객체를 버리고 새 객체를 만드는 단순한 조치 (불변성의 조잡한 관점)가 프로세스 간 경합으로부터 절대적인 보호를 제공합니까, 아니면 여전히 잠금이 필요한 코너 케이스가 있습니까?

저는 많은 기능 프로그래머와 수학자들이 "부작용 없음"에 대해 이야기하고 싶어하지만 "실제 세계"에서는 기계 명령을 실행하는 데 시간이 걸리더라도 모든 부작용이 있습니다. 나는 이론적 / 학술적 답변과 실용적 / 실제적인 답변 모두에 관심이 있습니다.

특정 범위 또는 가정을 고려할 때 불변성이 안전하다면 "안전 지대"의 경계가 정확히 무엇인지 알고 싶습니다. 가능한 경계의 예 :

  • I / O
  • 예외 / 오류
  • 다른 언어로 작성된 프로그램과의 상호 작용
  • 다른 머신과의 상호 작용 (물리적, 가상 또는 이론적)

이 질문을 시작한 그의 의견에 대해 @JimmaHoffa에게 감사드립니다 !

2 부

다중 프로세서 프로그래밍은 종종 일부 코드를 더 빠르게 실행하기 위해 최적화 기술로 사용됩니다. 잠금 대 불변 객체를 사용하는 것이 언제 더 빠릅니까?

Amdahl의 법칙에 명시된 한계를 감안할 때, 불변의 객체 대 변이의 잠금과 비교하여 가비지 컬렉터를 고려하거나 사용하지 않고 전반적인 성능을 언제 더 향상시킬 수 있습니까?

요약

이 두 질문을 하나로 결합하여 스레딩 문제에 대한 해결책으로 불변성에 대한 경계 상자의 위치를 ​​파악하려고합니다.


21
but everything has a side effect-아뇨. 어떤 값을 받아들이고 다른 값을 반환하고, 함수 외부의 것을 방해하지 않으며, 부작용이 없으므로 스레드로부터 안전합니다. 컴퓨터가 전기를 사용한다는 것은 중요하지 않습니다. 원한다면 메모리 셀을 때리는 우주 광선에 대해서도 이야기 할 수 있지만, 그 주장을 실용적으로 유지합시다. 함수가 실행되는 방식이 전력 소비에 어떤 영향을 미치는지 고려하고 싶다면 스레드 안전 프로그래밍과 다른 문제입니다.
Robert Harvey

5
@RobertHarvey-아마도 부작용의 다른 정의를 사용하고 있으며 대신 "실제 부작용"이라고 말했을 것입니다. 예, 수학자에게는 부작용없이 기능이 있습니다. 실제 머신에서 실행되는 코드는 데이터 변경 여부에 관계없이 머신 리소스를 실행합니다. 예제의 함수는 대부분의 머신 아키텍처에서 스택에 반환 값을 넣습니다.
GlenPeterson

1
당신이 실제로 그것을 통해 얻을 수있는 경우에, 나는 당신의 질문이 악명 높은 종이의 심장에 간다 생각 research.microsoft.com/en-us/um/people/simonpj/papers/...
지미 호파

6
논의의 목적으로 , 구현 세부 사항과 관련이없는 일종의 잘 정의 된 프로그래밍 언어를 실행하는 Turing-complete 시스템을 언급한다고 가정합니다 . 다시 말해서, 내가 선택한 프로그래밍 언어로 작성하는 함수가 언어의 범위 내에서 불변성을 보장 할 수 있다면 스택이 무엇을하고 있는지는 중요하지 않습니다 . 고급 언어로 프로그래밍 할 때 스택에 대해 생각하지 않아도됩니다.
Robert Harvey

1
@RobertHarvey 저술; Monads heh 그리고 당신은 첫 몇 페이지에서 그것을 모을 수 있습니다. 나는 전반적으로 부작용을 실질적으로 순수한 방법으로 다루는 기술을 자세히 설명하기 때문에 언급합니다 .Glen의 질문에 대답 할 것이라고 확신합니다. 더 읽을 거리.
Jimmy Hoffa

답변:


35

이것은 완전히 대답하면 정말로 광범위하게 표현되는 이상한 문구입니다. 나는 당신이 요구하는 특정 사항을 정리하는 데 중점을 둘 것입니다.

불변성은 디자인 트레이드 오프입니다. 일부 작업을 더 어렵게 만듭니다 (큰 개체의 상태를 빠르게 수정하고 개체를 단편적으로 작성하고 실행 상태를 유지하는 등) (더 쉬운 디버깅, 프로그램 동작에 대한 추론 용이, 작업시 변경되는 사항에 대해 걱정할 필요 없음) 동시에 등). 우리가이 질문에 관심을 갖는 것은 이것이 마지막이지만, 그것이 도구라는 점을 강조하고 싶습니다. 대부분의 최신 프로그램 에서 발생하는 것보다 더 많은 문제를 해결하는 좋은 도구 이지만 은색 총알은 아닙니다 ... 프로그램의 본질적인 동작을 변경하는 것은 아닙니다.

자, 당신은 무엇을 얻습니까? 불변성은 한 가지를 얻는다 : 당신은 불변의 객체가 당신의 밑에 변화하는 것에 대해 걱정하지 않고 자유롭게 읽을 수 있습니다 (진실하게 깊은 불변이라고 가정하면 ... 불변의 객체를 가진 불변의 객체를 갖는 것은 일반적으로 거래 차단기입니다). 그게 다야. 잠금, 스냅 샷, 데이터 파티셔닝 또는 기타 메커니즘을 통해 동시성을 관리 할 필요가 없습니다. 잠금에 대한 원래 질문의 초점은 ... 질문의 범위가 맞지 않습니다.

많은 것들이 객체를 읽는다는 것이 밝혀졌습니다. IO는하지만 IO 자체는 동시 사용 자체를 제대로 처리하지 않는 경향이 있습니다. 거의 모든 처리가 수행되지만 다른 객체는 변경 가능하거나 처리 자체가 동시성에 익숙하지 않은 상태를 사용할 수 있습니다. 일부 언어에서는 객체를 복사하는 것이 큰 문제가되지 않습니다. 전체 사본은 (거의) 절대적인 작업이 아니기 때문입니다. 이것은 불변의 객체가 당신을 돕는 곳입니다.

성능은 앱에 따라 다릅니다. 자물쇠는 (보통) 무겁습니다. 다른 동시성 관리 메커니즘은 더 빠르지 만 디자인에 큰 영향을 미칩니다. 일반적으로 변경 불가능한 객체를 사용하고 약점을 피하는 동시성이 높은 설계는 변경 가능한 객체를 잠그는 동시 설계보다 성능이 우수합니다. 당신의 프로그램이 약간 동시 적이라면 그것은 의존하고 그리고 / 또는 중요하지 않습니다.

그러나 성능이 가장 큰 관심사가되어서는 안됩니다. 동시 프로그램 작성은 어렵습니다 . 동시 프로그램 디버깅은 어렵다 . 불변 개체는 동시성 관리를 수동으로 구현할 때 오류가 발생하지 않도록하여 프로그램 품질을 향상시킵니다. 동시 프로그램에서 상태를 추적하지 않기 때문에 디버깅이 더 쉽습니다. 그것들은 당신의 디자인을 더 단순하게 만들어 버그를 제거합니다.

요약하면 불변성은 동시성을 올바르게 처리하는 데 필요한 문제를 해결하지만 제거하지는 않습니다. 그 도움은 보편적 인 경향이 있지만, 가장 큰 장점은 성능보다는 품질 관점에서 비롯됩니다. 그리고, 불변성으로 인해 앱에서 동시성을 관리하지 않아도됩니다. 죄송합니다.


+1 이것은 의미가 있지만, 매우 불변의 언어로 병행 성을 올바르게 처리하는 것에 대해 여전히 걱정해야하는 곳의 예를들 수 있습니까? 당신은 당신이 말하지만, 그러한 시나리오는 나에게 분명하지 않습니다
Jimmy Hoffa

@JimmyHoffa 불변 언어에서는 여전히 스레드 사이의 상태를 업데이트해야합니다. 내가 아는 가장 불변의 두 언어 (Clojure 및 Haskell)는 스레드간에 수정 된 상태를 전달하는 방법을 제공하는 참조 유형 (원자 및 Mvar)을 제공합니다. 참조 유형의 의미는 특정 유형의 동시성 오류를 방지하지만 다른 유형은 여전히 ​​가능합니다.
stonemetal

흥미로운 @stonemetal, Haskell과 함께한 4 개월 동안 나는 아직 Mvars에 대해 들어 보지 못했지만, 나는 항상 Erlang의 메시지 전달과 비슷하게 동작하는 동시성 상태 통신에 STM을 사용한다고 들었습니다. 필자가 생각할 수있는 동시 문제를 해결하지 못하는 불변성의 완벽한 예는 UI를 업데이트하는 것입니다. 다른 버전의 데이터로 UI를 업데이트하려고하는 2 개의 스레드가있는 경우 하나는 최신 버전이므로 두 번째 업데이트를 받아야하므로 어떻게해서 든 시퀀싱을 보장해야하는 경쟁 조건 .. 흥미로운 생각 .. 세부 사항에 감사드립니다
Jimmy Hoffa

1
@jimmyhoffa-가장 일반적인 예는 IO입니다. 언어가 변경되지 않더라도 데이터베이스 / 웹 사이트 / 파일은 변경되지 않습니다. 다른 하나는 일반적인지도 / 축소입니다. 불변성은 맵의 집계가 더 완벽하다는 것을 의미하지만 여전히 모든 맵이 병렬로 축소되면 조정을 처리해야합니다.
Telastyn

1
@JimmyHoffa : MVars는 저수준의 가변성 동시성 프리미티브 (기술적으로, 가변성 저장 위치에 대한 불변의 참조)이며 다른 언어에서 볼 수있는 것과 크게 다르지 않습니다. 교착 상태 및 경쟁 조건이 매우 가능합니다. STM은 교착 상태 나 경쟁 조건없이 컴포저 블 트랜잭션을 허용하는 잠금없는 변경 가능 공유 메모리 (메시지 전달과는 매우 다름)에 대한 고급 동시성 추상화입니다. 불변의 데이터는 스레드로부터 안전합니다.
CA McCann

13

어떤 값을 받아들이고 다른 값을 반환하고 함수 외부의 것을 방해하지 않고 부작용이 없으므로 스레드로부터 안전합니다. 함수가 실행되는 방식이 전력 소비에 어떤 영향을 미치는지 고려하고 싶다면 다른 문제입니다.

구현 세부 사항과 관련이없는 일종의 잘 정의 된 프로그래밍 언어를 실행하는 Turing-complete 시스템을 참조한다고 가정합니다. 다시 말해서, 내가 선택한 프로그래밍 언어로 작성하는 함수가 언어의 범위 내에서 불변성을 보장 할 수 있다면 스택이 무엇을하고 있는지는 중요하지 않습니다. 고급 언어로 프로그래밍 할 때 스택에 대해 생각하지 않아도됩니다.

이것이 어떻게 작동하는지 설명하기 위해 C #에서 몇 가지 간단한 예제를 제공 할 것입니다. 이러한 예가 사실이 되려면 몇 가지 가정을해야합니다. 첫째, 컴파일러는 오류없이 C # 사양을 따르고 둘째는 올바른 프로그램을 생성한다는 것입니다.

문자열 컬렉션을 허용하고 컬렉션의 모든 문자열을 쉼표로 구분하여 연결 한 문자열을 반환하는 간단한 함수를 원한다고 가정 해 봅시다. C #에서 단순하고 순진한 구현은 다음과 같습니다.

public string ConcatenateWithCommas(ImmutableList<string> list)
{
    string result = string.Empty;
    bool isFirst = false;

    foreach (string s in list)
    {
        if (isFirst)
            result += s;
        else
            result += ", " + s;
    }
    return result;
} 

이 예제는 변경할 수 없습니다. 어떻게 알 수 있습니까? string객체는 불변 이기 때문에 . 그러나 구현이 이상적이지 않습니다. result변경할 수 없기 때문에 루프를 통해 매번 새로운 문자열 객체를 작성하여 result가리키는 원래 객체를 교체해야 합니다. 이것은 여분의 스트링을 모두 청소해야하기 때문에 속도에 부정적인 영향을 미치고 가비지 컬렉터에 압력을 가할 수 있습니다.

이제 내가 이것을한다고 가정 해 봅시다.

public string ConcatenateWithCommas(ImmutableList<string> list)
{
    var result = new StringBuilder();
    bool isFirst = false;

    foreach (string s in list)
    {
        if (isFirst)
            result.Append(s);
        else
            result.Append(", " + s);
    }
    return result.ToString();
} 

string result변경 가능한 객체 () 로 교체되었습니다 StringBuilder. 이다 많은 새로운 문자열이 루프를 통해 매번 생성하지 않기 때문에, 빠른 첫 번째 예보다. 대신 StringBuilder 객체는 각 문자열의 문자를 문자 모음에 추가하고 모든 것을 마지막에 출력합니다.

StringBuilder가 변경 가능하더라도이 함수는 변경할 수 없습니까?

그렇습니다. 왜? 때문에 때마다이 함수가 호출 될 때, 새의 StringBuilder는 그냥 호출에 대해 생성됩니다. 이제 스레드 안전하지만 변경 가능한 구성 요소를 포함하는 순수한 함수가 있습니다.

하지만 내가 이걸하면 어쩌지?

public class Concatenate
{
    private StringBuilder result = new StringBuilder();
    bool isFirst = false;

    public string ConcatenateWithCommas(ImmutableList<string> list)
    {
        foreach (string s in list)
        {
            if (isFirst)
                result.Append(s);
            else
                result.Append(", " + s);
        }
        return result.ToString();
    } 
}

이 방법은 스레드 안전합니까? 아닙니다. 왜? 클래스가 내 메소드가 의존하는 상태를 유지하고 있기 때문입니다. 경쟁 조건이 이제 메소드에 존재합니다 : 하나의 스레드가 수정할 수 IsFirst있지만 다른 스레드가 첫 번째를 수행 할 수 있습니다 Append().이 경우 문자열의 시작 부분에 쉼표가 있어야합니다.

왜 이렇게 하시겠습니까? 글쎄, 나는 스레드가 result순서에 관계없이 또는 스레드가 들어오는 순서에 따라 문자열을 누적하기를 원할 수 있습니다 . 아마도 로거 일지 모릅니다.

어쨌든, 그것을 고치기 위해 lock메소드의 내부에 문장을 넣었습니다 .

public class Concatenate
{
    private StringBuilder result = new StringBuilder();
    bool isFirst = false;
    private static object locker = new object();

    public string AppendWithCommas(ImmutableList<string> list)
    {
        lock (locker)
        {
            foreach (string s in list)
            {
                if (isFirst)
                    result.Append(s);
                else
                    result.Append(", " + s);
            }
            return result.ToString();
        }
    } 
}

이제 다시 스레드 안전합니다.

내 불변의 메소드가 스레드로부터 안전하지 못할 수있는 유일한 방법은 메소드가 어떻게 구현의 일부를 누출하는 경우입니다. 이런 일이 일어날 수 있습니까? 컴파일러가 정확하고 프로그램이 올바른 경우에는 아닙니다. 그런 방법에 대한 잠금이 필요할까요? 아니.

동시성 시나리오에서 구현이 유출 될 수있는 방법에 대한 예는 여기를 참조하십시오 .


2
내가 실수하지 않는 한, a List가 변경 가능 하기 때문에 첫 번째 함수에서 '순수'라고 주장하면 다른 스레드가 목록에서 모든 요소를 ​​제거하거나 foreach 루프에있는 동안 더 많은 묶음을 추가 할 수 있습니다. 즉, 플레이 얼마나 확실하지 IEnumerator존재의 while(iter.MoveNext())편하지만은 않는 한 IEnumerator(의심) 불변 그는 foreach 루프를 때리기 위협 것이다.
Jimmy Hoffa

사실, 스레드가 컬렉션을 읽는 동안 컬렉션이 쓰여지지 않는다고 가정해야합니다. 메소드를 호출하는 각 스레드가 자체 목록을 작성하는 경우 올바른 가정이됩니다.
Robert Harvey

나는 그것이 참조로 사용하는 가변 객체가있을 때 그것을 '순수한'이라고 부를 수 있다고 생각하지 않습니다. 그것이 IEnumerable을받은 경우에 당신은 할 수 추가 또는는 IEnumerable에서 요소를 제거 할 수 없기 때문에 그 주장을 할 수 있지만, 그것은 배열 될 수 또는 IEnumerable을 계약이 어떠한 형태의 보증하지 않습니다 있도록 목록을 IEnumerable로 넘겨 순도. 이 함수를 순수하게 만드는 실제 기술은 패스 바이 복사로 불변성이 될 것입니다. C # 은이를 수행하지 않으므로 함수가받을 때 바로 List를 복사해야합니다. 하지만 그렇게하는 유일한 방법은 foreach를 사용하는 것입니다.
Jimmy Hoffa

1
@JimmyHoffa : 젠장,이 닭고기와 계란 문제에 집착하게 했어요! 어디서나 해결책이 있으면 알려주십시오.
Robert Harvey

1
방금이 답변을 보았습니다. 제가 접한 주제에 대한 최고의 설명 중 하나입니다. 예제는 매우 간결하며 실제로 이해하기 쉽습니다. 감사!
Stephen Byrne

4

귀하의 질문을 이해했는지 잘 모르겠습니다.

IMHO가 그렇습니다. 모든 객체가 변경 불가능한 경우 잠금이 필요하지 않습니다. 그러나 상태를 유지해야하는 경우 (예 : 데이터베이스를 구현하거나 여러 스레드에서 결과를 집계해야하는 경우) 가변성과 잠금도 사용해야합니다. 불변성은 잠금의 필요성을 제거하지만 일반적으로 완전히 불변의 응용 프로그램을 가질 여유가 없습니다.

파트 2에 대한 답변-잠금은 잠금이없는 것보다 항상 느려 야합니다.


3
두 번째 부분은 "잠금과 불변 구조물 사이의 성능 트레이드 오프는 무엇입니까?"입니다. 대답이 가능하다면 자체 질문을받을 가치가 있습니다.
Robert Harvey

4

불변 객체에 대한 단일 가변 참조로 관련 상태 무리를 캡슐화하면 패턴을 사용하여 많은 종류의 상태 수정을 잠금없이 수행 할 수 있습니다.

do
{
   oldState = someObject.State;
   newState = oldState.WithSomeChanges();
} while (Interlocked.CompareExchange(ref someObject.State, newState, oldState) != oldState;

두 스레드가 someObject.state동시에 업데이트하려고하면 두 개체가 모두 이전 상태를 읽고 서로의 변경없이 새 상태를 결정합니다. CompareExchange를 실행하는 첫 번째 스레드는 다음 상태에 대해 생각한 내용을 저장합니다. 두 번째 스레드는 상태가 더 이상 이전에 읽은 것과 일치하지 않으므로 첫 번째 스레드의 변경 사항이 적용된 시스템의 다음 다음 상태를 다시 계산합니다.

이 패턴은 길들인 스레드가 다른 스레드의 진행을 차단할 수 없다는 이점이 있습니다. 경쟁이 심할 때에도 일부 스레드가 항상 진행되고 있다는 이점이 있습니다. 그러나 경합이있는 경우 많은 스레드가 작업을 수행하는 데 많은 시간을 소비하여 결국 폐기 할 수 있다는 단점이 있습니다. 예를 들어, 별도의 CPU에있는 30 개의 스레드가 모두 동시에 객체를 변경하려고하면 첫 번째 시도, 1 초, 1 초, 3 초 등에서 성공하여 각 스레드가 평균적으로 약 15 회 시도합니다. 데이터를 업데이트합니다. "권고"잠금을 사용하면 상황을 크게 개선 할 수 있습니다. 스레드가 업데이트를 시도하기 전에 "경고"표시기가 설정되어 있는지 확인해야합니다. 그렇다면, 업데이트를 수행하기 전에 잠금을 획득해야합니다. 스레드가 업데이트 시도에 실패하면 경합 플래그를 설정해야합니다. 잠금을 획득하려고 시도하는 스레드가 대기중인 다른 사람이없는 것을 발견하면 경합 플래그를 지워야합니다. 여기서 "잠금"에는 잠금이 필요하지 않습니다. 코드가 없으면 코드가 올바르게 작동합니다. 잠금의 목적은 코드가 성공하지 못할 작업에 소요되는 시간을 최소화하는 것입니다.


4

당신은 시작

명백한 불변성은 다중 프로세서 프로그래밍에서 잠금의 필요성을 최소화

잘못된. 사용하는 모든 수업에 대한 설명서를주의해서 읽어야합니다. 예를 들어 C ++의 const std :: string은 스레드로부터 안전 하지 않습니다 . 불변 개체는 액세스 할 때 변경되는 내부 상태를 가질 수 있습니다.

그러나 당신은 이것을 완전히 잘못된 관점에서보고 있습니다. 객체가 불변인지 아닌지는 중요하지 않으며, 중요한 것은 변경 여부입니다. "운전 면허 시험을 치르지 않으면 음주 운전 면허증을 잃을 수 없습니다"라고 말하는 것과 같습니다. 사실이지만 요점을 놓치고 있습니다.

이제 예제 코드에서 누군가 "ConcatenateWithCommas"라는 함수로 작성했습니다. 입력이 변경 가능하고 잠금을 사용하면 무엇을 얻을 수 있습니까? 문자열을 연결하려고 시도하는 동안 다른 사람이 목록을 수정하려고하면 잠금으로 인해 충돌이 발생하지 않을 수 있습니다. 그러나 다른 스레드가 변경하기 전이나 후에 문자열을 연결할지 여부는 여전히 알 수 없습니다. 따라서 결과는 다소 쓸모가 없습니다. 잠금과 관련이 없으며 잠금으로 해결할 수없는 문제가 있습니다. 그러나 불변의 객체를 사용하고 다른 스레드가 전체 객체를 새로운 객체로 교체하면 새 객체가 아닌 이전 객체를 사용하므로 결과가 쓸모가 없습니다. 실제 기능 수준에서 이러한 문제에 대해 생각해야합니다.


2
const std::string나쁜 예와 약간의 청어입니다. C ++ 문자열은 변경 가능하며 const불변성을 보장 할 수 없습니다. const함수 만 호출 될 수 있다고 말하면됩니다 . 그러나 이러한 기능은 여전히 ​​내부 상태를 변경하여 const버릴 수 있습니다. 마지막으로, 다른 언어와 동일한 문제가 있습니다. 단지 참조가 귀하의 참조도 마찬가지 const라는 의미는 아닙니다 . 아니요, 진정한 불변 데이터 구조를 사용해야합니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.