C#线程(二、创建使用多线程)

本文详细介绍了在 C# 中使用多线程的各种方法,包括通过委托、Thread 类及不同委托类型创建线程,还讲解了如何设置线程名称、前台与后台线程的区别、线程优先级调整等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参自:http://www.cnblogs.com/miniwiki/archive/2010/06/18/1760540.html

http://blog.youkuaiyun.com/lyh916/article/details/46340021http://www.devsiki.com/


创建和开始使用多线程


一、委托方式发起线程:

static int Test(int i,string str)
{
   Console.WriteLine("test"+i+str);
   Thread.Sleep(100);//让当前线程休眠(暂停线程的执行) 单位ms
   return 100;
}
static void Main(string[] args) { //在main线程中执行 一个线程里面语句的执行 是从上到下的
//1,通过委托 开启一个线程
Func<int, string, int> a = Test;
IAsyncResult ar = a.BeginInvoke(100, "siki", null, null);// 开启一个新的线程去执行 a所引用的方法 
// IAsyncResult 可以取得当前线程的状态
//可以认为线程是同时执行的(异步执行)
Console.WriteLine("main");
while (ar.IsCompleted == false)//如果当前线程没有执行完毕
{
     Console.Write(".");
     Thread.Sleep(10); //控制子线程的检测频率
}
int res = a.EndInvoke(ar);//取得异步线程的返回值
Console.WriteLine(res);}
结果:

main
test100siki
..........100
检测进程结束:

static int Test(int i,string str)
        {
            Console.WriteLine("test"+i+str);
            Thread.Sleep(100);//让当前线程休眠(暂停线程的执行) 单位ms
            return 100;
        }
static void Main(string[] args) {//在main线程中执行 一个线程里面语句的执行 是从上到下的
            //1,通过委托 开启一个线程
            Func<int, string, int> a = Test;
            IAsyncResult ar = a.BeginInvoke(100, "siki", null, null);// 开启一个新的线程去执行 a所引用的方法 
            Console.WriteLine("main");
	//检测线程结束
            bool isEnd = ar.AsyncWaitHandle.WaitOne(1000);
            //1000毫秒表示超时时间,如果等待了1000毫秒 线程还没有结束的话 那么这个方法会返回false 如果在1000毫秒以内线程结束了,那么这个方法会返回true
            if (isEnd)
            {
                int res = a.EndInvoke(ar);
                Console.WriteLine(res);
            }
}
结果:

main
test100siki
100
通过回调 检测线程结束:

static int Test(int i,string str)
        {
            Console.WriteLine("test"+i+str);
            Thread.Sleep(100);//让当前线程休眠(暂停线程的执行) 单位ms
            return 100;
        }
static void Main(string[] args) {//在main线程中执行 一个线程里面语句的执行 是从上到下的
	//通过回调 检测线程结束
            Func<int, string, int> a = Test;
            //倒数第二个参数是一个委托类型的参数,表示回调函数,就是当线程结束的时候会调用这个委托指向的方法 倒数第一个参数用来给回调函数传递数据
            IAsyncResult ar = a.BeginInvoke(100, "siki", OnCallBack, a);// 开启一个新的线程去执行 a所引用的方法           
            Console.ReadKey();
	}
static void OnCallBack( IAsyncResult ar )
        {
            Func<int, string, int> a = ar.AsyncState as Func<int, string, int>;
            int res =  a.EndInvoke(ar);
            Console.WriteLine(res+"在回调函数中取得结果");
        }

结果:test100siki
100在回调函数中去的结果

上面的回调函数还可以写成lambda表达式形式:

 a.BeginInvoke(100, "siki", ar =>
            {
                int res = a.EndInvoke(ar);
                Console.WriteLine(res + "在lambda表达式中取得");
            }, null);

二、通过Thread发起线程:

1.ThreadStart委托

线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的:

public delegate void ThreadStart();
调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创建TheadStart委托:

