Kisser Leon

这个kisser不太冷
posts - 100, comments - 102, trackbacks - 0, articles - 0

多线程学习中碰到的一个很有意思的问题

Posted on 2007-03-27 21:24 kk 阅读(5056) 评论(11)  编辑 收藏 引用 所属分类: IT

#include <stdio.h>
#include <windows.h>

const int numThreads = 3;

 

DWORD WINAPI helloFunc(LPVOID pArg)

{

       int num = (int) pArg;

       printf("Hello Thread %d\n", num);

      

       return 0;

}

 

int main()

{     

       HANDLE hThread[numThreads];

 

       for (int i = 0; i < numThreads; i++)

       {

              hThread[i] = CreateThread(NULL, 0, helloFunc, (LPVOID)i, 0, NULL);

       }

 

       WaitForMultipleObjects(numThreads, hThread, TRUE, INFINITE);

 

       return 0;

}

 

上面可以说是一个最简单的多线程程序了。

运行时库选项:

(1)       单线程调试 (/MLd)

(2)       多线程调试 DLL (/MDd)

(3)       多线程调试 (/MTd)

上面三个是 debug 版本的,还有与它们相对的三个 release 版本等。

由于一开始的时候系统默认的是 /MLd ,所以产生一些很有意思的问题,比如说有些线程 的线程函数会被执行多次:

Hello Thread 1

Hello Thread 1

Hello Thread 0

Hello Thread 2

Press any key to continue

线程 1 被执行了两次!

Hello Thread 0

Hello Thread Hello Thread 1

Hello Thread Hello Thread 1

2

Press any key to continue

这个就更奇怪了!虽然是因为有 race condition 在,但是为什么会多出一个 Hello Thread 呢( 5 Hello Thread 4 个数字)?那就只有一个原因,有一个数字被覆盖了(难道会有可能没来得及输出吗?)!

Intel Thread Checker 进行 check 的时候,会发生下面这样的问题,甚是奇怪!

图片传不上来。下次再传。终于上传成功了,娃哈哈
P1.bmp


碰到这么多问题,因为偶是初学者,所以就一直没有察觉出来编译选项设置有问题。而且我一直觉得都是对的。只是对其中的一个线程的线程函数为什么会执行两遍感觉很
confuse WHY WHY WHY Google 一个线程的线程函数是否可以执行两遍,找不到有用的咚咚! Baidu 也没有相关信息。哭。只能暂时放弃,不过我始终还是不明白一个线程的线程函数为什么可以执行多次?我也没有见过这样的例子。可能这个问题在俺脑子里走太多路了,今天突发奇想,会不会是编译选项有问题?检查了一下才发现原来一开始忘了把它设为多线程的了,设回来之后就一切正常了,试了 N 多次都没有碰到过问题(虽然这不能证明肯定没有问题!线程执行顺序是不可预料的!要看 CPU 的心情的, J )。

 

Next, 接下来查一下编译器相关资料。

Multithreaded Libraries Performance 

The single-threaded CRT is no longer in vs2005 available. This topic discusses how to get the maximum performance from the multithreaded libraries.

The performance of the multithreaded libraries has been improved and is close to the performance of the now-eliminated single-threaded libraries. For those situations when even higher performance is required, there are several new features.

·         Independent stream locking allows you to lock a stream and then use _nolock Functions that access the stream directly. This allows lock usage to be hoisted outside critical loops.

·         Per-thread locale reduces the cost of locale access for multithreaded scenarios (see _configthreadlocale).

·         Locale-dependent functions (with names ending in _l) take the locale as a parameter, removing substantial cost (for example, printf, _printf_l, wprintf, _wprintf_l).

·         Optimizations for common codepages reduce the cost of many short operations.

·         Defining _CRT_DISABLE_PERFCRIT_LOCKS forces all I/O operations to assume a single-threaded I/O model and use the _nolock forms of the functions. This allows highly I/O-based single-threaded applications to get better performance.

·         Exposure of the CRT heap handle allows you to enable the Windows Low Fragmentation Heap (LFH) for the CRT heap, which can substantially improve performance in highly scaled scenarios.

 

运行时库是程序在运行时所需要的库文件 ,通常运行时库是以 LIB DLL 形式提供的。 C 运行时库诞生于 20 世纪 70 年代,当时的程序世界还很单纯,应用程序都是单线程的,多任务或多线程机制在此时还属于新观念。所以这个时期的 C 运行时库都是单线程的。

随着操作系统多线程技术的发展,最初的 C 运行时库无法满足程序的需求,出现了严重的问题。 C 运行时库使用了多个全局变量(例如 errno )和静态变量,这可能在多线程程序中引起冲突。假设两个线程都同时设置 errno ,其结果是后设置的 errno 会将先前的覆盖,用户得不到正确的错误信息。

  因此, Visual C++ 提供了两种版本的 C 运行时库。一个版本供单线程应用程序调用,另一个版本供多线程应用程序调用。多线程运行时库与单线程运行时库有两个重大差别:
  ( 1 )类似 errno 的全局变量,每个线程单独设置一个。这样从每个线程中可以获取正确的错误信息。
  ( 2 )多线程库中的数据结构以同步机制加以保护。这样可以避免访问时候的冲突。

   Visual C++ 提供的多线程运行时库又分为静态链接库和动态链接库两类,而每一类运行时库又可再分为 debug 版和 release 版,因此 Visual C++ 共提供了 6 个运行时库。如下表:

