Java 및 C #에서 메모리 안전성을 제공하는 방식과 유사한 프로그래밍 언어로 스레드 안전성을 어떻게 제공 할 수 있습니까?


10

Java 및 C #은 배열 범위 및 포인터 역 참조를 확인하여 메모리 안전성을 제공합니다.

경쟁 조건 및 교착 상태의 가능성을 방지하기 위해 프로그래밍 언어로 어떤 메커니즘을 구현할 수 있습니까?


3
당신은 녹가하는 일에 관심이있을 수 있습니다 두려워 동시성 녹
빈센트 Savard을

2
모든 것을 불변으로 만들거나 안전한 채널로 모든 것을 비 동기화하십시오. GoErlang에 관심이있을 수도 있습니다 .
치료

@Theraot "안전한 채널과 모든 것을 비 동기화"-정교하게 설명해주십시오.
mrpyo

2
@mrpyo 프로세스 또는 스레드를 노출시키지 않으며 모든 호출이 약속되며 모든 것이 동시에 실행됩니다 (런타임이 실행을 예약하고 필요에 따라 시스템 스레드를 생성 / 풀링). 상태를 보호하는 논리는 메커니즘에 있습니다. 정보를 전달합니다 ... 런타임은 스케줄링에 의해 자동으로 직렬화 될 수 있으며, 더 뉘앙스 동작, 특히 생산자 / 소비자 및 집계가 필요한 스레드 안전 솔루션을 갖춘 표준 라이브러리가 있습니다.
Theraot

2
그건 그렇고, 또 다른 가능한 접근 방식이 있습니다 : 트랜잭션 메모리 .
Theraot

답변:


14

레이스는 객체의 앨리어싱이 동시에 발생하고 하나 이상의 앨리어스가 변경되는 경우 발생합니다.

따라서 경쟁을 방지하려면 이러한 조건 중 하나 이상을 적용하지 않아야합니다.

다양한 접근 방식이 다양한 측면을 다룹니다. 함수형 프로그래밍은 불변성을 강조하여 불변성을 제거합니다. 잠금 / 원자는 동시성을 제거합니다. Affine 유형은 앨리어싱을 제거합니다 (변경 가능한 앨리어싱을 제거해야 함). 액터 모델은 일반적으로 앨리어싱을 제거합니다.

앨리어싱 할 수있는 개체를 제한하여 위의 조건을 피할 수 있습니다. 여기에서 채널 및 / 또는 메시지 전달 스타일이 나타납니다. 임의의 메모리를 별칭으로 지정할 수 없으며, 채널이나 대기열의 끝 부분에 레이스가 없어야합니다. 일반적으로 잠금 또는 원자와 같은 동시성을 피합니다.

이러한 다양한 메커니즘의 단점은 작성할 수있는 프로그램을 제한한다는 것입니다. 제한이 많을수록 프로그램이 줄어 듭니다. 따라서 앨리어싱이나 변경 작업이 없으며 추론하기 쉽지만 매우 제한적입니다.

그 때문에 Rust가 그러한 약동을 일으키고 있습니다. 앨리어싱과 변경 가능성을 지원하지만 동시에 발생하지 않는지 확인하는 컴파일러는 엔지니어링 언어입니다 (학술 언어와 반대). 이상적이지는 않지만 많은 이전 프로그램보다 더 큰 클래스의 프로그램을 안전하게 작성할 수 있습니다.


11

Java 및 C #은 배열 범위 및 포인터 역 참조를 확인하여 메모리 안전성을 제공합니다.

C #과 Java가이를 수행하는 방법에 대해 먼저 생각하는 것이 중요합니다. C 또는 C ++에서 정의되지 않은 동작을 정의 된 동작 으로 변환 하여이를 수행합니다 . 프로그램 충돌 . Null 역 참조 및 배열 인덱스 예외는 올바른 C # 또는 Java 프로그램에서 발견 되지 않아야 합니다. 프로그램에는 버그가 없어야하기 때문에 처음부터 발생하지 않아야합니다.

