进程与线程
进程
当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源,如Window句柄,文件系统句柄或其他内核对象。每个进程都分配的虚拟内存。
而一个进程又是由多个线程所组成的。
可以打开计算机设备管理查看自己电脑CPU数目,Ctrl+Alt+.调出任务管理器也可以查看,任务管理器还有详细的目前进程数目和线程数目。
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
线程
线程是程序中独立的指令流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),一个进程的内存空间是共享的,每个线程都可以使用这些共享空间。即不同的线程可以执行同样的函数。
任何程序在执行时,至少有一个主线程。
多线程
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
一个进程的多个线程可以同时运行在不同的CPU上或多核CPU的不同内核上。
多线程的好处:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
多线程的不利方面:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
- 多线程需要协调和管理,所以需要CPU时间跟踪线程
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
- 线程太多会导致控制太复杂,最终可能造成很多Bug
操作系统的设计
归结为三点:
- 以多进程形式,允许多个任务同时运行
- 以多线程形式,允许单个任务分成不同的部分运行
- 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
利用异步委托去创建线程
创建线程的一种简单方式是定义一个委托,并异步调用它。
委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。接下来定义一个方法,使用委托异步调用(开启一个线程去执行这个方法)。
当我们通过BeginInvoke开启一个异步委托的时候,返回的结果是IAsyncResult,我们可以通过它的AsyncWaitHandle属性访问等待句柄。
1 static string TakesAWhile(int times)
2 {
3 Console.WriteLine("异步函数开始!");
4 Thread.Sleep(times);//程序运行到这里的时候会暂停ms毫秒
5 return "异步结束!";
6 }
7 public delegate string TakesAWhileDelegate(int ms);// 声明委托
8 static void Main(string[] args)
9 {
10 TakesAWhileDelegate _delegateTasks = TakesAWhile;
11 IAsyncResult ar = _delegateTasks.BeginInvoke(3000, null, null);
12 while (ar.IsCompleted == false)
13 {
14 Console.Write("-");
15 Thread.Sleep(10);//每隔10mswhile循环一次,Thread类控制线程
16 }
17 Console.WriteLine(_delegateTasks.EndInvoke(ar));
18 }
这个属性返回一个WaitHandler类型的对象,它中的WaitOne()方法可以等待委托线程完成其任务,WaitOne方法可以设置一个超时时间作为参数(要等待的最长时间),如果发生超时就返回false。
1 static string TakesAWhile(int times)
2 {
3 Console.WriteLine("异步函数开始!");
4 Thread.Sleep(times);//程序运行到这里的时候会暂停ms毫秒
5 return "异步结束!";
6 }
7 public delegate string TakesAWhileDelegate(int ms);// 声明委托
8 static void Main(string[] args)
9 {
10
11 TakesAWhileDelegate _delegateTasks = TakesAWhile;
12 IAsyncResult ar = _delegateTasks.BeginInvoke(3000, null, null);
13 while (true)
14 {
15 Console.Write("-");
16 if (ar.AsyncWaitHandle.WaitOne(10, false))// 等待每隔10ms,如果当前异步操作执行完毕时就会返回一个true
17 {
18 Console.WriteLine("异步结束!");
19 break;
20 }
21 }
22 Console.WriteLine(_delegateTasks.EndInvoke(ar));
23 }
等待委托的结果的第3种方式是使用异步回调。在BeginInvoke的第三个参数中,可以传递一个满足AsyncCallback委托的方法,AsyncCallback委托定义了一个IAsyncResult类型的参数其返回类型是void。
对于最后一个参数,可以传递任意对象,以便从回调方法中访问它。(我们可以设置为委托实例,这样就可以在回调方法中获取委托方法的结果)
格式如下:
委托对象.BeginInvoke(委托的参数列表, 回调函数,对象);
例子如下:
1 static string TakesAWhile(int times)
2 {
3 Console.WriteLine("异步函数开始!");
4 Thread.Sleep(times);//程序运行到这里的时候会暂停ms毫秒
5 return "异步结束!";
6 }
7 public delegate string TakesAWhileDelegate(int ms);// 声明委托
8 static void Main(string[] args)
9 {
10
11 TakesAWhileDelegate _delegateTasks = TakesAWhile;
12
13 _delegateTasks.BeginInvoke(3000, TakesAWhileCompleted,_delegateTasks);
14 while (true)
15 {
16 Console.Write("-");
17 Thread.Sleep(10);
18 }
19
20 }
21 static void TakesAWhileCompleted(IAsyncResult ar)
22 {//回调方法是从委托线程中调用的,并不是从主线程调用的,可以认为是委托线程最后要执行的程序
23 if (ar == null) throw new ArgumentNullException("ar");
24 TakesAWhileDelegate _temp = ar.AsyncState as TakesAWhileDelegate;
25 Console.Write(_temp.EndInvoke(ar));
26 }
可以使用在BeginInvoke中使用Lambda表达式,更方便:
1 _delegateTasks.BeginInvoke(3000, ar => Console.WriteLine(_delegateTasks.EndInvoke(ar)), null);
2 while (true)
3 {
4 Console.Write("-");
5 Thread.Sleep(10);
6 }
开启BeginInvoke后,判断线程是否结束的方法,总结如下:
- 利用IAsyncResult中的IsCompleted属性判断是否完成
- 获取IAsyncResult中的AsyncWaitHandle.WaitOne()线程超时是否,超时的返回参数true
- 利用BeginInvoke的第三个参数AsyncCallback委托的方法
利用Thread类去创建和控制线程
MSDN查阅地址:
https://msdn.microsoft.com/zh-cn/library/system.threading.thread(v=VS.95).aspx
使用Thread类可以创建和控制线程,并获取其状态。Thread构造函数的参数是一个的委托类型。
例如:
1 static void Main(string[] args)
2 {
3 Thread _nextThread = new Thread(ThreadNext);
4 _nextThread.Start();//线程开启
5 Console.WriteLine("主线程运行!");
6 Thread.Sleep(50);
7 _nextThread.Abort();//终止线程
8 Console.WriteLine("线程终止!");
9 }
10 static void ThreadNext() {
11 while (true)
12 {
13 Console.WriteLine("线程开启—");
14 }
15 }
若要向Thread类传值,有两种方法,第一种:使用带ParameterizedThreadStart委托参数的Thread构造函数
官方描述的Thread类两个构造函数
需注意:线程不会在创建时开始执行。 若要为执行而调度线程,请调用 Start 方法。 若要将数据对象传递给线程,请使用 Start(Object) 方法重载。
特别注意:传递的值为Object对象!
使用例子:
1 static void Main(string[] args)
2 {
3 string str ="线程进行--";
4 Thread _nextThread = new Thread(ThreadNext);
5 _nextThread.Start(str);//传入一个对象
6 Console.WriteLine("主线程运行!");
7 Thread.Sleep(20);
8 _nextThread.Abort();//终止线程
9 Console.WriteLine("线程终止!");
10 }
11 static void ThreadNext(object str) {//这里参数类型必须为object对象!
12 while (true)
13 {
14 Console.WriteLine(str);
15 }
16 }
还有一种方法:初始化一个对象,对象内部的方法去调用对象里面的成员,线程方法(实例方法)就可以调用内部成员达到传值的目的。
使用例子如下:
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 UserThread myThread = new UserThread("线程开启");
6 Thread _nextThread = new Thread(myThread.WriteMessage);
7 _nextThread.Start();//传入一个对象
8 Console.WriteLine("主线程运行!");
9 Thread.Sleep(20);
10 _nextThread.Abort();//终止线程
11 Console.WriteLine("线程终止!");
12 }
13 }
14 class UserThread
15 {
16 private string message;
17 public UserThread(string message)
18 {
19 this.message = message;
20 }
21 public void WriteMessage()
22 {
23 while (true)
24 {
25 Console.WriteLine(message);
26 }
27
28 }
29 }
线程的控制
在默认情况下,用Thread类创建的线程是前台线程。线程池中的线程总是后台线程。
常用的属性和方法:
- 线程前后台的控制。在用Thread类创建线程的时候,可以设置IsBackground属性,表示它是一个前台线程还是一个后台线程。
- 线程的优先级。Thead类中设置Priority属性,以影响线程的基本优先级 ,Priority属性是一个ThreadPriority枚举定义的一个值。定义的级别有Highest ,AboveNormal,BelowNormal 和 Lowest
- 线程插入。可以调用Thread对象的Join方法,表示把Thread加入进来,停止当前线程,并把它设置为WaitSleepJoin状态,直到加入的线程完成为止
- 终止线程。使用Thread对象的Abort()方法可以停止线程。
- 开始线程的。将当前实例的状态更改为 ThreadState.Running。
- 睡眠当前线程。Thread.Sleep()方法可以让当前线程休眠进入WaitSleepJoin状态
线程争用问题:
当程序较大时,运行程序的时候,在计算机有限的资源下,无法避免会产生多个线程争用的问题,对数据进行多次或没有修改。
解决方案为使用Lock关键字,锁住引用对象。Lock只能锁引用对象!
操作如下:
当数据非应用类型,我们可以通过在对象初始化时,同时初始化一个object类型的变量sync,每次修改数据对象时都先锁定sync对象。
使用例子:
static void RaceCondition(object o ){
StateObject state = o as StateObject;
int i = 0;
while(true){
lock(state){
state.ChangeState(i++);
}
}
}
或者
private object sync = new object();
public void ChangeState(int loop){
lock(sync){
if(state==5){
state++;
Console.WriteLine("State==5:"+state==5+" Loop:"+loop);
}
state = 5;
}
}
线程死锁问题:
同时出现两个锁,两个线程都在等另一个线程解锁。
1 public class SampleThread{
2 private StateObject s1;
3 private StateObject s2;
4 public SampleThread(StateObject s1,StateObject s2){
5 this.s1= s1;
6 this.s2 = s2;
7 }
8 public void Deadlock1(){
9 int i =0;
10 while(true){
11 lock(s1){
12 lock(s2){
13 s1.ChangeState(i);
14 s2.ChangeState(i);
15 i++;
16 Console.WriteLine("Running i : "+i);
17 }
18 }
19 }
20 }
21 public void Deadlock2(){
22 int i =0;
23 while(true){
24 lock(s2){
25 lock(s1){
26 s1.ChangeState(i);
27 s2.ChangeState(i);
28 i++; Console.WriteLine("Running i : "+i);
29 }
30 }
31 }
32 }
33 }
34 var state1 = new StateObject();
35 var state2 = new StateObject();
36 new Task(new SampleTask(s1,s2).DeadLock1).Start();
37 new Task(new SampleTask(s1,s2).DeadLock2).Start();
解决方法就是一开始就设定好锁定的先后顺序。
线程池
线程池不需要自己创建,ThreadPool类是系统提供管理线程的线程池类。这个类会在需要时增减池中线程的线程数,直到达到最大的线程数。 池中的最大线程数是可配置的。
创建线程需要时间。 如果有不同的小任务要完成,就可以事先创建许多线程 , 在应完成这些任务时发出请求。 这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。。
在双核 CPU中 ,默认设置为1023个工作线程和 1000个 I/o线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。 如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
任务
在.NET4 新的命名空间System.Threading.Tasks包含了类抽象出了线程功能,在后台使用的ThreadPool进行管理的。
任务表示应完成某个单元的工作。这个工作可以在单独的线程中运行,也可以以同步方式启动一个任务。
启动任务三种方法:
///TaskMethod表示一个委托方法
/// <summary>
/// 启动任务t1
/// </summary>
TaskFactory _Taskfactory = new TaskFactory();
Task t1 = _Taskfactory .StartNew(TaskMethod);
/// <summary>
/// 启动任务t2
/// </summary>
Task t2 = TaskFactory.StartNew(TaskMethod);
/// <summary>
/// 启动任务t3
/// </summary>
Task t3 = new Task(TaskMethod);
t3.Start();
连续任务
连续任务的特点是,连续任务的开启必要条件是上一个任务已经完成。也就是说:
如果一个任务t1的执行是依赖于另一个任务t2的,那么就需要在这个任务t2执行完毕后才开始执行t1。这个时候我们可以使用连续任务。
1 static void DoFirst(){
2 Console.WriteLine("开始任务 : "+Task.CurrentId);
3 Thread.Sleep(3000);
4 }
5 static void DoSecond(Task t){//t为上一个任务
6 Console.WriteLine("任务"+t.Id+" finished.");//上一个任务已完成
7 Console.WriteLine("this task id is "+Task.CurrentId);
8 Thread.Sleep(3000);
9 }
10 static void Main(string[] args){
11 Task t1 = new Task(DoFirst);
12 Task t2 = t1.ContinueWith(DoSecond);
13 }
任务的层次结构
我们在一个任务中启动一个新的任务,相当于新的任务是当前任务的子任务,两个任务异步执行,如果父任务执行完了但是子任务没有执行完,它的状态会设置为WaitingForChildrenToComplete,只有子任务也执行完了,父任务的状态就变成RunToCompletion。