线程的概念
- 进程就好比工厂的车间,它代表CPU所能处理的单个任务。
- 线程就好比车间里的工人,一个进程可以包含多个线程。
- 车间里的控件是共享的,比如许多房间是每个工人都能进出的,这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享空间
- 一个防止别人进入的简单方法,就是给门口加一把锁,先到的人锁上门,后到的看到门上锁,就在门口排队,等锁打开再进去。这就叫互斥锁,防止多个线程同时读写某块内存区域。
- 还有一些房间,可以同时进入N个人,比如厨房,也就是说,如果人数大于N,多出来的人就只能在外面等着,这好比某些内存区域,只能供给固定的数目的线程使用。
- 这样的解决方法,就是在门口挂上N把钥匙,一个人进去就拿一把钥匙,出来时就把钥匙挂回来,后面的人发现钥匙架空了,说明里面人满了,就只能在门口排队,这种做法叫信号量,用来保证多个线程不会相互冲突。
一般我们会为比较耗时的操作开启单独的线程去执行,比如下载操作。
线程开启的方式1
Action a = Method;//声明一个委托 这里用于开启线程
a.BeginInvoke(参数1,参数2,...,null,null);//开启一个新的线程去执行a所引用的方法
IAsyncResult ar = a.BeginInvoke(...);//IAsyncResult可以获取当前线程的状态
ar.IsCompleted;//判断当前线程是否执行完毕
a.EndInvoke(ar);//取得异步线程的返回值
Thread.Sleep(10);//控制子线程的检测频率
bool isEnd = ar.AsyncWaitHandle.WaitOne(1000);//1000毫秒表示超过时间,如果等待了1000毫秒线程还没结束,会返回false,否则会返回true
线程开启的方式2
static void Test(object filename)
{
Console.WriteLine("开始下载"+filename+Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("下载完成!");
}
static void Main(string[] args)
{
//1.直接传递方法
Thread t = new Thread(Test);
//创建Thread对象,这个线程并没有启动 如果传递的方法有参数,则参数必须是Object类型的
t.Start("xxx");//开始,启动线程 通过Start方法传递参数
Console.ReadKey();
-----------------------------------------------------------------------------------------------------------------
//2.通过对象传递方法
MyThread myThread = new MyThread("abc","www.123.bbs");//创建类对象,将数据传入
Thread t = new Thread(myThread.DownLoad);//调用对象中的方法
t.Start();//开启线程
Console.ReadKey();
}
class MyThread
{
string filename;
string filepath;
public MyThread(string filename, string filepath)
{
this.filename = filename;
this.filepath = filepath;
}
public void DownLoad()
{
Console.WriteLine("开始下载" + filepath + filename);
Thread.Sleep(2000);
Console.WriteLine("下载完成");
}
}
异步回调方法
Func<string, int, int> a = Test;
a.BeginInvoke("A", 11, OnCallBack, a);//开启一个新的线程去执行委托中的方法
//倒数第二个参数是一个委托类型的参数,表示回调函数,当线程结束时会调用这个委托指向的方法
//倒数第一个参数用于给回调函数传递数据
Console.ReadKey();
}
static void OnCallBack(IAsyncResult ar)
{
Func<string, int, int> a = ar.AsyncState as Func<string, int, int>;
int res = a.EndInvoke(ar);
Console.WriteLine(res+"在回调函数中取得结果");
}
使用lambda表达式(等同于上面的代码)
static void OnCallBack(IAsyncResult ar)
{
Func<string, int, int> a = Test;
a.BeginInvoke("A", 11,ar=>
{
int res = a.EndInvoke(ar);
Console.WriteLine(res);
},null);
}
后台线程与前台线程
当只有一个前台线程在运行时,应用程序就在运行,如果多个前台线程在运行时,Main方法结束了,应用程序的进程仍然运行,直到所有前台线程完成其任务为止。
Thread t = new Thread(Test); //默认是前台线程
在默认情况下,用Thread创建的线程都是前台线程,在线程池里的线程都是后台线程。
t.IsBackground = true; //设置线程为后台线程
如果一个线程是后台线程,当应用程序关闭时,如果这个后台线程还没有执行完,则会被强制杀掉。
当所有前台线程运行完毕,如果还有后台线程运行的话,所有后台线程会被终止掉。
控制线程
1.获取线程状态(Running还是Unstarted..)当我们通过调用Thread对象的Start方法,可以创建线程,但是调用了Start方法之后,新线程不是马上进入Running状态,而是处于Unstarted状态,只有当操作系统的线程调度器选择运行这个线程,这个线程状态才会修改为Running状态。
我们使用Thread.Sleep()方法可以让当前线程休眠进入WaitSleepJoin状态。
t.Abort();//终止这个线程的执行
2.使用Thread对象的Abort()方法可以停止线程。调用这个方法,会在终止要终止的线程中抛出一个ThreadAbortException类型的异常,我们可以try catch这个异常,然后在线程结束前做一些清理的工作。
t.Jion();//让当前线程睡眠,等待t线程执行完,再继续执行当前线程
3.如果需要等待线程的结束,可以调用Thread对象的Join方法,表示把Thread加入进来,停止当前线程,并把它设置为WaitSleepJoin状态,直到加入的线程完成为止。
线程池
创建线程需要时间。如果有不同的小任务要完成,就可以事先创建许多线程 , 在应完成这些任务时发出请求。
使用线程池需要注意的事项:
- 线程池中的所有线程都是后台线程 。 如果进程的所有前台线程都结束了,所有的后台线程就会停止。 不能把入池的线程改为前台线程 。
- 不能给入池的线程设置优先级或名称。
- 入池的线程只能用于时间较短的任务。 如果线程要一直运行(如 Word的拼写检查器线程),就应使用Thread类创建一个线程。
ThreadPool.QueueUserWorkItem(Test,"种子1号");//开启一个工作线程 这个Test方法必须有一个参数 后面的是传递给Test方法的参数
例子:
static void Test(object filename)
{
Console.WriteLine("开始下载"+filename+Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
Console.WriteLine("下载完成!" + Thread.CurrentThread.ManagedThreadId);
}
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(Test,"种子1号");//发起一个工作线程队列
ThreadPool.QueueUserWorkItem(Test,"种子2号");
ThreadPool.QueueUserWorkItem(Test,"种子3号");
ThreadPool.QueueUserWorkItem(Test,"种子4号");
ThreadPool.QueueUserWorkItem(Test,"种子5号");
Console.ReadKey();
}
输出结果:
任务
开启任务的两种种方式:
(1)Task t = new Task(ThreadMethod);//传递一个需要线程去执行的方法
t.Start();//任务开启
(2)TaskFactory tf = new TasFactory();//创建一个任务的工厂
Task t = tf.StartNew(ThreadMethod); //通过StartNew方法开启
连续任务
Task t1=new Task(DoFirst);
Task t2=t1.ContinueWith(DoScend);
Task t3=t1.ContinueWith(DoScend);
Task t4=t2.ContinueWith(DoScend);
我们在一个任务中启动一个新的任务,相当于新的任务是当前任务的子任务,两个任务异步执行,如果父任务执行完了但是子任务没有执行完,它的状态会设置为WaitingForChildrenToComplete,只有子任务也执行完了,父任务的状态就变成RunToCompletion。
例如,执行时t2比t4先执行完毕,则会进入等待状态,只有等t4执行完毕才会变成完成状态。
线程争用问题
class MyThread
{
public int state = 5;
public void ChangeState()
{
state++;
if (state == 5)
{
Console.WriteLine("state=5");
}
state = 5;
}
}
.......
static void ChangeState(object o)
{
MyThread myThread = o as MyThread;
while (true)
{
myThread.ChangeState();
}
}
static void Main(string[] args)
{
MyThread m = new MyThread();
Thread t1 = new Thread(ChangeState);//创建一个线程
Thread t2 = new Thread(ChangeState);//创建另一个线程
t1.Start(m);
t2.Start(m);
}
当我们运行Main时
结果就会出现这段本不应该出现的语句
这是由于两个线程同时抢占了同一个MyThread对象也就是m的内存地址,导致一个线程中执行到state赋值为5的语句时,另一个线程中正好下一句在判断state是否等于5,这时就会出现线程争用的问题。
解决这个问题就需要给线程上锁
给上面的代码添加一个锁
while (true)
{
lock (myThread)//向系统申请获取并锁定该对象,如果该对象没有被锁定,则可以获取并执行后面的语句
//如果已经锁定,则这个语句会暂停,直到申请到该对象
{
myThread.ChangeState();
}
}
要注意,lock语句只能锁定引用类型。
线程死锁
public void Method1(){
while(true){
lock(a1){
lock(a2){
.......
}
}
}
}
public void Method2(){
while(true){
lock(a2){
lock(a1){
.......
}
}
}
}
此时,用两个线程分别调用这两个方法时,会出现死锁,当Method1锁定a1时,Method2锁定了a2,两个语句继续往下走,则都会暂停,因为下面要获取的对象都被锁定了。
要解决死锁问题,只有在编程开始设计阶段,就要确定好锁定的顺序。