1. 进程和线程的概念

线程

对于所有需要等待的操作,例如移动文件,数据库和网络访问都需要一定的时间,此时就可以启动一个新的线程,同时完成其他任务。

一个进程的多个线程可以同时运行在不同的CPU上或多核CPU的不同内核上。

线程是程序中独立的指令流。

在VS编辑器中输入代码的时候,系统会分析代码,用下划线标注遗漏的分号或其他语法错误,这就是用一个后台线程完成。

Word文档需要一个线程等待用户输入,另一个线程进行后台搜索,第三个线程将写入的数据存储在临时文件中。

运行在服务器上的应用程序中等待客户请求的线程称为侦听器线程。

进程

进程包含资源,如WIndows句柄,文件系统句柄或其他内核对象。每个进程都分配了虚拟内存,一个进程至少包含一个线程

一个程序启动,一般会启动一个进程,然后进程启动多个线程。

进程和线程的一个简单解释(资料来源-阮一峰)

  1. 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
  2. 如果工厂的电力有限一次只能供给一个车间使用。也就是说一个车间开工的时候,其他车间就必须停工。背后的含义就是单个CPU一次只能运行一个任务(多核CPU可以运行多个任务)。
  3. 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
  4. 一个车间里可以有很多工人,他们协同完成一个任务。
  5. 线程就好比车间里的工人,一个进程可以包括多个线程。
  6. 车间的控件是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享空间。
  7. 一个防止他人进入的简单方法,就是门口加一把锁(厕所)。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫”互斥锁”(Mutual exclusion,缩写为Mutex)防止多个线程同时读写某一块内存区域。
  8. 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
  9. 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做”信号量”(Semaphore),用来保证多个线程不会互相冲突。

不难看出,Mutex是Semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为Mutex较为简单且效率高,所以在必须保证资源独占的情况下,可以采用这种设计。

操作系统的设计,因此可以归结为三点:

  • 以多进程形式,允许多个任务同时运行;
  • 以多线程形式,允许单个任务分成不同的部分运行;
  • 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

2. 线程开启方式-异步委托(BeginInvoke)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Program
{
static void Test()
{
Console.WriteLine("Test");
}
static void Main(string[] args)
{
Action a = Test;
a.Invoke();
Console.WriteLine("Main");
}
}
//output:
//Test
//Main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Program
{
static void Test(int i,string str)
{
Console.WriteLine("Test" + i + str);
}
static void Main(string[] args)
{
Action<int,string> a = Test;
a.Invoke(66,"AAA");
Console.WriteLine("Main");
}
}
//output:
//Test66AAA
//Main

有参有返回值线程

  • using System.Threading;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
using System.Threading;

namespace Thread_Task_and_Sync_Tset
{
class Thread_Test1
{
static int Test(string str,int i)
{
Console.WriteLine("Test"+str+i);
//使用thread.sleep直接挂起进程
Thread.Sleep(100);
return 100;
}
static void Main(string[] args)
{
//要异步,先委托
Func<string,int,int> a = Test;
//使用BeginInvoke开启异步,此方法返回一个IAsyncResult,可以表示异步执行的状态
IAsyncResult ar = a.BeginInvoke("Fuck", 66, null, null);
Console.WriteLine("Main");
//根据IAsyncResult状态输出点号
while (ar.IsCompleted == false)
{
Console.Write(". ");
Thread.Sleep(10);
}
//使用EndInvoke获得IAsyncResult的结果
int res = a.EndInvoke(ar);
Console.WriteLine(res);
}
}
}
//output:
//Main
//TestFuck66
//.......100

Invokebegininvoke区别在于,Invoke会阻塞当前线程,直到Invoke调用结束,才会继续执行下去,而begininvoke 则可以异步进行调用,也就是该方法封送完毕后马上返回,不会等待委托方法的执行结束,调用者线程将不会被阻塞。

3. 检测委托线程的结束(通过等待句柄AsyncWaitHandler和回调函数)

AsyncWaitHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Threading;

namespace Thread_Task_and_Sync_Tset
{
class Thread_Test1
{
static int Test(string str,int i)
{
Console.WriteLine("Test"+str+i);
//使用thread.sleep直接挂起进程
Thread.Sleep(1000);
return 100;
}
static void Main(string[] args)
{
//要异步,先委托
Func<string,int,int> a = Test;
//使用BeginInvoke开启异步,此方法返回一个IAsyncResult,可以表示异步执行的状态
IAsyncResult ar = a.BeginInvoke("Fuck", 66, null, null);
//使用AsyncWaitHandle.WaitOne来等待异步方法执行,如果在指定时间(1100ms)内没有执行完,
//就执行下一句
bool isFinished = ar.AsyncWaitHandle.WaitOne(1100);
if (isFinished)
{
int res = a.EndInvoke(ar);
Console.WriteLine(res);
}
Console.WriteLine("Main");
}
}
}
//output:
//TestFuck66
//100
//Main

回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Threading;

namespace Thread_Task_and_Sync_Tset
{
class Thread_Test1
{
static int Test(string str,int i)
{
Console.WriteLine("Test"+str+i);
//使用thread.sleep直接挂起进程
Thread.Sleep(1000);
return 100;
}
static void Main(string[] args)
{
//要异步,先委托
Func<string,int,int> a = Test;
//倒数第二个参数是一个委托类型的参数,表示回调函数,就是当线程结束的时候会调用这个委托指向的方法
//倒数第一个参数用来给回调函数传递数据
IAsyncResult ar = a.BeginInvoke("Fuck", 66, 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);
}
}
}
//output:
//TestFuck66
//100

直接使用lambda表达式来写回调函数内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Threading;

namespace Thread_Task_and_Sync_Tset
{
class Thread_Test1
{
static int Test(string str,int i)
{
Console.WriteLine("Test"+str+i);
//使用thread.sleep直接挂起进程
Thread.Sleep(1000);
return 100;
}
static void Main(string[] args)
{
//要异步,先委托
Func<string,int,int> a = Test;
//倒数第二个参数是一个委托类型的参数,表示回调函数,就是当线程结束的时候会调用这个委托指向的方法
//倒数第一个参数用来给回调函数传递数据
a.BeginInvoke("Fuck", 66,
ar =>
{
int res = a.EndInvoke(ar);
Console.WriteLine(res);
},null);

Console.ReadKey();
}
}
}

结果与上图相同

4. 线程开启方式2-通过Thread类

Thread类

使用Thread类可以创建和控制线程。Thread构造函数的参数是一个无参无返回值的委托类型(Action)

1
2
3
4
5
6
7
8
static void Main(){
var t1 = new Thread(ThreadMain);
t1.Start();
Console.WriteLine("This is the main thread.");
}
static void ThreadMain(){
Console.WriteLine("Running in a thread.");
}

在这里哪个先输出是无法保证的,线程的执行由操作系统决定,我们只能知道Main线程和分支线程是同步执行的。在Thread的构造函数中传递一个方法,然后调用Thread.Start(),就会开启一个线程去执行传递的方法。

Thread类-Lambda表达式

既然Thread的构造参数是委托类型,那么肯定可以用Lambda表达式

1
2
3
4
5
static void Main(){
var t1 = new Thread( ()=> Console.WriteLine("Running in a thread,id : " + Thread.CurrentThread.ManangedThreadId) );
t1.Start();
Console.WriteLine("This is the main Thread. ID: " + Thread.CurrentThread.ManangedThreadId);
}

