CSharp之多线程编程

线程

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadTest{
public static readonly object locker = new object();
public void FuncWithParameter(object obj){
lock(locker){
Console.WriteLint(obj);
}
}
public void FuncWithoutParameter(){
lock(locker){
Console.WriteLine("无参方法")
}
}
}
public class Program{
static void Main(string[] args){
ThreadTest test = new ThreadTest();
Thread threadWithParameter = new Thread(new ParameterizedThreadStart(test.FuncWithParameter));
Thread threadWithoutParameter = new Thread(new ThreadStart(test.FuncWithoutParameter));
threadWithParameter.Start("有参方法");
threadWithoutParameter.Start();
}
}

线程方法

方法 描述 执行后状态 -
Start 启动线程
Suspend 挂起线程
Resume 继续已挂起的线程
Interrupt 终止处于WaitSleepJoin线程状态的线程
Join 阻塞调用线程,直到某个线程终止时为止
Sleep 将当前线程阻塞指定的毫秒数
Abort 终止线程

线程状态

状态 描述 - -
Aborted 线程已停止;
AbortRequested 线程的Thread.Abort()方法已被调用,但是线程还未停止;
Background 线程在后台执行,与属性Thread.IsBackground有关;
Running 线程正在正常运行;
Stopped 线程已经被停止;
StopRequested 线程正在被要求停止;
SuspendRequested 线程正在要求被挂起,但是未来得及响应;
Unstarted 未调用Thread.Start()开始线程的运行;
WaitSleepJoin 线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态;

通过使用AutoResetEvent和ManualResetEvent类来控制循环线程的暂停与继续 保证了每次循环的完整性。

简单来说只有调用Set()方法后才能执行WaitOne()后面的代码,AutoResetEvent和ManualResetEvent分别都有Set()改变为有信号 ,Reset()改变为无信号,WaitOne()将会阻塞当前调用的线程,直到有信号为止,即执行了Set()方法,WaitOne()方法还可以带指定时间的参数。

理解了AutoResetEvent后再理解ManualResetEvent也就不难了,AutoResetEvent与ManualResetEvent的区别是,AutoResetEvent.WaitOne()会自动改变事件对象的状态,即AutoResetEvent.WaitOne()每执行一次,事件的状态就改变一次,也就是从无信号变为有信号,或从有信号变为无信号。而ManualResetEvent则是调用Set()方法后其信号量不会自动改变,除非再设置Reset()方法。

在.Net多线程编程中,AutoResetEvent和ManualResetEvent这两个类经常用到, 他们的用法很类似,但也有区别。Set方法将信号置为发送状态,Reset方法将信号置为不发送状态,WaitOne等待信号的发送。可以通过构造函数的参数值来决定其初始状态,若为true则非阻塞状态,为false为阻塞状态。如果某个线程调用WaitOne方法,则当信号处于发送状态时,该线程会得到信号, 继续向下执行。其区别就在调用后,AutoResetEvent.WaitOne()每次只允许一个线程进入,当某个线程得到信号后,AutoResetEvent会自动又将信号置为不发送状态,则其他调用WaitOne的线程只有继续等待.也就是说,AutoResetEvent一次只唤醒一个线程;而ManualResetEvent则可以唤醒多个线程,因为当某个线程调用了ManualResetEvent.Set()方法后,其他调用WaitOne的线程获得信号得以继续执行,而ManualResetEvent不会自动将信号置为不发送。也就是说,除非手工调用了ManualResetEvent.Reset()方法,则ManualResetEvent将一直保持有信号状态,ManualResetEvent也就可以同时唤醒多个线程继续执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Program{
//ManualResetEvent
static AutoResetEvent autoResetEvent = new AutoResetEvent(false);//初始设置为false为默认阻塞线程
static void FuncAutoResetEvent(){
autoResetEvent.WaitOne();//等待autoResetEvent的Set信号,执行后状态自动设置为false,保证只有一个进程进入,而ManualResetEvent激活后不再设回false,可以同时唤醒多个线程。
Console.WriteLine("线程继续执行.......");
}
static void Main(string[] args){
Thread thread = new Thread(new ThreadStart(FuncAutoResetEvent));
thread.Start();
ConsoleKeyInfo key = Console.ReadKey();
if(key.Key == ConsoleKey.Y){
autoResetEvent.Set();
}
}
}

C#强制要求代码是线程安全的,即不允许跨线程访问Windows窗体的控件

1
2
3
Action<int> setVal = (i) => {this.myTxtBox.Text == i.ToString()};
this.myTxtBox.Invoke(setVal,i);
//Control.Invoke(Delegate method,params object[] args) 在拥有控件的基础窗口句柄的线程上,用指定的自变量列表执行指定委托。

.NET三种异步编程模式

异步编程模型(APM)