그러나 그것은 당신의 질문에 의해 당신이 의미하는 것이 아니라고 생각합니다! 우리는 서로 상호 대기하는 n 개의 스레드가 있는지 확인하고 그 경우 프로그램을 종료시키는 "데드락 안전"런타임을 쉽게 작성할 수 있지만, 그것이 당신을 만족시킬 것이라고 생각하지는 않습니다.

경쟁 조건 및 교착 상태의 가능성을 방지하기 위해 프로그래밍 언어로 어떤 메커니즘을 구현할 수 있습니까?

우리가 귀하의 질문에 직면하는 다음 문제는 교착 상태와 달리 "경주 조건"을 감지하기 어렵다는 것입니다. 스레드 안전에서 우리가 추구하는 것은 레이스를 제거 하지 않는다는 것을 기억하십시오 . 우리가 쫓는 것은 레이스에서 누가이기 든 프로그램을 올바르게 만드는 것입니다 ! 경쟁 조건의 문제는 두 개의 스레드가 정의되지 않은 순서로 실행되고 있으며 누가 먼저 완료할지 알 수 없다는 것입니다. 경쟁 조건의 문제점은 개발자가 일부 스레드 마무리 작업이 가능하다는 사실을 잊고 그 가능성을 설명하지 못한다는 것입니다.

따라서 귀하의 질문은 기본적으로 "프로그래밍 언어가 내 프로그램이 올바른지 확인할 수있는 방법이 있습니까?"로 요약됩니다. 그 질문에 대한 답은 실제로는 아닙니다.

지금까지 나는 당신의 질문을 비판했습니다. 여기서 기어를 바꾸고 질문의 정신을 다루겠습니다. 멀티 스레딩으로 인해 끔찍한 상황을 완화하기 위해 언어 디자이너가 선택할 수있는 선택이 있습니까?

상황은 정말 끔찍합니다! 특히 약한 메모리 모델 아키텍처에서 멀티 스레드 코드를 올바르게 얻는 것은 매우 어렵습니다. 어려운 이유에 대해 생각하는 것이 유익합니다.

  • 하나의 프로세스에서 여러 제어 스레드는 추론하기 어렵습니다. 하나의 실로 충분합니다!
  • 다중 스레드 환경에서는 추상화가 매우 새어 나옵니다. 단일 스레드 환경에서는 프로그램이 실제로 순서대로 실행되지 않더라도 프로그램이 순서대로 실행되는 것처럼 동작합니다. 멀티 스레드 세계에서는 더 이상 그렇지 않습니다. 단일 스레드에서 보이지 않는 최적화가 표시되므로 이제 개발자는 가능한 최적화를 이해해야합니다.
  • 그러나 악화됩니다. C # 사양에서는 구현이 모든 스레드에서 동의 할 수있는 일관된 읽기 및 쓰기 순서를 가질 필요는 없다고 말합니다 . "인종"이 존재하고 확실한 승자가 있다는 생각은 사실이 아닙니다! 많은 스레드에서 일부 변수에 대해 두 번의 쓰기와 두 번의 읽기가있는 상황을 고려하십시오. 현명한 세상에서 우리는 "글쎄, 우리는 누가 레이스에서 이길 지 알 수 없지만 적어도 레이스가있을 것이고 누군가는 이길 것"이라고 생각할 것입니다. 우리는 그 합리적인 세상에 있지 않습니다. C #에서는 여러 스레드가 읽기 및 쓰기 순서에 대해 동의하지 않을 수 있습니다 . 모두가 지켜보고있는 일관된 세계가있는 것은 아닙니다.

