ASP.NET中多线程问题

以前在使用VB来实现多线程的时候,发现有一定的难度。虽然也有这样那样的方法,但都不尽人意,但在C#中,要编写多线程应用程序却相当的简单。这篇文章将作简要的介绍,以起到抛砖引玉的作用!
    .NET将关于多线程的功能定义在System.Threading名字空间中。因此,要使用多线程,必须先声明引用此名字空间(using System.Threading;)。
       即使你没有编写多线程应用程序的经验,也可能听说过“启动线程”“杀死线程”这些词,其实除了这两个外,涉及多线程方面的还有诸如“暂停线程”“优先级”“挂起线程”“恢复线程”等等。下面将一个一个的解释。
    a.启动线程
    顾名思义,“启动线程”就是新建并启动一个线程的意思,如下代码可实现:
    Thread thread1 = new Thread(new ThreadStart( Count));
    其中的 Count 是将要被新线程执行的函数。
    b.杀死线程
    “杀死线程”就是将一线程斩草除根,为了不白费力气,在杀死一个线程前最好先判断它是否还活着(通过 IsAlive 属性),然后就可以调用 Abort 方法来杀死此线程。
    c.暂停线程
    它的意思就是让一个正在运行的线程休眠一段时间。如 thread.Sleep(1000); 就是让线程休眠1秒钟。
    d.优先级
    这个用不着解释了。Thread类中幸桓鯰hreadPriority属性,它用来设置优先级,但不能保证操作系统会接受该优先级。一个线程的优先级可分为5种:Normal, AboveNormal, BelowNormal, Highest, Lowest。具体实现例子如下:
    thread.Priority = ThreadPriority.Highest;
    e.挂起线程
    Thread类的Suspend方法用来挂起线程,知道调用Resume,此线程才可以继续执行。如果线程已经挂起,那就不会起作用。
    if (thread.ThreadState = ThreadState.Running)
    {
         thread.Suspend();
    }
    f.恢复线程
    用来恢复已经挂起的线程,以让它继续执行,如果线程没挂起,也不会起作用。
    if (thread.ThreadState = ThreadState.Suspended)
    {
         thread.Resume();
    }
    下面将列出一个例子,以说明简单的线程处理功能。此例子来自于帮助文档。
    using System;
    using System.Threading;
    // Simple threading scenario: Start a static method running
    // on a second thread.
    public class ThreadExample {
        // The ThreadProc method is called when the thread starts.
        // It loops ten times, writing to the console and yielding
        // the rest of its time slice each time, and then ends.
        public static void ThreadProc() {
            for (int i = 0; i < 10; i++) {
                Console.WriteLine("ThreadProc: {0}", i);
                // Yield the rest of the time slice.
                Thread.Sleep(0);
            }
        }
   
        public static void Main() {
            Console.WriteLine("Main thread: Start a second thread.");
            // The constructor for the Thread class requires a ThreadStart
            // delegate that represents the method to be executed on the
            // thread. C# simplifies the creation of this delegate.
            Thread t = new Thread(new ThreadStart(ThreadProc));
            // Start ThreadProc. On a uniprocessor, the thread does not get
            // any processor time until the main thread yields. Uncomment
            // the Thread.Sleep that follows t.Start() to see the difference.
            t.Start();
            //Thread.Sleep(0);
   
            for (int i = 0; i < 4; i++) {
                Console.WriteLine("Main thread: Do some work.");
                Thread.Sleep(0);
            }
   
            Console.WriteLine("Main thread: Call Join(), to wait until ThreadProc ends.");
            t.Join();
            Console.WriteLine("Main thread: ThreadProc.Join has returned. Press Enter to end program.");
            Console.ReadLine();
        }
    }

源文档 <http://www.haoxiai.net/wangzhanzhizuo/aspnet/54093.html>


