list :: empty () 멀티 스레드 동작?


9

다른 스레드가 요소를 가져 오기를 원하는 목록이 있습니다. 비어있을 때 목록을 보호하는 뮤텍스를 잠그지 않으려면 잠그기 empty()전에 확인하십시오 .

전화 list::empty()가 100 % 정확하지 않은 경우 괜찮 습니다. 동시 통화 list::push()list::pop()통화가 중단되거나 중단되는 것을 피하고 싶습니다 .

VC ++과 Gnu GCC가 때때로 empty()잘못되고 더 나쁜 것이 없다고 가정해도 안전 합니까?

if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
    mutex.lock();
    if(list.empty() == false){ // check again while locked to be certain
         element = list.back();
         list.pop_back();
    }
    mutex.unlock();
}

1
아니오, 당신은 이것을 가정 할 수 없습니다. 당신은 VC의 같은 동시 용기 사용할 수 concurrent_queue
파나지오티스 Kanavos

2
@Fureeish 이것은 답변이되어야합니다. 나는 std::list::size일정한 시간 복잡성을 보장한다고 덧붙였다. 이것은 기본적으로 크기 (노드 수)가 별도의 변수에 저장되어야 함을 의미한다. 그것을 호출하자 size_. std::list::empty그런 다음 무언가를로 반환 size_ == 0하고 동시 읽기 및 쓰기로 size_인해 데이터 경쟁이 발생하므로 UB입니다.
Daniel Langr

@DanielLangr "일정 시간"은 어떻게 측정됩니까? 단일 함수 호출 또는 전체 프로그램입니까?
curiousguy

1
@ curiousguy : DanielLangr는 "목록 노드 수에 독립적"으로 귀하의 질문에 대답했습니다. 즉, O (1)의 정확한 정의는 모든 요소가 요소 수에 관계없이 일정한 시간보다 짧은 시간 내에 실행된다는 것을 의미합니다. en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions 다른 옵션 (C ++ 11까지)은 선형 = O (n)을 의미하며, 크기는 요소 (링크 된 목록)를 계산해야합니다. 동시성 (비 원자 읽기 / 쓰기 카운터보다 명백한 데이터-레이스)
firda

1
@ curiousguy : dV로 자신의 예를 들자면 시간 복잡도는 수학과 같은 한계입니다. 이 모든 것들은 재귀 적으로 정의되거나 "모든 N에 대해 f (N) <C와 같은 C가 존재합니다"의 형태로 정의됩니다. 즉, O (1)의 정의입니다. 알고리즘은 모든 입력에서 C- 시간 미만으로 끝납니다). 할부 상환은 평균 을 의미합니다. 즉, 일부 입력은 처리하는 데 시간이 오래 걸릴 수 있지만 (예 : 재해시 / 재 할당 필요) 평균적으로 일정합니다 (가능한 모든 입력을 가정).
firda

답변:


10

전화 list::empty()가 100 % 정확하지 않은 경우 괜찮 습니다.

아니, 괜찮아 목록이 일부 동기화 메커니즘 외부에서 비어 있는지 확인하면 (뮤텍스 잠금) 데이터 경쟁이 있습니다. 데이터 경쟁이 있다는 것은 정의되지 않은 행동을 의미합니다. 정의되지 않은 동작이 있다는 것은 더 이상 프로그램에 대해 추론 할 수 없으며 결과가 "정확"하다는 것을 의미합니다.

정신 건강을 소중히 여긴다면 성능을 확인하고 뮤텍스를 확인한 후에 확인하십시오. 즉, 목록이 올바른 컨테이너가 아닐 수도 있습니다. 작업 내용을 정확히 알려 주시면 더 나은 컨테이너를 제안하실 수 있습니다.


개인적인 관점, 전화 list::empty()는 아무런 관련이없는 읽기 행동입니다race-condition
Ngọc Khánh Nguyễn

3
@ NgọcKhánhNguyễn 만약 그들이 목록에 요소를 추가하고 있다면, 동시에 크기를 쓰고 읽는 동안 데이터 경쟁이 발생합니다.
NathanOliver

6
@ NgọcKhánhNguyễn 그건 거짓입니다. 경쟁 조건은 read-write또는 write-write입니다. 당신이 저를 믿지 않는 경우, 제공 데이터 레이스에 표준 섹션을 읽기
NathanOliver