언어 디자이너가 일을 개선 할 수있는 확실한 방법이 있습니다. 최신 프로세서의 성능을 포기하십시오 . 멀티 스레드 프로그램이라도 모든 프로그램이 매우 강력한 메모리 모델을 갖도록하십시오. 이것은 멀티 스레드 프로그램을 여러 번 더 느리게 만들게되는데, 이는 멀티 스레드 프로그램이 처음에있는 이유와는 반대로 성능 향상을 위해 직접 작동합니다.

메모리 모델을 떠나도 멀티 스레딩이 어려운 다른 이유가 있습니다.

  • 교착 상태를 방지하려면 전체 프로그램 분석이 필요합니다. 프로그램이 다른 조직에 의해 서로 다른 시간에 작성된 구성 요소로 구성되어 있어도 잠금을 해제 할 수있는 전체 순서를 알고 전체 프로그램에서 해당 순서를 적용해야합니다.
  • 멀티 스레딩을 길들이기위한 주요 도구는 잠금이지만 잠금은 구성 할 수 없습니다 .

그 마지막 요점에 대한 자세한 설명이 있습니다. "구성 가능"이란 다음을 의미합니다.

double이 주어진 int를 계산한다고 가정 해보십시오. 올바른 계산 구현을 작성합니다.

int F(double x) { correct implementation here }

int가 주어진 문자열을 계산한다고 가정 해보십시오.

string G(int y) { correct implementation here }

이제 double이 주어진 문자열을 계산하려면 다음을 수행하십시오.

double d = whatever;
string r = G(F(d));

G와 F는 더 복잡한 문제에 대한 올바른 솔루션 으로 구성 될 수 있습니다 .

그러나 교착 상태로 인해 잠금에이 속성이 없습니다. L1, L2 순서로 잠금을 수행하는 올바른 방법 M1 및 L2, L1 순서로 잠금을 수행하는 올바른 방법 M2를 모두 잘못된 프로그램을 작성하지 않고 동일한 프로그램에서 사용할 수 없습니다. 잠금은 "모든 개별 방법이 정확하므로 모든 것이 정확하다"고 말할 수 없도록합니다.

그렇다면 언어 디자이너로서 무엇을 할 수 있습니까?

먼저 가지 마세요. 하나의 프로그램에서 여러 개의 제어 스레드를 사용하는 것은 좋지 않으며 스레드간에 메모리를 공유하는 것은 나쁜 생각이므로 먼저 언어 나 런타임에 넣지 마십시오.

이것은 분명히 스타터가 아닙니다.

더 근본적인 질문으로 우리의 관심을 돌입시다. 왜 우리는 처음에 여러 스레드가 있습니까? 두 가지 주된 이유가 있으며, 비록 매우 다르지만 동일한 문제로 자주 뭉쳐집니다. 대기 시간 관리와 관련되어 있기 때문에 혼란스러워합니다.

  • IO 대기 시간을 관리하기 위해 스레드를 잘못 만듭니다. 큰 파일을 작성하고, 원격 데이터베이스에 액세스하거나, UI 스레드를 잠그지 않고 작업자 스레드를 작성해야합니다.

나쁜 생각. 대신 코 루틴을 통해 단일 스레드 비동기를 사용하십시오. C #은 이것을 아름답게합니다. Java는 잘되지 않습니다. 그러나 이것이 현재 언어 디자이너가 스레딩 문제를 해결하는 데 도움을주는 주요 방법입니다. awaitC # 의 연산자 (F # 비동기 워크 플로 및 기타 선행 기술에서 영감을 받음)는 점점 더 많은 언어로 통합되고 있습니다.

  • 계산이 많은 작업으로 유휴 CPU를 포화시키기 위해 스레드를 올바르게 만듭니다. 기본적으로 스레드를 경량 프로세스로 사용합니다.