class ThreadTest {
  static void Main() {
    Thread t = new Thread (new ThreadStart (Go));
    t.Start();   // Run Go() on the new thread.
    Go();        // Simultaneously run Go() in the main thread.
  }
  static void Go() { Console.WriteLine ("hello!"); }
在这个例子中,线程t执行Go()方法,大约与此同时主线程也调用了Go(),结果是两个几乎同时hello被打印出来:

hello!
hello!
一个线程可以通过C#堆委托简短的语法更便利地创建出来:

static void Main() {
  Thread t = new Thread (Go);    // No need to explicitly use ThreadStart
  t.Start();
  ...
}
static void Go() { ... }
在这种情况,ThreadStart被编译器自动推断出来,另一个快捷的方式是使用匿名方法来启动线程:
static void Main() {
  Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });
  t.Start();
}
线程有一个IsAlive属性,在调用Start()之后直到线程结束之前一直为true。一个线程一旦结束便不能重新开始了。


2.ParameterizedThreadStart委托:带参委托

话又说回来,在上面的例子里,我们想更好地区分开每个线程的输出结果,让其中一个线程输出大写字母。我们传入一个状态字到Go中来完成整个任务,但我们不能使用ThreadStart委托,因为它不接受参数,所幸的是,.NET framework定义了另一个版本的委托叫做ParameterizedThreadStart, 它可以接收一个单独的object类型参数

public delegate void ParameterizedThreadStart (object obj);
之前的例子看起来是这样的:
class ThreadTest {
  static void Main() {
    Thread t = new Thread (Go);
    t.Start (true);             // == Go (true) 
    Go (false);
  }
  static void Go (object upperCase) {
    bool upper = (bool) upperCase;
    Console.WriteLine (upper ? "HELLO!" : "hello!");
  }
结果:
hello
HELLO!


在整个例子中,编译器自动推断出ParameterizedThreadStart委托,因为Go方法接收一个单独的object参数,就像这样写:

Thread t = new Thread (new ParameterizedThreadStart (Go));
t.Start (true);
ParameterizedThreadStart的特性是在使用之前我们必需对我们想要的类型(这里是bool)进行装箱操作,并且它只能接收一个参数。

一个替代方案是使用一个匿名方法调用一个普通的方法如下:
static void Main() {
  Thread t = new Thread (delegate() { WriteText ("Hello"); });
  t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }

优点是目标方法(这里是WriteText),可以接收任意数量的参数,并且没有装箱操作。不过这需要将一个外部变量放入到匿名方法中,向下面的一样:

static void Main() {
  string text = "Before";
  Thread t = new Thread (delegate() { WriteText (text); });
  text = "After";
  t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }
匿名方法打开了一种怪异的现象,当外部变量被后来的部分修改了值的时候,可能会透过外部变量进行无意的互动。有意的互动(通常通过字段)被认为是足够了!一旦线程开始运行了,外部变量最好被处理成只读的——除非有人愿意使用适当的锁。

另一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么,如下列重写了原来的例子:
class ThreadTest {
  bool upper;
  
  static void Main() {
    ThreadTest instance1 = new ThreadTest();
    instance1.upper = true;
    Thread t = new Thread (instance1.Go);
    t.Start();
    ThreadTest instance2 = new ThreadTest();
    instance2.Go();        // 主线程——运行 upper=false
  }
  
  void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }


3.命名线程


线程可以通过它的Name属性进行命名,这非常有利于调试:可以用Console.WriteLine打印出线程的名字,Microsoft Visual Studio可以将线程的名字显示在调试工具栏的位置上。线程的名字可以在被任何时间设置——但只能设置一次,重命名会引发异常。

程序的主线程也可以被命名,下面例子里主线程通过CurrentThread命名:
class ThreadNaming {
  static void Main() {
    Thread.CurrentThread.Name = "main";
    Thread worker = new Thread (Go);
    worker.Name = "worker";
    worker.Start();
    Go();
  }
  static void Go() {
    Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
  }
}
Hello from main
Hello from worker

4.前台和后台线程

线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。 C#也支持后台线程,当所有前台线程结束后,它们不维持程序的存活

.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。 


一个线程是前台线程还是后台线程可由它的IsBackground属性来决定。这个属性是可读又可写的。它的默认值为false,即意味着一个线程默认为前台线程。我们可以将它的IsBackground属性设置为true,从而使之成为一个后台线程。 
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态

线程的IsBackground属性控制它的前后台状态,如下实例:

using System;  
using System.Threading;  
  
class ThreadTest11  
{  
    static void Main(string[] args)  
    {  
        for (int i = 0; i < 10; i++)  
        {  
            Thread t = new Thread(Go);  
            //若添加这句话,则创建完十个线程之后就会退出控制台程序  
            //若注释这句话,则等待五秒后就会退出控制台程序  
            //t.IsBackground = true;  
            t.Start();  
        }   
    }  
  