前几天遇到了一个问题,我在页面逻辑里需要调用一个webservice,处理一个比较耗时的操作,但是我不需要知道其返回值。于是我希望ASP.NET能像winform一样使用自动生成的webservice异步方法,通过ASP.NET多线程来解决这一问题。
你是不是想说:在页面调用webservice的时候,直接调用其异步实现不就完了吗?
这其实是行不通的,为了实现异步调用,我们需要对页面进行小小的改动,在Page元素里加上Async=true
我们很快就会发现这样做的问题:
让我们测试一下吧,现在我们在一个webservice的Helloworld方法中放入一个Thread。Sleep(10000),然后调用他的异步实现。通过调试,我们可以发现虽然程序运行至HelloworldAsync时,非常快速的返回并往下运行,但是当所有逻辑处理完成后,页面并不Response,而是硬生生等待我们的线程睡醒了才返回。
可是如果我希望真正做到调了不管怎么办呢?

你可以使用ASP.NET多线程中的Thread,或者ThreadPool,自己来启动一个线程,我推荐使用ThreadPool,这样的话,这些线程都会被iis的线程池管理起来,不会造成崩溃
我们来分析一下ASP.NET多线程这两种模式的运用有什么特点
WebService自带的异步模式为下图的模式

这种模式适合无返回的情况,这种情况下,对子线程的调用应该越晚越好,我们可以看到,主、子线程共存的时间越短,我们的稀缺资源线程就越安全,请注意的是,也许总的执行时间不会比同步的情况更少,但是我们很快就返回了用户界面,所以用户体验能够得到提高
使用web多线程的缺点 :
看了上面的叙述,你也许会说,那干脆把我所有的调用都改成异步调用吧,你尽管去做吧,绝对是一场灾难,因为在异步的同时,一定一会产生一个新的线程等待调用的返回,即使你调用函数的返回值为void,所以异步调用的负面效果将是会产生许多子线程,所以注意当你的调用非常耗时,这个子线程也将长期占用你的线程池,如果这样的调用大量出现,照样会消耗掉所有的可用线程
那么什么情况下适合在web上使用哪种ASP.NET多线程模式呢
我们来看看这段伪代码,他的用途是提交一个报告,方法传入一个报告,并从一个WebService中获得一些报告的内容,接着插入数据库,然后在文件服务器上生成一个报告文件,最后发出一个通知,让我们逐条命令的过一下这个方法,看看什么地方适合改为异步调用?(记得我们的讨论都是基于web的,关于桌面运用的多线程请参考 多线程总结一)

