线程
Thread类在System.Threading程序集中,在初步学习线程时,线程之间的状态转换以及Thread提供的以下几个方法需要了解:
- 线程状态转移图:
Start:操作系统将当前实例的状态更改为 Running。(运行态)
Join:等待由该实例表示的线程终止。
Sleep:将当前线程的执行暂停指定的时间。
Suspend:挂起线程,或者如果线程已挂起,则不起作用。
Resume:继续已挂起的线程。
创建线程
使用构造器来创建线程,构造器接受一个委托表示线程要执行的动作,由委托的具体实现函数有没有参数分别调用两种构造函数:
- 无参数的构造器
//创建并启动线程
Thread t1 = new Thread(MyFuction);
t1.Start();
//MyFuction函数体
static void MyFuction(Object a)
{
for(int i = 0;i < 100; i++)
{
Console.Write("2");
}
}
- 有参数的构造器
static void Main(string[] args)
{
//创建线程并传入参数
Thread t = new Thread(MyFuction);
t.Start(10);
for(int i = 0; i < 100; i++)
{
Console.Write("1");
}
}
//具体函数的参数声明
static void MyFuction(Object a)
{
for(int i = 0;i < 100; i++)
{
Console.Write("2");
}
}
注意:创建线程传入的参数类型只能声明为Object,并且是在Start方法中将参数传入。
运行结果如图:
向线程传递数据
除了上述的通过Object对象来封装参数并传递给Thread对象的方式,使用Lambda表达式更为简洁和有效:
static void Main()
{
Thread t = new Thread( () => {
MyFuction(prama1,prama2);
});
}
static void MyFuction(object prama1,object prama2)
{
//业务逻辑
....
}
对比两种方式,使用Lambda表达式实际上实在方法中再次调用其他有参数的方法来传值,而使用构造器的方式需要将所有参数封装进一个object。
注意
在线程开始后不要随意修改参数变量的值,举例:
for(int i = 0;i < 10;i++)
{
new Thread(() => {
Console.Write(i)
}).Start();
}
以上程序输出的结果可能不是想象中的0123456789
而可能是0444555999这样不确定的结果。
原因在于创建线时传入的参数i,在整个循环周期中都在同一个内存地址,创建的十个线程在执行具体的打印任务时访问的也都是同一个地址。
本地状态和共享状态
本地状态
CLR为每个线程分配了独立的内存栈来保证局部变量的隔离。
static void Main()
{
new Thread(MyFuction).Start();
MyFuction();
}
static void MyFuction()
{
for(int i = 0;i < 10;i++)
Console.Write("1");
}
以上代码会输出20个1,因为变量i在两个线程中是隔离的。
共享状态
导致共享状态的原因是多个线程使用了同一个引用,造成这样的结果有以下几种方式:
- Lambda表达式将捕获到的变量转换为字段
static void Main()
{
bool _done = false;
ThreadStart myFuction = () => {
if(!_done){
_done = true;
Console.Write("Done");
}
};
new Thread(myFuction).Start();
myFuction();
}
Done只会被打印一次而不是两次,因为Lambda表达式将_done转换为了字段,造成两个线程使用了同一个_done。
- 多个线程使用了同一个实例对象的引用,那么对象的数据就会被多个线程共享
class ThreadTest
{
bool _done;
static void Main(){
//创建了一个共享的实例
Thread t = new ThreadTest();
new Thread(myFuction).Start();
myFuction();
}
}
- 多个线程共享静态字段
class ThreadTest
{
//被共享的静态字段
static bool _done;
static void Main(){
new Thread(myFuction).Start();
myFuction();
}
}
共享数据会造成不安全的操作,多个线程共享变量时,上述的几种方式有概率会打印两次Done。
锁与线程安全
在多个线程中共享数据的基本处理方法:C#的lock语句
class ThreadTest
{
static bool _done;
//排他锁
static readonly object _locker = new object();
static void Main(){
new Thread(myFuction).Start();
myFuction();
}
static void myFuction(){
//排他锁
lock (_locker)
{
if(!_done){
Console.Write("Done");
_done = true;
}
}
}
}
两个线程竞争锁资源时,一个线程会等待另一个线程释放资源,保证数据安全。
线程中的异常处理
线程执行与创建线程的异常处理流程无关,举例:
static void Main()
{
try{
new Thread(myFuction).Start();
}catch{
Console.Write("捕获异常");
}
}
static void myFuction(){
//抛出一个空引用异常
throw null;
}
上面的例子中永远都捕获不到异常,新创建的线程依旧会被空引用异常影响。
正常的方式应该时将异常处理的流程放进myFuction方法内。
前台程序与后台程序
Thread类中的IsBackground属性标记线程是否是后台程序。
- 显示创建的线程为前台线程,即
IsBackground属性为rue; - 只要有一个前台线程还在运行,应用程序就仍然保持运行状态。
- 当所有前台线程结束时,应用程序就会停止,且所有运行的后台线程也会随之终止。 举例:
static void Main(string[] args)
{
Thread thread = new Thread(() =>
{
Console.ReadLine();
});
if (args.Length > 0)
{
thread.IsBackground = false;
}
thread.Start();
}
从命令行运行上述程序,不带参数时(dotnet run)线程处于前台状态,会一直等待用户输入而不会结束;
如果应用程序启动时带有参数,则工作线程就会设置为后台状态,而应用程序也将在主线程结束时退出,从而终止ReadLine的执行。