    static void Go()  
    {  
        DateTime start = DateTime.Now;  
        while ((DateTime.Now - start).Seconds < 5) ;  
    }  
}  

后台线程终止的这种方式,使任何最后操作都被规避了,这种方式是不太合适的。好的方式是 明确等待任何后台工作线程完成后再结束程序,可能用一个timeout(大多用Thread.Join)。如果因为某种原因某个工作线程无法完成,可以用试图终止它的方式,如果失败了,再抛弃线程,允许它与 与进程一起消亡。(记录是一个难题,但这个场景下是有意义的)

拥有一个后台工作线程是有益的,最直接的理由是它当提到结束程序它总是可能有最后的发言权。交织以不会消亡的前台线程,保证程序的正常退出。

抛弃一个前台工作线程是尤为险恶的,尤其对Windows Forms程序,因为程序直到主线程结束时才退出(至少对用户来说),但是它的进程仍然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却可以在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。


对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。


既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的IsBackground属性呢?

下面是一些基本的原则: 对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。比如一个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,那么那个线程就该是作为后台线程的。而对于一些服务于用户界面的线程往往是要设置为前台线程的,因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里我只是给出了一些原则,具体到实际的运用往往需要编程者的进一步仔细斟酌。 

5.线程优先级

线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
只有多个线程同时为活动时,优先级才有作用。

设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程的级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别,像下面这样:(我没有告诉你如何做到这一点:))
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

ProcessPriorityClass.High 其实是一个短暂缺口的过程中的最高优先级别:Realtime。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最高的有用进程级别。

如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。(虽然在写这篇文章的时候,在互联网电话程序Skype侥幸地这么做, 也许是因为它的界面相当简单吧。)

 降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。

最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信,共享内存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和 MapViewOfFile)

6.异常处理

任何线程创建范围内try/catch/finally块,当线程开始执行便不再与其有任何关系。考虑下面的程序:

public static void Main() {
 try {
   new Thread (Go).Start();
 }
 catch (Exception ex) {
   // 不会在这得到异常
   Console.WriteLine ("Exception!");
 }
 
 static void Go() { throw null; }
}
这里try/catch语句一点用也没有,新创建的线程将引发NullReferenceException异常。当你考虑到每个线程有独立的执行路径的时候,便知道这行为是有道理的,

补救方法是在线程处理的方法内加入他们自己的异常处理:
public static void Main() {
   new Thread (Go).Start();
}
  
static void Go() {
  try {
    ...
    throw null;      // 这个异常在下面会被捕捉到
    ...
  }
  catch (Exception ex) {
    记录异常日志,并且或通知另一个线程
    我们发生错误
    ...
  }

 从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着忽略异常不再是一个选项了。因此为了避免由未处理异常引起的程序崩溃, try/catch块需要出现在每个线程进入的方法内,至少要在产品程序中应该如此。对于经常使用“全局”异常处理的Windows Forms程序员来说,这可能有点麻烦,像下面这样:
using System;
using System.Threading;
using System.Windows.Forms;
  
static class Program {
  static void Main() {
    Application.ThreadException += HandleError;
    Application.Run (new MainForm());
  }
  
  static void HandleError (object sender, ThreadExceptionEventArgs e) {
    记录异常或者退出程序或者继续运行...
  }
}

Application.ThreadException事件在异常被抛出时触发,以一个Windows信息(比如:键盘,鼠标活着 "paint" 等信息)的方式,简言之,一个Windows Forms程序的几乎所有代码。虽然这看起来很完美,它使人产生一种虚假的安全感——所有的异常都被中央异常处理捕捉到了。由工作线程抛出的异常便是一个没有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代码,包括构造器的形式,在Windows信息开始前先执行)

.NET framework为全局异常处理提供了一个更低级别的事件: AppDomain.UnhandledException,这个事件在任何类型的程序(有或没有用户界面)的任何线程有任何未处理的异常触发。尽管它提供了好的不得已的异常处理解决机制,但是这不意味着这能保证程序不崩溃,也不意味着能取消.NET异常对话框。

在产品程序中,明确地使用异常处理在所有线程进入的方法中是必要的,可以使用包装类和帮助类来分解工作来完成任务,比如使用BackgroundWorker类(在第三部分进行讨论)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值