概念

  1. .NET 1.0提出了APM(Asynchronous Programming Model)即异步编程模式。
    .NET的类库有以BeginXXX和EndXXX类似的方法,就是使用异步编程模型。
  2. NET Framework很多类也实现了该模式,同时我们也可以自定义类来实现该模式,即在自定义的类中实现返回类型为IAsyncResult接口的BeginXXX方法和EndXXX方法,另外委托类型也定义了BeginInvoke和EndInvoke方法。
    异步编程模型的本质
  3. 利用委托和线程池帮助我们实现异步编程模型模式。
  4. 该模式利用一个线程池线程去执行一个操作,在FileStream类BeginRead方法中就是执行一个读取文件操作,该线程池线程会立即将控制权返回给调用线程,此时线程池线程在后台进行这个异步操作;
  5. 异步操作完成之后,通过回调函数来获取异步操作返回的结果,此时就是利用委托的机制。

借助委托来实现APM

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
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
AsyncCount count = new AsyncCount(Program.count);
IAsyncResult result = count.BeginInvoke(1, 100, out int threadId, null, null);
Thread.Sleep(1000);
Console.WriteLine("回到主线程");
Console.WriteLine("开始等待子线程");
int cou = count.EndInvoke(out threadId, result);
Console.WriteLine($"结果是{cou}");
}
public static int count(int start, int end, out int threadId)
{
Console.WriteLine("进入子线程");
int count = 0;
for (int i = start; i < end; i++)
{
Thread.Sleep(100);
count += i;
}
threadId = Thread.CurrentThread.ManagedThreadId;
return count;
}
public delegate int AsyncCount(int start, int end, out int threadId);
}

基于事件的异步模式(EAP)

Event-based Asynchronous Pattern(EAP),基于事件的异步模式,实际上就是提供了完成任务事件的类,供外部订阅来,实现异步。

还拿数数这个模型来表示一下基于事件的异步模式类。

1
2
3
4
5
6
7
public class CountModel
{
public int Count(int start,int end);
public void CountAsync(int start,int end);
public void CountAsync(int start,int end,object userState);
public event CountCompletedEventHandler CountCompleted;
}

下面为具体的实现

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
public class CountModel
{
public int Count(int start,int end)
{
Console.WriteLine("同步线程开始");
int count = 0;
for (int i = start; i < end; i++)
{
Thread.Sleep(1000);
count += i;
}
Console.WriteLine("同步线程完成");

return count;
}
public void CountAsync(int start,int end,object userState)
{
Thread thread = new Thread(new ThreadStart(() =>
{
Console.WriteLine("异步线程开始");
int count = 0;
for (int i = start; i < end; i++)
{
Thread.Sleep(1000);
count += i;
}
CountCompleted?.Invoke(this, new CountCompletedEventArgs() { Result = count });
Console.WriteLine("异步线程完成");
}));
thread.Start();
}
public event CountCompletedEventHandler CountCompleted;
public delegate void CountCompletedEventHandler(object sender, CountCompletedEventArgs args);
public class CountCompletedEventArgs
{
public int Result;
}
}

基于任务的异步模式(TAP)

Task-based Asynchronous Pattern(TAP)

TAP与EAP的不同在于,EAP中异步方法返回类型为void,而TAP的返回类型为可等待类型如(Task、Task、ValueTask 和 ValueTask),线程结束后不再通过触发完成事件,二是通过立即返回的Task中获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CountModel
{
public Task<int> CountAsync(int start,int end,object userState)
{
return Task.Run(()=>{
int count = 0;
for (int i = start; i < end; i++)
{
Thread.Sleep(1000);
count += i;
}
return count;
})
}
}

Task库的使用

优越性:Task >> ThreadPool >> Thread

async/await

用async来修饰一个方法,表明这个方法是异步的,声明的方法的返回类型必须为:void或Task或Task。方法内部必须含有await修饰的方法,如果方法内部没有await关键字修饰的表达式,哪怕函数被async修饰也只能算作同步方法,执行的时候也是同步执行的。

被await修饰的只能是Task或者Task类型,通常情况下是一个返回类型是Task/Task的方法,当然也可以修饰一个Task/Task变量,await只能出现在已经用async关键字修饰的异步方法中。上面代码中就是修饰了一个变量ResultFromTimeConsumingMethod。

三种async异步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//第一种返回Task,可等待
public async Task MethodAsync(int input){
await Task.Run(()=>{
Console.WriteLine(input);
})
}
//第二种返回Task<TResult>,可等待
public async Task<int> MethodAsync(int input){
return await Task.Run(()=>{
return input;
})
}
//第三种无返回,不可等待
public async void MethodAsync(int input){
await Task.Run(()=>{
Console.WriteLine(input)
})
}
  • 异步方法的返回类型:void Task Task,后两者为可等待方法。
  • 可等待方法如果不进行等待,VS会进行提示。
  • 可等待方法需要使用var task = MethodAsync()来获取结果或者等待,或者使用await标记形成新的异步方法。
  • 必须成对出现
  • await关键字为两线程的分割点,await关键词前的语句在主线程执行,后面另起新线程执行。