1. public void CreateReport(Report report){
2.
3. //从webservice上取得报告的一些信息,
4. 不取得这些信息报告,报告是不完整的,是不能提交的
5.
6. Report fullreport=CallWebService(report);
7.
8. //插入数据库,很重要的工作
9.
10. InsertIntoDataBase(fullreport)
11.
12. try{
13.
14. //生成报告文件,这里是一个耗时而且容易出错的操作
15.
16. WriteStaticFile(fullreport)
17.
18. }
19.
20. catch{//记录错误日志。。。。}
21.
22. //这个只是通知邮件
23.
24. CallMailService2(fullreport)
25.
26. }
第一条语句CallWebService()从一个webservice里加载一些报告的内容,这个是业务逻辑相关的,因为如果不加载的话报告内容是不完整的,不能提交,显然不能改为异步调了不管的模式,在这里你可以尝试模式一,但是这个改动是没有作用的,因为其他所有的过程,包括插入数据库,生成报告都依赖于这个方法的返回,所以如果我们在这里使用异步的话,其他的所有操作都必须等待他的返回,所以采用异步除了多增加了线程以外,一点时间也不能节省
再来看插入数据库,和上面一样也没有必要使用异步调用
生成报告这里比较有趣,确实他是一个和逻辑息息相关的操作,但是通过分析代码,我们可以看出,虽然报告生成是一个重要业务步骤,但是并没有严格到说"如果不能生成报告,就必须回滚上面的操作",并且如果操作失败,在catch中也仅仅是记录了日志,并没有需要尝试重写的逻辑,(很有可能另外的某个程序或者某人,会定时查看日志,发现有错误就重新生成文件)也就是说,就这段代码而言,生成也可以算一个额外逻辑,那么自然也可以去异步操作.可是:千万注意!!
由于生成报告需要的时间较长,那么生成报告的子线程会长时间运行,长期无法返回线程池,如果请求量太大,频率太快,那就会耗尽线程资源了.
平心而论,这个问题其实不是异步造成的,即使时同步调用,执行此操作也需要化肥很长时间,调用量太大,频率太快,也会造成排队.而且由于返回时间太长,用户体验也不会好,所以我们的这个ASP.NET多线程的改造应该是有益的。

源文档 <http://developer.51cto.com/art/200907/138426.htm>


在.NET中编写的程序将被自动的分配一个线程.让我们来看看用C#编程语言创建线程并且继续学习线程的知识。我们都知道.NET的运行时环境的主线程由Main ()方法来启动应用程序,而且.NET的编译语言有自动的垃圾收集功能,这个垃圾收集发生在另外一个线程里面,所有的这些都是后台发生的,让我们无法感觉到发生了什么事情.在这里默认的是只有一个线程来完成所有的程序任务,但是正如我们在第一篇文章讨论过的一样,有可能我们根据需要自己添加更多的线程让程序更好的协调工作。比如说我们的例子中,一个有用户输入的同时需要绘制图形或者完成大量的运算的程序,我们必须得增加一个线程,让用户的输入能够得到及时的响应,因为输入对时间和响应的要求是紧迫的,而另外一个线程负责图形绘制或者大量的运算。

.NET 基础类库的System.Threading命名空间提供了大量的类和接口支持多线程。这个命名空间有很多的类,我们将在这里着重讨论Thread这个类。

System.Threading.Thread类是创建并控制线程,设置其优先级并获取其状态最为常用的类。他有很多的方法,在这里我们将就比较常用和重要的方法做一下介绍:

Thread.Start():启动线程的执行;

Thread.Suspend():挂起线程,或者如果线程已挂起,则不起作用;

Thread.Resume():继续已挂起的线程;

Thread.Interrupt():中止处于 Wait或者Sleep或者Join 线程状态的线程;

Thread.Join():阻塞调用线程,直到某个线程终止时为止

Thread.Sleep():将当前线程阻塞指定的毫秒数;

Thread.Abort():以开始终止此线程的过程。如果线程已经在终止,则不能通过Thread.Start()来启动线程。

通过调用Thread.Sleep,Thread.Suspend或者Thread.Join可以暂停/阻塞线程。调用Sleep()和Suspend()方法意味着线程将不再得到CPU时间。这两种暂停线程的方法是有区别的,Sleep()使得线程立即停止执行,但是在调用Suspend()方法之前,公共语言运行时必须到达一个安全点。一个线程不能对另外一个线程调用Sleep()方法,但是可以调用Suspend()方法使得另外一个线程暂停执行。对已经挂起的线程调用Thread.Resume()方法会使其继续执行。不管使用多少次Suspend()方法来阻塞一个线程,只需一次调用Resume()方法就可以使得线程继续执行。已经终止的和还没有开始执行的线程都不能使用挂起。Thread.Sleep(int x)使线程阻塞x毫秒。只有当该线程是被其他的线程通过调用Thread.Interrupt()或者Thread.Abort()方法,才能被唤醒。
如果对处于阻塞状态的线程调用Thread.Interrupt()方法将使线程状态改变,但是会抛出ThreadInterupptedException异常,你可以捕获这个异常并且做出处理,也可以忽略这个异常而让运行时终止线程。在一定的等待时间之内,Thread.Interrupt()和Thread.Abort()都可以立即唤醒一个线程。

下面我们将说明如何从一个线程中止另外一个线程。在这种情况下,我们可以通过使用Thread.Abort()方法来永久销毁一个线程,而且将抛出ThreadAbortException异常。使终结的线程可以捕获到异常但是很难控制恢复,仅有的办法是调用Thread.ResetAbort()来取消刚才的调用,而且只有当这个异常是由于被调用线程引起的异常。因此,A线程可以正确的使用Thread.Abort()方法作用于B线程,但是B线程却不能调用Thread.ResetAbort()来取消Thread.Abort()操作。
Thread.Abort()方法使得系统悄悄的销毁了线程而且不通知用户。一旦实施Thread.Abort()操作,该线程不能被重新启动。调用了这个方法并不是意味着线程立即销毁,因此为了确定线程是否被销毁,我们可以调用Thread.Join()来确定其销毁,Thread.Join()是一个阻塞调用,直到线程的确是终止了才返回。但是有可能一个线程调用Thread.Interrupt()方法来中止另外一个线程,而这个线程正在等待Thread.Join()调用的返回。
尽可能的不要用Suspend()方法来挂起阻塞线程,因为这样很容易造成死锁。假设你挂起了一个线程,而这个线程的资源是其他线程所需要的,会发生什么后果。因此,我们尽可能的给重要性不同的线程以不同的优先级,用Thread.Priority()方法来代替使用Thread.Suspend()方法。

Thread类有很多的属性,这些重要的属性是我们多线程编程必须得掌握的。

Thread.IsAlive属性:获取一个值,该值指示当前线程的执行状态。如果此线程已启动并且尚未正常终止或中止,则为 true;否则为 false。

Thread.Name 属性:获取或设置线程的名称。

Thread.Priority 属性:获取或设置一个值,该值指示线程的调度优先级。
Thread.ThreadState 属性:获取一个值,该值包含当前线程的状态。
在下面的例子中,我们将看看怎么设置这些属性,在随后的例子中我们将详细的讨论这些属性。
创建一个线程,首先得实例化一个Thread类,在类得构造函数中调用ThreadStart委派。这个委派包含了线程从哪里开始执行。当线程启动后,Start()方法启动一个新的线程。下面是例子程序。
using System;
using System.Threading ;
namespace LearnThreads
{
class Thread_App
{
public static void First_Thread()
{
Console.WriteLine("First thread created");
Thread current_thread = Thread.CurrentThread;
string thread_details = "Thread Name: " + current_thread.Name + "\r\nThread State: " + current_thread.ThreadState.ToString()+"\r\n Thread Priority level:"+current_thread.Priority.ToString();
Console.WriteLine("The details of the thread are :"+ thread_details);
Console.WriteLine ("first thread terminated");
}

public static void Main()
{
ThreadStart thr_start_func = new ThreadStart (First_Thread);
Console.WriteLine ("Creating the first thread ");
Thread fThread = new Thread (thr_start_func);
fThread.Name = "first_thread";
fThread.Start (); //starting the thread
}
}
}

在这个例子中,创建了一个fThread的线程对象,这个线程负责执行First_Thread()方法里面的任务。当Thread的Start() 方法被调用时包含First_Thread()的地址ThreadStart的代理将被执行。

Thread状态
System.Threading.Thread.ThreadState属性定义了执行时线程的状态。线程从创建到线程终止,它一定处于其中某一个状态。当线程被创建时,它处在Unstarted状态,Thread类的Start() 方法将使线程状态变为Running状态,线程将一直处于这样的状态,除非我们调用了相应的方法使其挂起、阻塞、销毁或者自然终止。如果线程被挂起,它将处于Suspended状态,除非我们调用resume()方法使其重新执行,这时候线程将重新变为Running状态。一旦线程被销毁或者终止,线程处于Stopped状态。处于这个状态的线程将不复存在,正如线程开始启动,线程将不可能回到Unstarted状态。线程还有一个Background状态,它表明线程运行在前台还是后台。在一个确定的时间,线程可能处于多个状态。据例子来说,一个线程被调用了Sleep而处于阻塞,而接着另外一个线程调用Abort方法于这个阻塞的线程,这时候线程将同时处于WaitSleepJoin和AbortRequested状态。一旦线程响应转为Sle阻塞或者中止,当销毁时会抛出ThreadAbortException异常。

线程优先级
System.Threading.Thread.Priority枚举了线程的优先级别,从而决定了线程能够得到多少CPU时间。高优先级的线程通常会比一般优先级的线程得到更多的CPU时间,如果不止一个高优先级的线程,操作系统将在这些线程之间循环分配CPU时间。低优先级的线程得到的CPU时间相对较少,当这里没有高优先级的线程,操作系统将挑选下一个低优先级 的线程执行。一旦低优先级的线程在执行时遇到了高优先级的线程,它将让出CPU给高优先级的线程。新创建的线程优先级为一般优先级,我们可以设置线程的优先级别的值,如下面所示:
Highest
AboveNormal
Normal
BelowNormal
Lowest

结论:在这一部分,我们讨论了线程的创建何线程的优先级。System.Threading命名空间还包含了线程锁定、线程同步何通讯、多线程管理类以及死锁解决等等高级特性,在后面的部分我们将继续讨论这些内容。

源文档 <http://9host.cn/asp.net/2007411213758.html>

 


上次做了个东西,用了多线程,现总结如下。
使用多线程是为了提高CPU的利用率,即在在相同的时间里面做更多的事情(但前提是系统资源没有完全耗尽),ASP.NET中使用多线程可以加快页面在服务器端的生成速度。一般页面生成过程中花费时间最多的是数据库查询阶段,如果你的页面有10个查询,不使用多线程的话,这10个查询将是串行执行的——即依次执行每一个查询。如果使用多线程,将可以使这10个查询几乎同时执行。这显然会提高页面的生成速度。
在网上搜索了些许帖子说在IIS进程中使用多线程是不稳定的,可我经过实践却发现ASP.NET使用多线程也没出啥问题。不过在ASP.NET中使用多线程得注意一些地方,不然确实是不稳定,甚至是行不通的。比如不能在多线程中使用HttpContext下的任何方法和属性,这就包括Cookie、Session、Response、Request、Application等等,当使用这些方法或者属性的时候,IIS进程将会直接崩溃。更要注意的是,由于多线程与页面的加载(Load)是异步执行的,必须让这些创建的线程在Load执行完之前同步,不然可能导致数据没有加载成功。 可能会有人问HttpContext等都被限制了,页面中还能做什么呢?我们完全可以把创建的线程与页面主体隔开,把需要的数据先在页面主体中获取,然后直接传入到创建的线程中就可解决。话不多说,具体如何,请看下文。
假设某个页面中有2个SQL查询,一个是根据Url传递来的参数P确定当前页的内容,另一个查询是显示所有分类。第一个查询语句需要用到Request.QueryString获取P传递来的页码,第二个查询语句则可直接写SQL语句。
假设第一个查询语句如:SELECT * FROM Archives WHERE Page=传递的页码
假设第二个查询语句如:SELECT * FROM Category
我们先创建一个类,用于接受参数P和数据绑定控件ID(此处使用Repeater控件绑定数据),此类能够把SQL语句查询的结果绑定到数据控件(Repeater)中。
复制代码
public classBindData
{
    private int currentPage = 1;
    private Repeater rpID;

    public BindData(Repeater rpID)
    {
        this.rpID = rpID;
    }
    public BindData(Repeater rpID,int page)
    {
        this.rpID = rpID;
        this.currentPage = page;
    }  
   

    public void BindCategory()
    {
        string strSql="SELECT * FROM Category";
        this.BindDataToRepeater(strSql, this.rpID);

    }

    public void BindArchive()
    {
        string strSql = string.Format("SELECT * FROM Archives WHERE Page={0}",this.currentPage);
        this.BindDataToRepeater(strSql, this.rpID);
    }

    private void BindDataToRepeater(string strSql, Repeater rp)
    {
        if (rp == null) return;
        SqlConnection conn = new SqlConnection("data source=数据服务器地址;User ID=用户名;pwd=密码;Initial Catalog=数据库名");
        SqlCommand cmd = new SqlCommand(strSql, conn);
        SqlDataReader dtr;
        try

        {
            conn.Open();
            dtr = cmd.ExecuteReader();
            controlID.DataSource = rp;
            controlID.DataBind();
            if (!dtr.IsClosed)
                dtr.Close();
        }
        catch { }
        finally
        {
            cmd.Dispose();
            if (conn.State = ConnectionState.Open)
                conn.Close();
        }

    }
}

1. 上面创建的BindData类中有2个构造函数,分别用于绑定分类、绑定Arhive的不同形式。如果使用其他数据绑定控件则可进行相应修改;
2. 创建了1个私有方法BindDataToRepeater用于把对应的SQL语句查询的结果绑定到对应的Repeater控件上。同时在此方法中使用了SqlDataReader,以提高绑定数据的速度。如果你使用了数据工厂可修改BindDataToRepeater中的具体实现过程;
3. 2个共有方法BindCategory和BindArchive分别用于创建不同SQL语句、设置Repater的ID;
4. 同时需要引入System.Web.UI、System.Web.UI.HtmlControls、System.Data.SqlClient3个必要的命名空间。
值得注意的是在BindDataToRepeater方法中使用了try..catch语句,但并没有在catch块中做任何事情,为什么我们用try.catch却不在catch块中做点什么事情呢,不是多此一举吗?使用try..catch是为了防止在执行BindDataToRepeater时抛出异常,若此处出现异常且此方法是在多线程中执行的,将会导致IIS进程崩溃,进而影响其他页面的正常执行,故而用try...catch防止BindDataToRepeater抛出错误。
我们之所以为数据绑定创建一个类,是为了提高内存利用率,当数据加载(Load)完毕的时候,为这个类创建的实例就会销毁。我们也可以通过在页面中创建几个全局变量来实现。但我还是建议以类的形式传递数据而不是使用全局变量。下面,我们开始在页面的Load中创建线程了。首先你需要在页面中引入System.Threading命名空间。
复制代码
protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            int Page = 1;
            if(Request.QueryString["p"]!=null)
                Page = Convert.ToInt32(Request.QueryString["p"]);

            BindData LoadArchives = new BindData(rpArhive,Page);
            Thread thArhives=new Thread(new ThreadStart(LoadArchives.BindArchive));
            thArhives.Start();
     
            BindData LoadCategory=new BindData(rpCategory);
            Thread thCategory = new Thread(new ThreadStart(LoadCategory.BindCategory));
            thCategory.Start();

            //进行页面的其他非数据绑定操作

 

            thArhives.Join();
            thCategory.Join();
          
           


        }
    }

