在做字符串分析的时候,常常会用到字符串分割技术,一般都会想到使用strtok,但遗憾的是,strtok函数是在多线程概念尚未普及的时候写的,没有考虑多线程会带来使用该函数会带来意外危险的问题。
下面简单分析一下strtok在多线程程序里面使用的可能的危险。
strtok的内部实现上,使用了一个静态地址指针,指向上一次分析完成以后的字符串的结束的地址。
- UINT Thread1(LPVOID pParam)
- {
- char szSrc[128] = "THIS,IS,A,TEST";
- char szSeps[32] = ",";
- char *pToken = strtok(szSrc, szSeps);
- while(pToken != NULL)
- {
- cout << pToken << endl;
- pToken = strtok(NULL, szSeps);
- }
- }
- UINT Thread2(LPVOID pParam)
- {
- char szSrc[128] = "HELLO WORLD THIS IS STRTOK TEST";
- char szSeps[32] = " ";
- char *pToken = strtok(szSrc, szSeps);
- while(pToken != NULL)
- {
- cout << pToken << endl;
- pToken = strtok(NULL, szSeps);
- }
- }
当程序执行的时候,可能Thread1先执行 char *pToken = strtok(szSrc, szSeps);
这时默认的strtok内部下一个指针地址指向了char szSrc[128] = "THIS,IS,A,TEST";的字符串"IS,A,TEST"
接下来线程2开始执行, char *pToken = strtok(szSrc, szSeps);
这时默认的strtok内部下一个指针地址指向了char szSrc[128] = "HELLO WORLD THIS IS STRTOK TEST";的字符
串" WORLD THIS IS STRTOK TEST"
那么接下来会发生什么奇怪的现象呢?
Thread1 执行strtok(NULL, ",")由于分析的源串已经指向到了" WORLD THIS IS STRTOK TEST",所以将不能再分析得到任何以逗号(,)分隔的字串了!线程Thread2由于指向不变,仍然可以分析得到以空格( )分隔的字符串。
这个问题说明了在strtok是一个全局函数,里面使用了一个静态地址指针,这样的话,多线程调用就随时会改变静态指针的指向,程序运行的结果也就未为可知,不是程序本身希望实现的功能。
所以,在多线程程序里面,最好不要使用strtok函数!使用就得冒很大的程序运行结果不正确的风险
这里我写了一个多线程安全的TStrTok函数,可以实现字符串的分隔,基本原理是:将分隔字符填0,然后提取各个字符串。
下面是源码:
- #include <afxtempl.h>
- #include <iostream>
- using namespace std;
- /*--------------------------------------------------------------------------
- - src (in) : 源串
- - sep (in) : 分隔串
- - srcLen (in): 源串长度
- - sepLen (in): 分隔串长度
- - sl (out): 分析结果集字符串
- *--------------------------------------------------------------------------*/
- BOOL MStrTok(const char* src, const char *sep, int srcLen, int sepLen, CStringArray &sa)
- {
- if( (src == NULL) || (sep == NULL) )
- {
- return FALSE;
- }
- int nMinLenSrc = min(strlen(src), srcLen);
- int nMinLensep = min(strlen(sep), sepLen);
- char *pNew = new char[nMinLenSrc + 1];
- memset(pNew, 0, nMinLenSrc + 1);
- memcpy(pNew, src, nMinLenSrc);
- char *p = (char *)sep;
- char *q = (char *)pNew;
- int i = 0;
- int j = 0;
- while(*p != 0 && i < nMinLensep)
- {
- q = pNew;
- for(j = 0; j < nMinLenSrc; j++)
- {
- if(*q == *p)
- *q = 0;
- q++;
- }
- p++;
- i++;
- }
- q = pNew;
- i = 0;
- while(i < nMinLenSrc)
- {
- if(*q != 0)
- {
- sa.Add(q);
- i += strlen(q);
- q = pNew + i;
- }
- else
- {
- i++;
- q++;
- }
- }
- delete []pNew;
- return TRUE;
- }
- int main()
- {
- char szTest[] = "THIS IS A ; ,,,,, TStrTok,,, TEST, OK ;NOW;;;;;;???";
- char szSeps[] = ",; ";
- CStringArray sa;
- MStrTok(szTest, szSeps, sizeof(szTest), sizeof(szSeps), sa);
- cout << endl;
- for(int i = 0; i < sa.GetSize(); i++)
- {
- cout << (LPCTSTR)sa[i] << " ";
- }
- cout << endl;
- getchar();
- return 0;
- }
- /*
- 运行结果如下:
- THIS IS A TStrTok TEST OK NOW ???
- */
----------------------------------------------------------------------------------------------------------------------------------------
【后记】:
本文发表以后,很多网友提出了很多很好的意见和建议。我查了一下strtok_r这个多线程的字符串分割
函数,发现在VC下无定义;在Unix下倒是可以用,但是Unix文档有说明,该函数会改写源串,不建议使用。
另外,我希望一次将所有的字符串都分割出来,而不是一次次的调用,所以,我还是保留我的MStrTok
的定义方式,和原始的strtok是有差别的,使用时请注意。
MStrTok的输出是VC的CStringArray,如果要用标准C++,可以用vector代替CStringArray,在此不再赘述。
原来的MStrTok效率可以改进,一次扫描足以得到结果,下面是改进后的MStrTok:
- BOOL MStrTok(const char* src, const char *sep, int srcLen, int sepLen, CStringArray &sa)
- {
- if( (src == NULL) || (sep == NULL) )
- {
- return FALSE;
- }
- int nMinLenSrc = min(strlen(src), srcLen);
- int nMinLensep = min(strlen(sep), sepLen);
- char *pNew = new char[nMinLenSrc + 1];
- memset(pNew, 0, nMinLenSrc + 1);
- memcpy(pNew, src, nMinLenSrc);
- char *p = (char *)pNew;
- char c;
- int i = 0, n = 0;
- char *pStr = NULL;
- while(*p != 0 && n < nMinLenSrc)
- {
- c = *sep;
- for(i = 0; (i < nMinLensep) && (c != 0); i++)
- {
- c = *(sep + i);
- if(c == *p)
- {
- *p = 0;
- }
- }
- if(*p == 0)
- {
- if(pStr != NULL)
- {
- sa.Add(pStr);
- pStr = NULL;
- }
- }
- else
- {
- if (pStr == NULL)
- {
- pStr = p;
- }
- }
- p++;
- n++;
- }
- if(pStr != NULL)
- {
- sa.Add(pStr);
- }
- delete []pNew;
- return TRUE;
- }