언어 디자이너는 병렬 처리에 적합한 언어 기능을 만들어 도움을 줄 수 있습니다. 예를 들어 LINQ가 어떻게 자연스럽게 PLINQ로 확장되는지 생각해보십시오. 당신이 현명한 사람이고 TPL 작업을 매우 병렬적이고 메모리를 공유하지 않는 CPU 바운드 작업으로 제한하면 여기서 큰 승리를 얻을 수 있습니다.

우리는 무엇을 더 할 수 있습니까?

  • 컴파일러가 가장 치명적인 실수를 감지하여 경고 나 오류로 바꿉니다.

C #에서는 교착 상태에 대한 레시피이므로 잠금 상태에서 기다릴 수 없습니다. C #에서는 항상 잘못된 행동을하기 때문에 값 유형을 고정 할 수 없습니다. 값이 아닌 상자를 잠그십시오. 별칭이 습득 / 릴리스 의미를 부과하지 않기 때문에 C #에서 휘발성 별칭을 지정하면 경고합니다. 컴파일러가 일반적인 문제를 감지하고 방지 할 수있는 더 많은 방법이 있습니다.

  • 가장 자연스러운 방법이 가장 올바른 방법 인 "품질의 구덩이"기능을 디자인하십시오.

C #과 Java는 참조 객체를 모니터로 사용할 수있게하여 큰 디자인 오류를 일으켰습니다. 따라서 교착 상태를 추적하기가 어려워지고 정적으로 방지하기가 어려워지는 모든 종류의 나쁜 습관이 권장됩니다. 그리고 모든 객체 헤더에서 바이트를 낭비합니다. 모니터는 모니터 클래스에서 파생되어야합니다.

  • 많은 양의 Microsoft Research 시간과 노력이 소프트웨어 트랜잭션 메모리를 C #과 같은 언어에 추가하려는 시도로 이루어졌으며, 주요 언어로 통합하기에 충분한 성능을 얻지 못했습니다.

STM은 훌륭한 아이디어이며, Haskell에서 장난감 구현을 가지고 놀았습니다. 잠금 기반 솔루션보다 올바른 부품으로 올바른 솔루션을 훨씬 더 우아하게 작성할 수 있습니다. 그러나 나는 왜 대규모로 작동하지 못했는지에 대한 세부 사항에 대해 충분히 알지 못합니다. 다음에 조 더피에게 물어봐

  • 또 다른 대답은 이미 불변성을 언급했습니다. 불변성이 효율적인 코 루틴과 결합되면 액터 모델과 같은 기능을 언어로 직접 빌드 할 수 있습니다. 예를 들어 얼랭을 생각해보십시오.

프로세스 미적분학 기반 언어에 대한 많은 연구가 있었고 그 공간을 잘 이해하지 못합니다. 그것에 대한 몇 가지 논문을 직접 읽어보고 통찰력이 있는지 확인하십시오.

  • 타사가 좋은 분석기를 쉽게 작성할 수 있도록합니다.

Roslyn에서 Microsoft에서 근무한 후 Coverity에서 근무했으며 Roslyn을 사용하여 분석기 프런트 엔드를 얻는 것이 었습니다. Microsoft가 제공하는 정확한 어휘, 구문 및 의미 분석을 통해 일반적인 멀티 스레딩 문제를 발견 한 탐지기 작성에 집중할 수있었습니다.

  • 추상화 수준 올리기

우리가 인종과 교착 상태와 그 모든 것들을 가지고있는 근본적인 이유는 우리가 해야 할 일을하는 프로그램을 작성하고 있기 때문에 명령형 프로그램을 작성하는 데는 모두 헛된 것입니다. 컴퓨터는 사용자가 말한 것을 수행하며 잘못된 일을하도록 지시합니다. 현대의 많은 프로그래밍 언어는 선언적 프로그래밍에 관한 것입니다. 원하는 결과를 말하고 컴파일러가 그 결과를 달성 할 수있는 효율적이고 안전하며 올바른 방법을 찾도록하십시오. LINQ를 다시 생각해보십시오. 우리는 당신이 의도from c in customers select c.FirstName 를 표현하는 말을 원합니다 . 컴파일러가 코드 작성 방법을 알아 내도록하십시오.

  • 컴퓨터를 사용하여 컴퓨터 문제를 해결하십시오.