&#x20;Thread类是可以接受以Object为参数的方法的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace ThreadClass
{

class ThreadClassTest
{
static void MethodForThread1()
{
Thread.Sleep(1000);
Console.WriteLine("This is for Thread without param");
}
static void MethodForThread2(object something)
{
Thread.Sleep(2000);
Console.WriteLine("This is for Thread with param"+something);
}
static void Main(string[] args)
{
Thread t1 = new Thread(MethodForThread1);
t1.Start();

Thread t2 = new Thread(MethodForThread2);
t2.Start("333");

Thread t3 = new Thread(() =>
{
Thread.Sleep(3000);
Console.WriteLine("This is for Thread with Lambda");
});
t3.Start();
}
}
}
//output:
//This is for Thread without param
//This is for Thread with param333
//This is for Thread with Lambda

给线程传递数据-通过委托

给线程传递一些数据可以采用两种方式,一种方式是使用带ParameterizedThreadStart委托参数的Thread构造函数,一种方式是创建一个自定义类,把线程的方法定义为实例方法,这样就可以初始化实例数据,之后启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
public struct Data{//声明一个结构体来传输数据
public string Message;
}
static void ThreadMainParameters(Object o){
Data d = (Data)o;
Console.WriteLine("Running in a thread,received : "+ d.Message);
}
static void Main(){
var d = new Data(Message = "Info");
var t2 = new Thread(ThreadMainWithParameters);
t2.Start(d);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
namespace ThreadClass
{
class ClassForThread
{
private string fileName;
private string filePath;

public ClassForThread(string fileName,string filePath)
{
this.fileName = fileName;
this.filePath = filePath;
}

public void Download()
{
Console.WriteLine("现在正在开始下载"+fileName+filePath);
Thread.Sleep(1000);
Console.WriteLine("下载完成");
}
}
class ThreadClassTest
{
static void Main(string[] args)
{
ClassForThread myThread = new ClassForThread("小黄片儿", "C://My Documents");
//构造Thread时候,可以传递一个静态方法,也可以传递一个动态普通方法
Thread myt = new Thread(myThread.Download);
myt.Start();

Console.ReadKey();
}


}
}
//output:
//现在正在开始下载小黄片儿C://My Documents
//下载完成

5. 线程的其它概念后台和前台线程,线程的优先级,线程的状态

后台和前台线程

只有一个前台线程在运行,应用程序的进程就在运行,如果多个前台线程在运行,但是Main方法结束了,应用程序的进程仍然是运行的,直到所有的前台线程完成其任务为止。

在默认情况下,用Thread类的创建的线程是前台线程。线程池中的线程总是后台线程。

在用Thread类创建线程的时候,可以设置IsBackground属性,表示它是一个前台线程还是一个后台线程。

看下面例子中前台线程和后台线程的区别:

1
2
3
4
5
6
7
8
9
10
11
12
class Program{
static void Main(){
var t1 = new Thread(ThreadMain){IsBackground = false};
t1.Start();
Console.WriteLine("Main thread ending now.");
}
static void ThreadMain(){
Console.WriteLine("Thread +" +Thread.CurrentThread.Name + "started");
Thread.Sleep(3000);
Console.WriteLine("Thread +" +Thread.CurrentThread.Name + "started");
}
}

后台线程用的地方:如果关闭Word应用程序,拼写检查器继续运行就没有意义了,在关闭应用程序的时候,拼写检查线程就可以关闭。

当所有的前台线程运行完毕,如果还有后台线程运行的话,所有的后台线程会被终止掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace ThreadClass
{
class ThreadClassTest
{
static void MethodForThread3(object obj)
{
Console.WriteLine("Downloading..." + Thread.CurrentThread.ManagedThreadId + obj);
Thread.Sleep(2000);
Console.WriteLine("Download Complete");
}
static void Main(string[] args)
{
Thread mythread = new Thread(MethodForThread3);
mythread.IsBackground = true;
mythread.Start("PornVideo");

Console.ReadKey();
}
}
}
//output:
//Downloading...3PornVideo
//Download Complete

线程的优先级

线程由操作系统调度,一个CPU线程同一时间只能做一件事情(运行一个线程中的计算任务),当有很多线程需要CPU去执行的时候,线程调度器会根据线程的优先级去判断先去执行哪一个线程,如果优先级相同的话,就使用一个循环调度规则,逐个执行每个线程。

在Thread类中,可以设置Priority属性,以影响线程的基本优先级,Priority属性是一个ThreadPriority枚举定义的一个值。定义的级别有Highest,AboveNormal,BelowNormal,和Lowest。

控制线程

  1. 获取线程的状态(Running还是Unstarted),当我们通过调用Thread对象的Start方法,可以创建线程,但是调用了Start方法之后,新线程不是马上进入Running状态,而是处于Unstarted状态,只有当操作系统的线程调度器选择了要运行的线程,这个线程的状态才会修改为Running状态。我们使用Thread.Sleep()方法可以让当前线程休眠进入WaitSleepJoin状态。
  2. 使用Thread对象的Abort()方法可以停止线程。调用这个方法,会在要终止的线程中抛出一个ThreadAbortException类型的异常,我们可以try catch这个异常,然后在结束线程前做一些清理的工作。
  3. 如果需要等待线程的结束,可以调用Thread对象的Join方法,表示把Thread加入进来,停止当前线程,并把它设置为WaitSleepJion状态,直到加入的线程完成为止。

6. 线程开启方式3-线程池

创建线程需要时间。如果有不同的小任务要完成,就可以事先创建许多线程,在应完成这些任务时发出请求。这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。

不需要自己创建线程池,系统已经有一个ThreadPool类管理线程。这个类会在需要时增减池中的线程数,直到达到最大的线程数。池中最大线程数是可配置的。在双核CPU中,默认设置为1023个工作线程和1000个I/O线程。

CPU的线程数指的是当前CPU能够并发执行的最大线程数(比如16线程、32线程等),线程池中的线程是一个缓存的概念,它们通过调度、抢占轮番执行,所以一个线程池能容纳几千个线程

可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。

如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。

线程池示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;
using System.Threading;

namespace ThreadPoolTest
{
class ThreadPoolTest1
{
static void Main(string[] args)
{
int nWorkerThreads;
int nCompletionFortThreads;
//使用out关键字将方法内获得的参数赋值给变量
ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionFortThreads);
Console.WriteLine("Max Worker Threads:" + nWorkerThreads + " I/O Completion Threads:" + nCompletionFortThreads);
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(JobForAThread);

}
Thread.Sleep(3000);
Console.ReadKey();
}
static void JobForAThread(object state)
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine("Loop " + i + " is running in pooled thread " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(300);
}
}
}
}

