[완료] 멀티 프로세스간 or 멀티 쓰레드간 데이타의 동기화의 필요성

thisnome의 이미지

안녕하세요.

개발을 하다가 사소해 보이지만, 누구하나 근거가 있는 확실한 심판관이 될 수 없어서 동료들 모두 고민하게 만드는 문제가 생겼습니다.

-환경-

멀티 프로세스간 이던, 멀티 쓰레드간 이던 상관 없습니다.

오직 특정 데이타에 쓰려는 한넘 A 와 읽으려는 넘 B 가 있습니다.

서로 접근하려는 공유 데이타는 가장 단순한 integer 형 입니다. (c 라고 하겠습니다.)

멀티 프로세스간이라면 c 는 shared mem 에 위치할테고, 멀티 쓰레드라면 global 변수라 가정합니다.

-문제-

A 는 값을 변경(쓰기)하기만 하고, B 는 그 값을 읽기만 하니..

A 와 B 사이에서는 c 에 접근할때 lock (semaphore 나 mutex 를 말합니다.) 을 할 필요가 없다.

B 가 얻을 수 있는 정보는 A 가 바꾸기전 정보가 x, 바꾼후의 정보가 y 라면 x 나 y 둘중의 하나만 얻게된다.

라고 주장하는 그룹이 있구요,

단순한 4 bytes 짜리 변수 하나인 c 라고는 하지만,

CPU 가 그 변수 c 에 값을 넣는 동작을 할때는 automic operation 을 보장 받지 못하기때문에

lock 이 꼭 필요하다.

라고 주장하는 그룹이 있습니다.

어떤 주장이 맞는지요?

시스템/환경/CPU 마다 다르다면, 그 기준은 어떻게 되는지

꼭 답변을 모르시더라도 답을 얻을 수 있는 자료가 있다면 부탁드립니다.

태공의 이미지


복잡한 주제군요 일단 C코드상에서 sequential program order를 생각해보자면

proc A에서 다음의 C 코드를 수행한다고 하고
*addr = 1;(1)
*addr = *addr + 1; (2)

Proc B에서
val = *addr; (3)
를 수행한다고 합시다.

여기서 (2)는 read & modify & write의 구조를 가지게 되고 이 세단계가 atomic 하게 처리되지 않는다는 것이죠.
즉 일반적으로 (2)은 실질적으로 3개의 instruction으로 볼수 있습니다. 프로그램 순서상으로 Proc A를 먼저 실행하고 Proc B를 나중에 실행한다고 하면 (이런 경우가 종종생깁니다) (3)의 실행이 완료된 이후에는 val 값이 2를 가리켜야 sequential program order를 맞출수 있게 됩니다.

하지만 어떠한 경우에는 CPU가 (2)의 두번째 instruction까지만 실행하고
인터럽트에 의해 제어권이 proc B로 넘어 갈 가능성이 생깁니다. 이때는 (3)을 실행하고 난후에는 *addr에 변경된 값이 써지기 전이니 val이 1값을 가져서 순서가 엇갈리게 됩니다.

이런 경우 문제가 될 소지가 많습니다. 예를 들어 addr가 가리키는 영역이 리스트 알맹이 갯수였는데 이전에 하나남아있던 list element를 가리키는 데이터를 날리고 갯수를 0으로 하려고 했는데 proc B가 확인하기로는 한개가 남아있는 것으로 착각할수 있는것이죠.

머 DMA나 멀티프로세서의 경우를 생각하면 더욱 엄격한 조건이 가해져야 되는 것은 말할 필요도 없겠습니다.

틀린 부분있으면 지적바랍니다.

jick의 이미지

x86 기준으로 단지 "덮어쓰기"만 하는 것이라면 (그러니까 i++ 같은 명령이 아니라면) atomicity가 보장됩니다. 단, 변수의 align이 맞아야만 합니다. (즉 주소가 4byte의 배수여야 합니다.)