1
@ NgọcKhánhNguyễn : 쓰기와 읽기가 원 자성으로 보장되지 않기 때문에 동시에 실행될 수 있으므로 읽기가 완전히 잘못된 것을 얻을 수 있습니다 (찢어진 읽기라고 함). 리틀 엔디안 8 비트 MCU에서 쓰기를 0x00FF에서 0x0100으로 변경하고, 낮은 0xFF를 0x00으로 다시 쓰는 것으로 시작하여 읽기가 이제 정확히 0을 얻습니다. 그러나 읽기 스레드가 이미 잘못된 값을 얻었습니다 (0x00FF도 0x0100도 아니지만 예기치 않은 0x0000).
firda

1
@ NgọcKhánhNguyễn 일부 아키텍처에서는 가능하지만 C ++ 가상 머신은 그러한 보장을 제공하지 않습니다. 하드웨어가 그렇게하더라도 스레드 동기화가 없으면 단일 스레드 만 실행하고 그에 따라 최적화한다고 가정 할 수 있으므로 컴파일러가 변경 사항을 전혀 볼 수없는 방식으로 코드를 최적화하는 것이 합법적입니다.
NathanOliver

6

서로에 대해 동기화되지 않은 읽기 및 쓰기 (대부분 이름이 같다고 가정하는 경우 아마도 size멤버에 std::list대해)가 있습니다 . 하나의 스레드가 (외부에서 ) 호출 하고 다른 스레드가 내부에 들어 와서 실행 한다고 상상해보십시오 . 그런 다음 수정중인 변수를 읽습니다. 이것은 정의되지 않은 행동입니다.empty()if()if()pop_back()


2

일이 어떻게 잘못 될 수 있는지에 대한 예로서 :

충분히 똑똑한 컴파일러 mutex.lock()list.empty()반환 값을 변경할 수 없으므로 내부 if검사를 완전히 건너 뛸 수 있으므로 결국 pop_back첫 번째 요소 다음에 마지막 요소가 제거 된 목록이 표시됩니다 if.

왜 그렇게 할 수 있습니까? 에 동기화가 없으므로 list.empty()동시에 변경되면 데이터 경쟁을 구성합니다. 표준에 따르면 프로그램에는 데이터 레이스가 없어야하므로 컴파일러는 당연한 것으로 간주합니다 (그렇지 않으면 거의 최적화를 수행 할 수 없음). 따라서 비 동기화에 대한 단일 스레드 관점을 가정하고 list.empty()일정하게 유지해야한다는 결론을 내릴 수 있습니다.

이것은 코드를 손상시킬 수있는 몇 가지 최적화 (또는 하드웨어 동작) 중 하나 일뿐입니다.


현재 컴파일러는 최적화하고 싶지 않은 것 같습니다 a.load()+a.load()...
curiousguy

1
@curiousguy 어떻게 최적화 하시겠습니까? 전체 순차 일관성을 요청하면 다음과 같은 결과를 얻을 수 있습니다.
Max Langhof

@MaxLanghof 최적화 a.load()*2가 명확 하지 않다고 생각 하십니까? a.load(rel)+b.load(rel)-a.load(rel)어쨌든 최적화 되지 않았습니다 . 없어 잠금 (본질적으로 대부분 순서 일관성이 있음)이 더 최적화 될 것으로 예상하는 이유는 무엇입니까?
curiousguy

@curiousguy 비 원자 액세스 (여기서는 잠금 전후)와 원자의 메모리 순서가 완전히 다르기 때문에? 잠금이 "더 많이"최적화 될 것으로 기대하지는 않습니다. 동기화되지 않은 액세스는 순차적으로 일관된 액세스보다 최적화 될 것으로 기대합니다. 자물쇠의 존재는 내 요지와 관련이 없습니다. 그리고 컴파일러는에 최적화 a.load() + a.load()할 수 없습니다 2 * a.load(). 더 알고 싶으시면 이것에 대해 질문하십시오.
Max Langhof

@MaxLanghof 나는 당신이 무엇을 말하려고하는지 전혀 모른다. 잠금은 본질적으로 일관됩니다. 왜 구현이 일부 스레딩 프리미티브 (자물쇠)가 아닌 다른 스레드 프리미티브 (자물쇠)에 대해 최적화를 시도합니까? 원자 사용에 대해 비 원자 액세스가 최적화 될 것으로 기대하십니까?
curiousguy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.