示例应用程序首先要读取工作线程和IO线程的最大线程数,把这些信息写入控制台中,接着在for循环中,调用ThreadPool.QueueUserWorkItem方法,传递一个WaitCallBack类型的委托,把JobForAThread方法赋予线程池中的线程。线程池收到这个请求后,就会从池中选择一个线程来调用该方法。如果线程池还没有运行,就会创建一个线程池,并启动第一个线程。如果线程池已经在运行,且有一个空闲线程来完成该任务,就把该作业传递给这个线程。

使用线程池需要注意的事项

  • 线程池中所有的线程都是后台线程,如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。
  • 不能给入池的线程设置优先级或名称。
  • 入池的线程只能用于时间较短的任务,如果线程要一直运行(如Word的拼写检查器线程),就应使用Thread类创建一个线程。

7. 线程开启方式4-任务

任务

在.Net4新的命名空间System.Threading.Tasks包含的类抽象出了线程的功能,在后台使用的ThreadPool进行管理的。任务表示应完成某个单元的工作。这个工作可以在单独的线程中运行,也可以以同步方式启动一个任务。

任务也是异步编程中的一种实现方式,而且是现在微软首推的方式。

启动任务

启动任务的三种方式

1
2
3
4
5
6
TaskFactory tf = new TaskFactory();
Task t1 = tf.StartNew(TaskMethod);
Task t2 = TaskFactory.StartNew(TaskMethod);
Task t3 = new Task(TaskMethod);
t3.Start();
//我们创建任务的时候有一个枚举类型的选项TaskCreationOptions