일반적인 범용 CPU에서도 4byte write는 atomicity를 거의 다 보장하는 것으로 알고 있습니다. 이를 확실히 알려면... 어쩔 수 없이 해당 벤더의 매뉴얼을 샅샅이 훑어보는 수밖에 없습니다. 매우 중요한 주제이므로 분명 어딘가에는 명시가 되어 있습니다. 어딘지 모른다는 게 문제지. -_-;;

그리고 매우 중요한 문제인데 변수가 하나일 때는 위의 얘기가 통하지만 두 개 이상이면 문제가 훨씬 복잡해집니다. 예를 들어 x, y가 처음에 0이었고 A가 x에 1을 쓰고 그 다음에 y에 1을 쓴다고 합시다.

그리고 B가 y를 먼저 읽고 x를 읽습니다.

이 경우 상식적(!)으로 생각하면 y가 1이면 x는 항상 1을 읽어야 할 것 같지만 0을 읽을 수 있습니다. 이런 상황을 막으려면 lock을 걸거나 memory barrier(fence) instruction을 써야 하는데 역시 자세한 건 해당 벤더의 매뉴얼을...... -_-;;; 일단 인터넷에서 memory consistency model을 찾아보세요.

태공의 이미지

좀더 내용을 추가해야 할 듯 합니다.

자 proc A에서 다음의 C 코드를 수행한다고 하고
func_a()
{
........
sendpkt(pElement)
free (pElement);

nPktCount--;

if ( nPktCount == 0 )
bPktEmpty = TRUE;
else
bPktEmpty = FALSE; =====> FALSE를 쓰기 바로 직전에 태스크 스위치가 일어나고 func_b가 수행되면
.........
}

Proc B에서
func_b()
{
.........
if ( bPktEmpty == TRUE )
go left
else
play_with( get_element_from(list));
.........
}
를 수행한다고 합시다.

bPktEmpty는 func_a에서 쓰기만 하고 func_b에서는 읽기만 하고 있고, single CPU 라고 한다면 쓰기에서 aomicity가 보장된다고 합시다.

이 함수 두개가 적절한 조치없이 별도의 쓰레드로 돌아가면 문제가 생깁니다.

문제는 sequential order를 맞춰야 할 부분이 어디서 어디까지인지 그리고 병렬화할수 있는 부분은 어떠한 부분인지 프로그래머가 명확히 알아야 한다는 점입니다. 이를 프로그래머가 일일이 챙겨야하는 부분은 상당히 신경을 많이 써야만 되기에 숙달된 프로그래머라하더라도 관리하기가 매우 어렵고 실수도 많이 하는 부분이죠. 현재 이 분야에 대해 연구가 많이 진행되고 있으니 관심있는 분은 검색하면 많은 자료를 찾을수 있을겁니다.

thisnome의 이미지

먼저 답변 감사드립니다.

태공님의 마지막 예제에는 nPktCount 와 bPktEmpty 라는 두가지 상태값이 존재해서 분명 각각의 data 에 automicity 가 보장된다 하더라도 lock 절차가 없다면 문제의 소지가 있다고 보여집니다.

그러나, 만약 제가 처음 올렸던 질문글의 내용에서 처럼 단순히 하나의 값을 전달하는 과정이라면, lock 이 없더라도 A (쓰는넘) 에서 어떤 instruction 단계에 있던지, B (읽는넘) 는 c (공유된 데이타) 의 값을 x (쓰기전값) 나 y (쓰고난후의값) 둘중에 하나만 얻게 되겠군요.

참고로 B 는 c 에 x 값이 나오면 다른 일을 하지 않고 반복적으로 c 를 읽는 작업을 수행합니다. y 값이 나오면 뭔가 다른 일을 하지요.. 다만, x 도 y 도 아닌 다른 값을 읽게되면 문제가 되는 로직입니다.

jick wrote:

일반적인 범용 CPU에서도 4byte write는 atomicity를 거의 다 보장하는 것으로 알고 있습니다.

스팍 멀티 환경입니만, 동일하겠죠..? (확인을 다시 해야한다는게 안습이네요..)