C 运行时库

库文件

Single thread(static link) ML

libc.lib

Debug single thread(static link) MLd

libcd.lib

MultiThread(static link)  MT

libcmt.lib

Debug multiThread(static link) MTd

libcmtd.lib

MultiThread(dynamic link) MD

msvert.lib

Debug multiThread(dynamic link) MDd

msvertd.lib


   2.C 运行时库的作用
   C 运行时库除了给我们提供必要的库函数调用(如 memcpy printf malloc 等)之外,它提供的另一个最重要的功能是为应用程序添加启动函数。
   C 运行时库启动函数的主要功能为进行程序的初始化,对全局变量进行赋初值,加载用户程序的入口函数。
  不采用宽字符集的控制台程序的入口点为 mainCRTStartup(void) 。下面我们以该函数为例来分析运行时库究竟为我们添加了怎样的入口程序。这个函数在 crt0.c 中被定义,下列的代码经过了笔者的整理和简化:

void mainCRTStartup(void)
{
  int mainret;
  /* 获得 WIN32 完整的版本信息 */
  _osver = GetVersion();
  _winminor = (_osver >> 8) & 0x00FF ;
  _winmajor = _osver & 0x00FF ;
  _winver = (_winmajor << 8) + _winminor;
  _osver = (_osver >> 16) & 0x00FFFF ;

  _ioinit(); /* initialize lowio */

  /* 获得命令行信息 */
  _acmdln = (char *) GetCommandLineA();

  /* 获得环境信息 */
  _aenvptr = (char *) __crtGetEnvironmentStringsA();

  _setargv(); /* 设置命令行参数 */
  _setenvp(); /* 设置环境参数 */

  _cinit(); /* C 数据初始化:全局变量初始化,就在这里! */

  __initenv = _environ;
  mainret = main( __argc, __argv, _environ ); /* 调用 main 函数 */

  exit( mainret );
}

从以上代码可知,运行库在调用用户程序的 main WinMain 函数之前,进行了一些初始化工作。初始化完成后,接着才调用了我们编写的 main WinMain 函数。只有这样,我们的 C 语言运行时库和应用程序才能正常地工作起来。

  除了 crt0.c 外, C 运行时库中还包含 wcrt0.c wincrt0.c wwincrt0.c 三个文件用来提供初始化函数。 wcrt0.c crt0.c 的宽字符集版, wincrt0.c 中包含 windows 应用程序的入口函数,而 wwincrt0.c 则是 wincrt0.c 的宽字符集版。

   Visual C++ 的运行时库源代码缺省情况下不被安装。如果您想查看其源代码,则需要重装 Visual C++ ,并在重装在时选中安装运行库源代码选项。

3.
各种 C 运行时库的区别

  ( 1 )静态链接的单线程库
  静态链接的单线程库只能用于单线程的应用程序 C 运行时库的目标代码最终被编译在应用程序的二进制文件中。通过 /ML 编译选项可以设置 Visual C++ 使用静态链接的单线程库。

  ( 2 )静态链接的多线程库
  静态链接的多线程库的目标代码也最终被编译在应用程序的二进制文件中,但是它可以在多线程程序中使用。通过 /MT 编译选项可以设置 Visual C++ 使用静态链接的单线程库。

  ( 3 )动态链接的运行时库
  动态链接的运行时库将所有的 C 库函数保存在一个单独的动态链接库 MSVCRTxx.DLL 中, MSVCRTxx.DLL 处理了多线程问题。使用 /MD 编译选项可以设置 Visual C++ 使用动态链接的运行时库。

   /MDd /MLd /MTd 选项使用 Debug runtime library( 调试版本的运行时刻函数库 ) ,与 /MD /ML /MT 分别对应。 Debug 版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,加强了对错误的检测,因此在运行性能方面比不上 Release 版本。

  下面看一个未正确使用 C 运行时库的控制台程序

#include <stdio.h>
#include <afx.h>
int main()
{
  CFile file;
  CString str("I love you");
  TRY
  {
   file.Open("file.dat",CFile::modeWrite | CFile::modeCreate);
  }
  CATCH( CFileException, e )
  {
   #ifdef _DEBUG
   afxDump << "File could not be opened " << e->m_cause << "\n";
   #endif
  }
  END_CATCH

  file.Write(str,str.GetLength());
  file.Close();
}

我们在 "rebuild all" 的时候发生了 link 错误:

nafxcwd.lib(thrdcore.obj) : error LNK2001: unresolved external symbol __endthreadex
nafxcwd.lib(thrdcore.obj) : error LNK2001: unresolved external symbol __beginthreadex
main.exe : fatal error LNK1120: 2 unresolved externals
Error executing cl.exe.

