我们在写程序的时候经常会用到线程技术。简单理解,线程就是可以让程序同时做两(多)件事。比如我们看直播,能在看视频直播和弹幕滚动能同时发生。如果单线程在直播的时候是不能发弹幕的,发送弹幕则不能直播。c#创建和使用线程是利用System.Threading命名空间下的Thread类,下面来介绍一下线程的概念以及如何使用。
Thread类
我们利用Thread类的方法可以实现线程的操作,比如创建、启动、终止、休眠等。我们创建的程序都会有一个Main()方法,Main()其实就在一个线程中执行,这个线程是我们生成程序时自动创建的。线程分为前台线程和后台线程。我们使用Thread创建的线程默认都是前台线程,在线程池里的线程属于后台线程,如果一个程序的所有前台线程都结束了,那么程序就会结束。后台线程也会结束(无论是否执行完毕),但是后台线程全部结束却不会影响前台线程和整个程序是否结束。我们可以通过IsBackground
属性来设置线程是否为后台线程。
创建和启动线程
线程是通过委托来创建的,实例化一个Thread,就能创建一个线程。线程的创建语法为Thread t = new Thread(myMethod)
,可以使用t.IsBackground = true;
来设置线程为后台线程。调用实例的Start()
方法即可启动线程。
举一个具体的例子:
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
//创建三个线程,分别打印三个字符
Thread printA = new Thread(PrintA);
Thread printB = new Thread(PrintB);
Thread printString = new Thread(PrintString);
//启动这三个线程
printA.Start();
printB.Start();
printString.Start("cc"); //启动带参数的线程
Console.WriteLine("Main Start!");
}
//打印字符A,50次
private static void PrintA()
{
for(int i = 0; i < 50; i++)
{
Console.Write("A");
}
}
private static void PrintB()
{
for(int i = 0;i < 50; i++)
{
Console.Write("B");
}
}
//带参数的方法只能为Object类型,
//如果是多个参数可以封装到一个类中,在线程中访问该类实例来访问数据
private static void PrintString(Object obj)
{
for(int i = 0;i < 50; i++)
{
Console.Write(obj);
}
}
}
}
执行该程序,可以看到如下(测试时结果可能不一样)。
我们可以很明显的看出来,打印的结果和我们执行代码的顺序是不一样的!这就是因为这三个函数在“同时”执行。
我们右键Thread
转到定义,可以如下代码:
//
// 摘要:
// Initializes a new instance of the System.Threading.Thread class, specifying a
// delegate that allows an object to be passed to the thread when the thread is
// started.
//
// 参数:
// start:
// A delegate that represents the methods to be invoked when this thread begins
// executing.
//
// 异常:
// T:System.ArgumentNullException:
// start is null.
public Thread(ParameterizedThreadStart start);
//
// 摘要:
// Initializes a new instance of the System.Threading.Thread class.
//
// 参数:
// start:
// A System.Threading.ThreadStart delegate that represents the methods to be invoked
// when this thread begins executing.
//
// 异常:
// T:System.ArgumentNullException:
// The start parameter is null.
public Thread(ThreadStart start);
可以看到参数start确实是一种委托(delegate)类型,至于使用哪种委托,要看定义的方法是否带参数,如果不带参数就自动使用第二种ThreadStart类型的委托调用该方法;如果带参数则使用ParameterizedThreadStart类型的委托调用该方法。另外通过这两种构造函数可以知道,我们还可以这样创建线程:
Thread a = new Thread(new ThreadStart(PrintA)); //不带参数
Thread c = new Thread(new ParameterizedThreadStart(PrintString)); //带参数
这种方式和例子中的代码是等价的,实际中使用哪种写法都可以。
传递多个参数
前面说过,给线程传递参数只能传递一个Object,但是我们想要传递多个数据怎么办?可以将数据封装到一个类中。在刚才的例子中做这样的修改:
...
Data data = new Data();
printString.Start(data);
...
private static void PrintString(Object obj)
{
Data data = obj as Data;
for(int i = 0;i < 25; i++)
{
Console.Write(data.c);
}
for (int i = 0; i < 25; i++)
{
Console.Write(data.d);
}
}
class Data
{
public string c = "cc";
public string d = "dd";
}
终止、取消和休眠线程
我们再看刚才的定义,可以看到有Abort()
和Join()
这个两个方法。这个Abort()方法的作用就是终止线程,我们可以在其他的线程中调用Thread实例的Abort方法来将该线程终止。但是使用这个方法是强制终止线程,属于非正常情况,实际上是取消线程(可能任务还没有完成,线程就被取消了)的执行而不是销毁线程。如果需要等待线程结束,可以调用Join()方法。A线程实例调用B线程的Join()方法后,B会进入等待状态,一直到A线程执行完毕后,B再进入执行状态。我们在第一版的代码上修改一下PrintA方法:
private static void PrintA()
{
Thread tj = new Thread(PrintString);
tj.Start("d");
for(int i = 0; i < 50; i++)
{
Console.Write("A");
if (i == 25)
tj.Join();
}
}
运行后会发现打印A字符过程中会打印tj线程的d字符,而且是等待d字符打印完50次后,A再继续打印。
当我们不希望当前线程继续执行,而是希望它停一会时,可以调用Thread的静态方法Sleep(),例如Thread.Sleep(1000)
,表示当前线程暂停1000ms,也就是1s。需要注意的时,Sleep方法只能暂停当前线程,不能在一个线程中暂停另一个线程。
线程优先级
线程实际上是由操作系统调用,如果给线程指定优先级那么可以改变线程的调用顺序。操作系统在调度线程时,首先调用优先级最高的线程。如果线程在等待(sleep指令、磁盘io等),那么它会停止运行并释放cpu资源。这时候优先级较低的线程会来抢占cpu。优先级相同的线程会被线程调度器循环调度,逐个交给cpu使用,如果线程被其他线程抢占,那么它就会排到最后。
我们创建的线程优先级默认是Normal,可以设置Priority属性来改变线程的优先级,例如t.priority = ThreadPriority.AboveNormal
。优先级级别有Highest,AboveNormal,BelowNormal和Lowest。注意,如果想把某线程设为最高级,会让其他线程难以被执行,程序可能会产生“假死”状态。
线程池(ThreadPool)
我们创建一两个线程的时候使用Thread来创建还是挺方便的,但是如果要创建许多线程时 ,会给系统增加很大的负担。这时我们要将暂时不需要的线程关掉,而当该线程再次需要时打开。如果我们手动来创建这样一个线程列表并维护,这会很复杂。ThreadPool能很好的帮助我们解决这个问题。线程池,顾名思义,是许多个线程的集合。当我们需要创建线程时,它会自动创建;当线程执行完毕是,它会自动将线程关闭。
前面说到过,线程池中的线程是后台线程,这些后台线程是相互关联,由ThreadPool统一调度的。线程池有一个最大线程数,我们创建的线程数如果多于这个最大值,那么就会进入等待队列等待(都是由线程池自动调度的),等某一个现有的线程执行完毕后再加入线程池执行队列执行。另外,一个池中的线程执行完任务后不会被销毁而是会返回等待队列,也就是说,一个线程执行结束了,它还可以被线程池重新使用而不必再次(都是自动的)。
下面来看一个例子:
class Program
{
static void Main(string[] args)
{
for(int i = 0;i < 5; i++)
{
//可以直接使用QueueUserWorkItem方法来向线程池中添加任务
ThreadPool.QueueUserWorkItem(new WaitCallback(tPool));
}
Thread.Sleep(1000);
}
static void tPool(Object state)
{
for(int i = 0; i < 3; i++)
{
Console.WriteLine("线程{0},正在执行第{1}次循环", Thread.CurrentThread.ManagedThreadId, i);
Thread.Sleep(100);
}
}
}
可以看到我们虽然创建了任务,但是实际上只有4个线程被使用,可以改变两次循环的参数再观察结果。
另外线程池还有几点需要注意:
- 线程池所有线程都是后台线程且不可以设置为前台线程
- 可以手动设置线程池中最大线程数,默认线程数与虚拟地址空间大小有关
- 线程池中的线程不是全部都在活跃状态,需要的时候才会有更多线程被激活
- 添加到线程池中的任务不是一定会被立即执行,如果当前可用线程不足则需要等待
- 不能给线程池中的线程设置优先级
- 线程池创建的线程不适合长期运行,适用于不确定线程的使用个数,但每个线程的运行时间不会很长的场景。比如QQ聊天,每打开一个聊天窗口则开启一个池中线程,关上聊天框就会自动结束线程,而QQ本身则应该在一个Thread创建的线程中运行。