上面的代码显示在!IsPostBack状态下绑定数据。利用Request.QueryString获取了当前页码,并创建了BindData的2个实例LoadArchives、LoadCategory,通过 Thread thArhives=new Thread(new ThreadStart(LoadArchives.BindArchive))为绑定Arhice创建线程,通过Thread thCategory = new Thread(new ThreadStart(LoadCategory.BindCategory))为绑定分类创建线程,同时调用Thread的Start方法使2个线程进入执行状态。最后,在Load的最下面用Thread的Join方法使创建的2个线程与页面加载同步。
值得注意的是,Join方法是必须的,如果不使用,可能导致创建的线程还未把数据完全绑定到Repeater上,Load就已经执行完毕,若如此页面上将没有任何数据。同时调用Start的代码行应尽量早,调用Join的代码行都应尽量迟——尽量放在Page_Load代码段的末尾,这样才能达到多线程的目的,若你每调用一个Start马上调用Join,其实质和没有使用多线程的效果是一样的。Join在MSND上的解释是:在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。
只要设置好ASPX页面Repeater的绑定项,数据就可成功加载了。上面仅仅展示了2个SQL语句的查询,如果你有10个或者更多的SQL查询,在Page_Load中创建10个线程,让他们异步执行,最后用Join同步到Load,是一个提高性能的不错方法。

posted @ 2012-05-16 16:16  光哥传说  阅读(2911)  评论(0编辑  收藏  举报