【C#】进程中最大线程创建数量分析

作者:凡星
QQ:184167125
一、 简介
多线程在当今软件开发中应用非常广泛,在实际开发过程中,发现很多不正确使用线程的现象,其中一点就是线程使用过于随意,导致进程中的线程数量过多,从而引起内存占用、性能浪费等问题。
本文将分别研究32位进程和64位进程下,可以创建最大线程数量及资源占用情况,供研发及设计人员参考。
研发环境:Win7 64位,.Net 3.5,VS2010
二、 基本概念
(以下是线程基本概念,高手可略过)
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
三、 分析内容
1、 方案
创建并运行线程,将线程保存在列表中,分别测试在32位进程和64位进程下,能够创建的最大线程数及资源占用情况。
2、 相关技术
1) 线程相关
我们都知道,每个线程都会占用一定的资源,最大的一部分是堆栈,线程的堆栈默认大小是1M(可通过api函数修改)。
2) 进程相关
进程的内存是有上限的,32位程序有4GB虚地址空间(用户可用空间有3GB),64位进程有8TB的虚地址空间。当进程占用的内存超过3G时,就会崩溃退出,这里的“进程内存”是指物理内存和虚拟内存之和。
3) 进程内存类型
进程的内存占用分为物理内存和虚拟内存,见下图。在做本测试前,请将您的任务管理器设置成如下所示。
【C#】进程中最大线程创建数量分析_第1张图片
3、 关键代码
程序界面很简单,要创建线程数量的输入框(默认创建10000个),开始创建的按钮,已创建线程数量的提示,和信息输出框。
【C#】进程中最大线程创建数量分析_第2张图片
1) 创建线程
创建线程采用如下代码,创建后台线程(程序退出,线程即退出),线程的工作函数是OpThread.

          Thread th = new Thread(OpThread);
            th.IsBackground = true;
            th.Start();

线程中的代码内容仅仅是Sleep,确保线程不退出,同时,在线程结束时增加信息输出。

     private void OpThread()
        {
            try
            {
                Thread.Sleep(500000);
            }
            catch (System.Exception ex)
            {
                MessageBox.Show(ex.StackTrace);
            }
            finally
            {
                AddTipMsg("线程退出.");
            }
        }

2) 开始创建
创建的线程保存到List中,为了能在线程创建时显示创建数量,防止程序无响应,因此,

        #region 最大线程测试
        List _lstThread = new List();
        private void btnCreat_Click(object sender, EventArgs e)
        {
            Thread th = new Thread(CreateThread);
            th.IsBackground = true;
            th.Start();
        }
        private void CreateThread()
        {
            int MaxCount = int.Parse(txtCount.Text);
            AddTipMsg(string.Format("开始创建线程, 总数:{0} 个", MaxCount));
            for (int i = 0; i < MaxCount; i++)
            {
                Thread th = new Thread(OpThread);
                th.IsBackground = true;
                th.Start();
                SetTip(string.Format("已创建线程数量:{0} 个", i+1));
                _lstThread.Add(th);
                Thread.Sleep(10);
            }
            AddTipMsg(string.Format("列表中有线程数:{0}", _lstThread.Count));
        }
        private void OpThread()
        {
            try
            {
                Thread.Sleep(500000);
            }
            catch (System.Exception ex)
            {
                MessageBox.Show(ex.StackTrace);
            }
            finally
            {
                AddTipMsg("线程退出.");
            }
        }
        #endregion

3) 设置信息输出
因为在线程中不能直接给界面上控件赋值,必须要在主线程中操作控件,此处的两个文本输出函数均采用BeginInvoke方式异步在主线程中输出信息。

        public void AddTipMsg(string src)
        {
            this.BeginInvoke(new MethodInvoker(() =>
            {
                string str = txtTipMsg.Text;
                str += string.Format("{0} {1}\r\n", DateTime.Now.ToString("hh:mm:ss"), src);
                txtTipMsg.Text = str;

            }));
        }
        public void SetTip(string src)
        {
            this.BeginInvoke(new MethodInvoker(() =>
            {
                labTip.Text = src;
            }));
        }

4) 设置进程异常
在测试时,有可能会出现内存溢出等问题(不再本文讨论范围,不做研究)导致进程崩溃退出,为了查看方便,增加进程崩溃时异常捕捉。如下所示:

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
    AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionEventHandler);

    Application.Run(new frmMain());
}
static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
        Exception ex = (Exception)e.Exception;
        MessageBox.Show(ex.Message + ex.StackTrace);
}
static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
    Exception ex = (Exception)e.ExceptionObject;
    MessageBox.Show(ex.Message+ex.StackTrace);
}

4、 进程编译
在64位Win7下,可以将目标编译成32位或64位的程序,如下图所示:
【C#】进程中最大线程创建数量分析_第3张图片
5、 实际测试
1)32位进程
执行前内存占用:
【C#】进程中最大线程创建数量分析_第4张图片
执行后内存占用:
这里写图片描述
执行后程序界面:显示的线程和任务管理器稍有不同,任务管理器是定时刷新进程信息的。每个线程平均占用内存约1.1M。
【C#】进程中最大线程创建数量分析_第5张图片
同时程序报如下错误:
【C#】进程中最大线程创建数量分析_第6张图片
多执行几次会发现,最大创建的线程数会有差别,但都在个位数。
【C#】进程中最大线程创建数量分析_第7张图片
2)64位进程
执行程序并直到出错后可以看到,
a) 程序内存占用已近13G;
b) 线程最大创建数量3138个;平均每个线程占用内存4.1M;
c) 操作系统报内存溢出错误;
内存占用:
这里写图片描述
程序界面:
【C#】进程中最大线程创建数量分析_第8张图片
操作系统报错:
【C#】进程中最大线程创建数量分析_第9张图片
四、 总结
在本机环境下,由上可得出以下结论:
1、32位进程最大可创建线程1335个左右,共占内存1.5G,每个线程占用内存约1.1M;虽然没有测试此时进程的运行情况,但可以推断出,此时进程已无法正常工作。
2、64位进程最大可创建线程数3138个,共占内存13G,每个线程占用内存约4.1M;此时操作系统报内存溢出。进程无法正常运行。
根据工作中的经验,多线程的程序创建线程最好控制在200以内,否则程序的性能将会受到影响。这一点在监控系统中要尤为注意,监控系统中往往会多个设备建立Socket连接,通常采用1个线程管理1个连接的方式实现。但是,在设备数量过多时(如:几千个设备),就需要本文提到的最大线程数量的问题,怎样进行管理,就需要特别注意。我会在后续的文章中进行分析。

测试程序下载地址稍后公布。

你可能感兴趣的:(技术预研)