jick 님의 답변을 보더라도 제 질문에 대한 결론은 lock 이 없어도 가능하다가 되겠네요.

혹시나 틀린 부분있다면 고민하지마시고 답글 날려주세요. ^^ 감사합니다.

mach의 이미지

일단, 락을 거는 것이 좋습니다. 특히, 다중환경(운영체제, HW등 차이)에서 수행하는 프로그램이라면 더욱 권고합니다.

이런 시나리오, 예를 들어, A가 연속적으로 스케쥴링을 받아서, c값을 2번 변경하고, 그 이후에 B가 (2번변경된 후의) c 값을 참조하는 시나리오에서도 전체 프로그램의 흐름에는 무리가 없는 경우인가요?

...
A가 C변경1 -- C1,
A가 C변경2 -- C2,
// C를 연속적으로 2번 변경하게 되었음.
B가 C를 참조(이 경우, B는 C1을 손실하고, C2만을 처리하게됨, 그러나 문제 없음?)
...

------------------ P.S. --------------
지식은 오픈해서 검증받아야 산지식이된다고 동네 아저씨가 그러더라.

------------------ P.S. --------------
지식은 오픈해서 검증받아야 산지식이된다고 동네 아저씨가 그러더라.

thisnome의 이미지

네.. A 가 c 를 여러번 변경한 후에라도, B 가 c 를 참조하게 되는것은 문제 없습니다.

아래는 코드의 내용입니다.

static char buf[무한];
static int QHead = 0;
static int QTail = 0;
 
InsertMsg (void *pMsg)
{
  memcpy (buf, pMsg, MSGLEN);
  QTail += MSGLEN;
}
 
ReadMsg (char *localbuf)
{
  if (QHead == QTail)
    return 들어온 데이타 없음;
 
  memcpy (localbuf, buf, MSGLEN);
  QHead += MSGLEN;
}

A 는 InsertMsg 를 반복 수행하는 프로세스 이며, B 는 ReadMsg 를 반복 수행하는 프로세스 입니다.

큐의 크기나, 메시지 사이즈등의 기타 요소부분은 무시하셔도 됩니다.

요점은 A 는 InsertMsg 를 하여 메시지를 넣고 QTail 을 늘리고,

B 는 QTail 과 QHead 가 같은 경우를 제외하곤, 내용을 읽고, QHead 를 옮겨 주는 일입니다.

지금까지 논의된 c 라는 변수는 QTail 을 의미합니다.

답변해 주시는데 도움이 될 것 같아서 추가하였습니다.

제 입장은 위의 코드에 문제가 있을것 같으며, mach 님의 의견대로 lock 을 해주는게 좋다고 생각했습니만, 반대하는 분들에 대해 확신 시킬수 있는 정보가 없고, 확실히 아는게 중요하다고 생각되서 질문을 올리게 되었습니다.

여러분들이라면 어떻게 하실런지요? ^^;

jick의 이미지

코드만 놓고 보자면 lock은 필요없어 보이지만 memory barrier instruction이 필요합니다. x86에서는 mfence, lfence, sfence 등의 명령이 있고 sparc에서는 아마 membar라는 명령을 썼던 것 같습니다. (자세한 것은 linux kernel에 대해 나온 아무 책에서나 memory barrier를 찾아보시면 됩니다.)

요즘 CPU는 명령의 순서를 멋대로 바꿔 실행하기 때문에, 이 경우 ReadMsg가 QHead의 값이 증가된 것을 읽었다고 해서 바로 다음의 memcpy에서 InsertMsg가 넣은 값을 읽는다는 보장이 없습니다. 이를 보장하기 위해서는 InsertMsg의 memcpy와 QTail 증가 사이에 write barrier가, 마찬가지로 ReadMsg의 if 문과 memcpy 사이에 read barrier가 있어야 합니다.

태공의 이미지

이거 발을 들여놨으니 마무리는 해야될것 같아 씁니다.
일단 주어진 설명 자체만으로 문제가 있냐 없냐하는 질문에 대해서는 있을수도 있고 없을수도 있다라고 해야겠네요.