发生错误的原因在于 Visual C++ 对控制台程序默认使用单线程的静态链接库,而 MFC 中的 CFile 类已暗藏了多线程。我们只需要在 Visual C++6.0 中依次点选 Project->Settings->C/C++ 菜单和选项,在 Project Options 里修改编译选项即可。

不过最上面的那个程序在 6.0里面是可以运行的,现象同2003的是一样的。
***********************************************

 

从字面上看,运行库是程序在运行时所需要的库文件。通常运行库是以 DLL 形式提供的。 Delphi C++ Builder 的运行库为 .bpl 文件,实际还是一个 DLL 。运行库中一般包括编程时常用的函数,如字符串操作、文件操作、界面等内容。不同的语言所支持的函数通常是不同的,所以使用的库也是完全不同的,这就是为什么有 VB 运行库、 C 运行库、 Delphi 运行库之分的原因。即使都是 C++ 语言,也可能因为提供的函数不同,而使用不同的库。如 VC++ 使用的运行库和 C++ Builder 就完全不同。

如果不使用运行库,每个程序中都会包括很多重复的代码,而使用运行库,可以大大缩小编译后的程序的大小。但另一方面,由于使用了运行库,所以在分发程序时就必须带有这些库,比较麻烦。如果在操作系统中找不到相应的运行库程序就无法运行。为了解决这个矛盾, Windows 总是会带上它自己开发的软件的最新的运行库。象 Windows 2000 以后的版本都包括 Visual Basic 5.0/6.0 的库。 Internet Explorer 总是带有最新的 Visual C++ 6.0 的库。 Windows XP 带有 Microsoft .NET 1.0 (用于 VB.NET C# )的库。 Visual C++ Delphi C++ Builder 允许用户选择所编译得到的程序是否依赖于运行库。而 VB FoxPro PowerBuilder LabWindows/CVI Matlab 就不允许用户进行这种选择,必须依赖于运行库。

 

小结

看了上面这么多咚咚以后(我估计没几个人会有这个耐心把这么多东西看完的,娃哈哈),不过我还是把它完整地看完了,中间那一段是抄的,讲得很好,讲得非常清楚。嗯。有一点是可以肯定的,那就是不要用 ML 单线程版本,况且 2005 已经不支持 ML (注意,这里 ML 不是 Make L*ve 的缩写,汗!)了。另外, ML 不支持多线程的,所以如果使用 ML 来编译运行的话,肯定会出很多问题的,虽然它没有明确说出会发生什么样的问题。

一个困扰偶很长时间的问题终于解决。把 MLd 改为 MDd 所有问题就都解决了,用 Intel Thread Checker check 了一下也没问题。如果大家有碰到同样的问题的话,希望以上能够给你一点有用的信息。有啥问题,欢迎与我联系。有啥说的不对的,请批评指正。恩。

Have fun.

Feedback

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2007-03-27 21:36 by 小熊
晚上传了n多次图片,都没有成功,哭
CSDN一次就pass了。sigh

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2007-03-28 12:52 by 小熊
线程函数结束,线程就结束

# re: 多线程学习中碰到的一个很有意思的问题 [未登录]  回复  更多评论   

2007-03-30 09:27 by hdqqq
hThread[i] = CreateThread(NULL, 0, helloFunc, (LPVOID)i, 0, NULL);
上面这句是有问题的,因为CreateThread返回时候,并不保证线程已经启动了,所以进入下一个循环后i就被修改,导致线程取得了错误的参数.

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2007-03-30 09:34 by 小熊
但是一个线程函数不可能被执行多次的。呵呵

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2007-03-30 13:31 by think
这样写倒是吃惊:(LPVOID)i
你的程序在我机子上没有问题,也从来不遇到过

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2007-03-30 17:16 by 小熊
(LPVOID)i是没问题的
你可以把i理解为是一个指针之类的。。。

在你的机子上没问题的原因很可能是,你的运行时库设置为MD或者MT了,那肯定没问题。如果你设置为ML的话,肯定会有问题的,呵呵,或者说多运行几次。

# re: 多线程学习中碰到的一个很有意思的问题 [未登录]  回复  更多评论   

2007-04-05 09:16 by christanxw
本质原因是你在线程中使用了printf()这样的非多线程安全的函数。你用它的多线程版本(/MTd或/MDd)就没问题了,或者你在使用/MLd时对printf加锁也不会出问题。

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2007-04-05 21:44 by 小熊
恩。不过printf()也不可能让一个线程函数执行多次的吧?
使用了多线程版本后,printf()确实是不会有问题了。恩

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2008-10-09 17:11 by 5871
很不明白,.....

# re: 多线程学习中碰到的一个很有意思的问题   回复  更多评论   

2009-01-20 17:49 by sak
你这个人很有意思,哇哈哈
咚咚呛cei cei 呛

# re: 多线程学习中碰到的一个很有意思的问题 [未登录]  回复  更多评论   

2010-04-13 20:07 by Leon
这两天我也在做操作系统里多线程的实验,这方面的程序确实错得让人匪夷所思,我那还有一个关于多线程的实验,还有相关的错误总结,你如果想看的话愿意和你分享!
QQ:250575616

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理