티스토리 뷰
문자열을 특정 구분자로 잘라서 파싱할 때 유용하게 사용되는 함수가 바로 strtok 되시겠다. 하지만 많은 웹사이트 글처럼 이 함수는 thread 에서 안전하지 못하다. 즉, 의도치 않게 동작할 수 있다는 의미인데 어디에도 명쾌하게 이유를 설명한 글이 없어서 직접 작성해본다.
우선 strtok 의 함수 원형을 glibc 에서 찾아보면 아래와 같다.
/* Parse S into tokens separated by characters in DELIM.
If S is NULL, the last string strtok() was called with is
used. For example:
char s[] = "-abc-=-def";
x = strtok(s, "-"); // x = "abc"
x = strtok(NULL, "-="); // x = "def"
x = strtok(NULL, "="); // x = NULL
// s = "abc\0=-def\0"
*/
char *
strtok (char *s, const char *delim)
{
static char *olds;
return __strtok_r (s, delim, &olds);
}
친절하게 예제까지 첨부되어 있다. 함수 자체는 매우 간결한데, 두 가지를 주의 깊게 봐야 하겠다. 바로 static char *olds 와 __strtok_r 을 호출한다는 것인데 하나씩 살펴보면 *olds 는 static 으로 선언되어 프로그램의 data 영역에 저장된다. data 영역에는 전역변수( global )도 포함되는데 어디서나 접근 가능한 변수라는 의미다. 즉, 임시로 사용되는 stack 영역이 아니기 때문에 multi-thread 에서 사용되면 atomic 하게 동작하지 않게 된다.
그래서 대안으로 사용되는 함수가 strtok_r 인데 이번에도 함수 내용을 살펴보면 아래와 같다.
/* Parse S into tokens separated by characters in DELIM.
If S is NULL, the saved pointer in SAVE_PTR is used as
the next starting point. For example:
char s[] = "-abc-=-def";
char *sp;
x = strtok_r(s, "-", &sp); // x = "abc", sp = "=-def"
x = strtok_r(NULL, "-=", &sp); // x = "def", sp = NULL
x = strtok_r(NULL, "=", &sp); // x = NULL
// s = "abc\0-def\0"
*/
char *
__strtok_r (char *s, const char *delim, char **save_ptr)
{
char *end;
if (s == NULL)
s = *save_ptr;
if (*s == '\0')
{
*save_ptr = s;
return NULL;
}
/* Scan leading delimiters. */
s += strspn (s, delim);
if (*s == '\0')
{
*save_ptr = s;
return NULL;
}
/* Find the end of the token. */
end = s + strcspn (s, delim);
if (*end == '\0')
{
*save_ptr = end;
return s;
}
/* Terminate the token and make *SAVE_PTR point past it. */
*end = '\0';
*save_ptr = end + 1;
return s;
}
strtok_r 을 살펴보기로 하고 대뜸 __strtok_r 을 가져왔다. 이는 내부적으로 strtok_r 이 __strtok_r 로 alias 되어 있기 때문에 크게 위 함수가 결국 strtok_r 을 살펴보는 것과 같다. 이 내용에 대해서는 더 깊이 살펴볼 필요가 없다. 글의 취지와 맞지 않으니까.
여하튼 여기까지만 자세히 살펴보면 strtok 는 결국 strtok_r 의 wrapping 함수라는 것을 알 수 있다. 단순히strtok 안에서 사용되는 static 한 변수 olds 를 애초에 함수의 인자로 받아서 처리하는 것이다. 그렇기 때문에 thread 에서 safety 하게 사용할 수 있게 된다.
우리는 strtok 와 strtok_r 의 원형을 모두 살펴보았기 때문에 strtok_r 의 마지막 인자인 char **save_ptr 에 대해서도 잘 이해할 수 있다. strtok_r 의 man page 에 좋은 예제가 있는데, 혹시 loop 안에서 strtok_r 을 중첩으로 사용하려면 **save_ptr 을 별도로 잘 관리해줘야 한다. 당연한 이야기지만 변수 한개가 여러 loop 에서 사용될 수 없다는 의미다.
아래 man page 의 일부 내용을 첨부하니, saveptr 쪽만 살펴보면 되겠다.
char *saveptr1, *saveptr2;
for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) {
token = strtok_r(str1, argv[2], &saveptr1);
if (token == NULL)
break;
printf("%d: %s\n", j, token);
for (str2 = token; ; str2 = NULL) {
subtoken = strtok_r(str2, argv[3], &saveptr2);
if (subtoken == NULL)
break;
printf(" --> %s\n", subtoken);
}
}
사실 thread 를 염두하고 프로그래밍을 한다는 것은 좋지만 애초에 그 원리를 알고 확장성을 고려해서 thread unsafety 함수는 피하는게 좋은 습관이겠다.
'개발 > Linux' 카테고리의 다른 글
VIM 주석을 달아보자 (Doxygen) (0) | 2017.04.11 |
---|---|
VIM 낱말 자동완성 (2) | 2017.03.28 |
sshd PermitRootLogin 기본 값 변경 (0) | 2017.02.13 |
하드링크와 심볼릭링크 개념잡기 (1) | 2017.01.05 |
DocuWiki 시간 동기화 (0) | 2016.12.19 |
- Total
- Today
- Yesterday