기계 학습 알고리즘은 수작업으로 코딩 된 알고리즘보다 일부 작업에서 훨씬 나아지지만 물론 정확성, 훈련 시간, 나쁜 훈련으로 인한 편견 등 많은 상충 관계가 있습니다. 그러나 현재 우리가 "수작업으로"코딩하는 많은 작업이 머지 않아 기계 생성 솔루션에 적용될 수있을 것입니다. 인간이 코드를 작성하지 않으면 버그를 작성하지 않은 것입니다.

죄송합니다. 조금 엉망이었습니다. 이것은 거대하고 어려운 주제이며 20 년 동안이 문제 영역에서 진보 해 온 PL 커뮤니티에서 명확한 합의가 이루어지지 않았습니다.


"따라서 당신의 질문은 기본적으로"프로그래밍 언어가 내 프로그램이 올바른지 확인할 수있는 방법이 있습니까? "로 요약됩니다. 그리고 그 질문에 대한 대답은 실제로는 아닙니다." -실제로는 가능합니다-공식적인 검증이라고 불리며 불편한 점에서 중요한 소프트웨어에 대해 일상적으로 수행되고 있다고 확신하므로 비현실적이라고 부르지 않을 것입니다. 그러나 당신은 아마도 언어 디자이너가되는 것을 아마 알 것입니다.
mrpyo

6
@ mrpyo : 잘 알고 있습니다. 많은 문제가 있습니다. 첫째 : 나는 한 번 MSFT 연구팀이 흥미 진진한 새로운 결과를 발표 한 공식 검증 회의에 참석했습니다. 그들은 기술을 확장하여 최대 20 줄 길이 의 멀티 스레드 프로그램을 검증하고 검증기를 일주일 이내에 실행할 수있었습니다. 이것은 흥미로운 발표 였지만 나에게는 쓸모가 없었습니다. 분석 할 2 천만 라인 프로그램이있었습니다.
Eric Lippert

@mrpyo : 둘째, 앞서 언급했듯이 잠금의 큰 문제는 스레드 안전 메소드로 작성된 프로그램이 반드시 스레드 안전 프로그램이 아니라는 것입니다. 개별적인 방법을 공식적으로 검증하는 것이 반드시 도움이되는 것은 아니며, 사소한 프로그램에는 전체 프로그램 분석이 어렵습니다.
Eric Lippert

6
@mrpyo : 셋째, 공식 분석의 큰 문제는 근본적으로 우리가 무엇을하고 있는가? 우리는 전제 조건과 사후 조건사양을 제시 하고 프로그램이 해당 사양을 충족하는지 확인합니다. 큰; 이론적으로는 완전히 가능합니다. 사양은 어떤 언어로 작성됩니까? 명확한, 검증 사양 언어가있는 경우, 그냥 우리의 모든 프로그램을 작성할 수 해당 언어를 , 컴파일 것을 . 왜 이러지 않습니까? 스펙 언어로 올바른 프로그램을 작성하는 것은 실제로 어렵다는 것이 밝혀졌습니다!
Eric Lippert

2
사전 조건 / 사후 조건을 사용하여 정확성에 대한 응용 프로그램을 분석 할 수 있습니다 (예 : 코딩 계약 사용). 그러나 이러한 분석은 조건이 작성 가능하고 잠금이 아닌 조건에서만 가능합니다. 또한 분석 할 수있는 방식으로 프로그램을 작성하려면 신중한 훈련이 필요합니다. 예를 들어 Liskov 대체 원칙을 엄격하게 준수하지 않는 응용 프로그램은 분석에 저항하는 경향이 있습니다.
Brian
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.