한가지 예를 들자면 보험을 들수 있을것 같은데요. 건강한 현재 상태에서는 보험이 필요없다고 느껴질수도 있을겁니다. 하지만 앞날이란 모르는것 미리 비용을 들여 인생보험을 들어놓는것이죠. 물론 비용을 많이 들이면 더 많이 보장 받을수 있고 이게 시장일겁니다. 비용과 보장에 trade-off관계가 있다는 건 다들 알겁니다.

본 질문에 관련해서 개발팀인원의 의견들중 별다른 이상없을거다 괜히 lock걸어서 효율을 떨어뜨리지마라 그런 얘기 충분히 나올수 있습니다. 지금 당장 아무 이상없는데 괜히 비용만 더 들이는 경우가 될수도 있습니다. 위에서도 언급했듯이 개발자가 몇가지점만 인지하고 대응할수 있다면 그대로 가도 무방할겁니다.

하지만 특정한 경우에는 문제가 생길 소지가 분명 있습니다. 예제코드를 보면

InsertMsg (void *pMsg)
{
memcpy (buf, pMsg, MSGLEN);
QTail += MSGLEN; //========> QTail==QHead이고 QTail을 증가하기 직전 scheduling out 되고 ReadMsg가 수행되면
}

ReadMsg (char *localbuf)
{
if (QHead == QTail)
return 들어온 데이타 없음;

memcpy (localbuf, buf, MSGLEN);
QHead += MSGLEN;
}

QTail==QHead이고 QTail을 증가하기 직전 scheduling out 되고 ReadMsg가 수행되면 실제 메모리에는 데이타가 있는데 ReadMsg에서는 없는 것으로 판단할 겁니다. 분명 넓은 의미에서 데이타 일관성이 어긋나는 경우가 생긴겁니다. (여기서는 buf,QHead,QTail을 하나로 묶인 덩어리로 봐야한다는 의미). 이게 사용목적에 따라 가시적으로 보이는 문제가 없을수도 있지만 만약에 돈이 관련된 계약에 사용되는 코드라 한다면 문제가 심각해 집니다.

또 이코드가 일회성코드라면 상관없겠지만 확장성을 염두에 두고 있다면 예전의 미국우주왕복선 폭발사고의 예를 보더라도 분명 적절한 방어코드를 넣어야 됩니다.

결론을 내리자면 책임질수 있는 개발자가 생각하기에 문제가 없다라고 확신하거나 충분히 대응가능하다라고 생각한다면(개인적으로 권하지는 않음) 그대로 해도 무방하지만 그렇지 않으면 보험을 들어 두는게 좋다라고 생각합니다.

추가사항: 조금더 세분화를 시킬수 있는게 변수의 상태가 이전값과 새로운값 두개만 있다라고 생각할게 아니고 과도기적인 상태가 있다라고 생각할수 있습니다. 분명 하드웨어적으로는 어느 한 순간에 "B (읽는넘) 는 c (공유된 데이타) 의 값을 x (쓰기전값) 나 y (쓰고난후의값) 둘중에 하나만 얻게" 된다고 보지만 "쓰기전" 값을 확정값과 임시값으로 분리하여 임시값은 조만간에 "쓰고난후의값"으로 대치될 값으로 봐야 합니다. 이 과도기적인 임시값을 갖게 되는 상태에서는 data consistency가 맞지 않게 되고 이는 control sequence를 어긋나게 할수 있으며 시스템을 고장낼수도 있음을 알아두어야 합니다.

thisnome의 이미지

답변 감사드립니다.

Memory Barrier 얘기가 나와서 인터넷에서 관련정보들을 찾아보며.. 문제가 더 어렵게 흘러가는구나.. 라는 생각을 갖고 있던차에
태공 님이 정리를 해주셨네요..

모든 답글 주신 분들께 다시한번 감사드립니다. 덕분에 관련지식도 더 늘게 되었네요..

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.