多线程和异步编程
进程、应用程序域、上下文及线程之间的关系:
进程:用来描述一组资源和程序运行所必须的内存分配,简单来说就是一个运行程序。
线程:进程(可执行应用程序)中的基本执行单元
应用程序域:承载.net可执行程序的逻辑分区
上下文:应用程序域可以被划分为多个上下文边界
1.一个.net进程可以承载多个应用程序域.每一个应用程序域可以承载多个相关的.net程序集
2.一个给定的应用程序域中包含一个或多个上下文。使用上下文CLR能将‘有特殊需求’的队象
放置到一个逻辑容器中,确保该对象的运行时需求能被满足。
3.一个应用程序域中可能有多个线程,而且一个特定的线程在其生命周期内并不一定被限制在
一个应用程序域中(线程调度使其可跨越应用程序域的边界)
4.在特定的时刻,一个线程也可以移动到一个特定的上下文中
委托的异步性:
定义一个委托:public delegate int DelCalculate(int x, int y);
通过IL反编译工具可以看到委托的方法结果有以下三个:
1.Invoke()方法用来调用被代理的对象以同步方式维护的方法
2.BeginInvoke()用于异步调用方法,返回的对象实现了IAsyncResult接口
3.EndInvoke()用于获取被调用方法的返回值,需要IAsyncResult类型作为参数
在研究委托的异步方法前,现来看一下同步调用方法的实现
同步调用方法:
public delegate int DelCalculate(int x, int y);
static void Main(string[] args)
{
Console.WriteLine("***** Synch Delegate Review *****");
//输出正在执行线程ID
Console.WriteLine("Main() invoked on thread {0}.",Thread.CurrentThread.ManagedThreadId);
//同步模式下调用Add()方法
DelCalculate cal = new DelCalculate(Add);
int result = cal.Invoke(5, 5);
//Add方法执行完才会执行下面的代码
Console.WriteLine("Doing more work in Main()");
Console.WriteLine("result is {0}",result);
Console.ReadLine();
}
private static int Add(int x, int y)
{
//输出正在执行线程ID
Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
//模拟一个耗时操作
Thread.Sleep(5000);
return x + y;
}
输出结果为:
***** Synch Delegate Review *****
Main() invoked on thread 1
Add() invoked on thread 1
Doing more work in Main()
result is 10
可以看到线程ID是一样的(所有的任务都由主线程完成),并且执行Add方法时会有5秒的停顿
异步调用方法:
static void Main(string[] args)
{
Console.WriteLine("***** Synch Delegate Review *****");
//输出正在执行线程ID
Console.WriteLine("Main() invoked on thread {0}.",Thread.CurrentThread.ManagedThreadId);
//在次线程中调用ADD方法
DelCalculate cal = new DelCalculate(Add);
//BeginInvoke有4个参数,前两个要与ADD方法匹配,后两个可为空
IAsyncResult iftAR = cal.BeginInvoke(5, 5,null,null);
//主线程做其他事情
Console.WriteLine("Doing more work in Main()");
//获取ADD方法的结果
int result = cal.EndInvoke(iftAR);
Console.WriteLine("result is {0}",result);
Console.ReadLine();
}
输出结果为:
***** Synch Delegate Review *****
Main() invoked on thread 1
Doing more work in Main()
Add() invoked on thread 3
result is 10
除了ID不同外,只要程序一运行消息 Doing more work in Main()会立刻显示出来。
存在的问题:主线程输出 Doing more work in Main() 消息后还是会处于阻塞状态直到次线程方法全部完成
a.IAsyncResult接口提供了IsCompleted属性,可以通过这个属性来判断异步调用(即次线程)是否完成
DelCalculate cal = new DelCalculate(Add);
IAsyncResult iftAR = cal.BeginInvoke(5, 5,null,null);
//在ADD方法完成之前会一直输出
while (!iftAR.IsCompleted)
{
Console.WriteLine("Doing more work in Main()");
Thread.Sleep(1000);
}int result = cal.EndInvoke(iftAR);
Console.WriteLine("result is {0}",result);
-------------------------------------------
除了IsCompleted属性外,IAsyncResult接口提供了AsyncWaitHandle属性,该属性返回一个WaitHandle类
实例,该实例公开了一个WaitOne()方法(可以指定等待时间),使用该方法可以实现更加灵活的等待逻辑
while (!iftAR.AsyncWaitHandle.WaitOne(1000,true))
{
Console.WriteLine("Doing more work in Main()");
}
-------------------------------------------
b.上面的方式还是不够高效,IsCompleted属性相当于主线程一直在询问次线程是否有执行完。 BeginInvoke
方法提供了一个AsyncCallback委托的实例作为参数(就是第三个参数),当提供了这个对象时,异步调用
完成时,委托会自动调用AsyncCallback对象所指定的方法
private static bool isDone = false;
static void Main(string[] args){
Console.WriteLine("***** Synch Delegate Review *****");
Console.WriteLine("Main() invoked on thread {0}.",Thread.CurrentThread.ManagedThreadId);
DelCalculate cal = new DelCalculate(Add);
//传入了第三个参数一个AsyncCallback委托的实例
IAsyncResult iftAR = cal.BeginInvoke(5, 5,new AsyncCallback(AddComplete),null);
//这里主线程做其他事情
while (!isDone)
{
Console.WriteLine("Doing more work in Main()");
Thread.Sleep(1000);
}Console.ReadLine();
}private static int Add(int x, int y)
{
Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
return x + y;
}static void AddComplete(IAsyncResult itfAR)
{
Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Your addition is complete");
//AsyncCallback委托的目标无法访问Main()中创建的委托,故此处需要强转
//AsyncResult类继承了IAsyncResult接口,并且AsyncResult类的只读属性AsyncDelegate返回了别处创建的原始异步委托的调用
AsyncResult ar = (AsyncResult)itfAR;
//(DelCalculate)ar.AsyncDelegate返回的就是Main()中创建的委托
DelCalculate cal = (DelCalculate)ar.AsyncDelegate;
Console.WriteLine("result is {0}.",cal.EndInvoke(itfAR));isDone = true;
}
c.BeginInvoke()方法的最后一个参数(object类型)允许主线程传递额外的状态信息给回调方法
IAsyncResult iftAR = cal.BeginInvoke(5, 5,new AsyncCallback(AddComplete),"calculate is over");
static void AddComplete(IAsyncResult itfAR)
{
//通过AsyncResult类的只读属性AsyncState获取传入的数据
string message = (string)itfAR.AsyncState;
Console.WriteLine(message);
isDone = true;
}
多线程:
手动创建次线程的步骤:
1.创建一个方法作为线程的入口点
2.创建一个ThreadStart/ParameterizedThreadStart委托,并把上一步所定义方法的地址传递给委托的构造函数
3.创建一个Thread对象,并把ThreadStart/ParameterizedThreadStart委托作为构造函数的参数
4.建立任意初始化线程的特性(名称、优先级等)
5.调用Thread.Start()方法
使用ThreadStart委托:该委托仅仅指向无返回值、无参数的方法
static void Main(string[] args)
{
Thread primaryThread = Thread.CurrentThread;
primaryThread.Name = "Primary";
Console.WriteLine("-->{0} is executing Main()", Thread.CurrentThread.Name);
Printer p = new Printer();
//第二步创建一个ThreadStart委托
ThreadStart start = new ThreadStart(p.PrintNumbers);
//第三步创建一个Thread对象
Thread newThread = new Thread(start);
//第四步
newThread.Name = "Secondary";
//第五步
newThread.Start();
MessageBox.Show("I am busy!", "Work on main thread..");
}
public class Printer
{
public void PrintNumbers() //第一步创建一个方法作为线程的入口点
{
Console.WriteLine("-->{0} is executing PrintNumbers()",Thread.CurrentThread.Name);
Console.WriteLine("Your Numbers: ");
for (int i = 0; i < 10; i++)
{
Console.WriteLine("{0}, ",i);
Thread.Sleep(2000);
}
Console.WriteLine();
}
}
使用ParameterizedThreadStart委托:接受一个Object类型参数,无返回值
Printer p = new Printer();
ParameterizedThreadStart start = new ParameterizedThreadStart(p.PrintNumbers);
Thread newThread = new Thread(start);
newThread.Start(10);
newThread.Name = "Secondary";
MessageBox.Show("I am ParameterizedThreadStart!");
public void PrintNumbers(object num)
{
Console.WriteLine("-->{0} is executing PrintNumbers()", Thread.CurrentThread.Name);
Console.WriteLine("Your Numbers: ");
//出于测试目的此处强转不严谨
for (int i = 0; i <(int)num; i++)
{
Console.WriteLine("{0}, ", i);
Thread.Sleep(2000);
}
Console.WriteLine();
}
前台线程:能阻止应用程序的终结,直到所有的前台线程都终止后,CLR才能关闭应用程序
后台线程:被CLR认为是程序执行中可作出牺牲的部分,即在任何时候都可以被忽略
并发问题:在构建多线程应用程序时,需要确保任何共享数据都处于被保护状态,以防止多个线程修改他的值
(当有两个线程访问同一共享数据时,由于线程调度器会随时挂起线程,所以如果A正在修改共享
数据但还没有完成,这时B又来访问此时A被挂起,由于A还没有完成所以此时B访问的就是错误的数据)
1.使用C#的Lock关键字进行同步(首先方式)
public class Printer
{
private object threadLock = new object();
public void PrintNumbers()
{
//如果没有Lock那么当有多个线程同时访问此方法时输出的可能就是不规则的数
lock(threadLock)
{
Console.WriteLine("-->{0} is executing PrintNumbers()", Thread.CurrentThread.Name);
Console.WriteLine("Your Numbers: ");
for (int i = 0; i < 10; i++)
{
Console.WriteLine("{0}, ", i);
Thread.Sleep(1000);
}
Console.WriteLine();
}
}
}
Main函数中如下:
Printer p = new Printer();
//创建10个线程使其都访问p.PrintNumbers()方法
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++)
{
threads[i] = new Thread(new ThreadStart(p.PrintNumbers));
threads[i].Name = string.Format("Worker thread #{0}", i);
}foreach (Thread item in threads) { item.Start(); }
2.使用System.Threading.Monitor类型进行同步
public class Printer
{
private object threadLock = new object();
public void PrintNumbers()
{
Monitor.Enter(threadLock);
try
{
Console.WriteLine("-->{0} is executing PrintNumbers()", Thread.CurrentThread.Name);
Console.WriteLine("Your Numbers: ");
for (int i = 0; i < 10; i++)
{
Console.WriteLine("{0}, ", i);
Thread.Sleep(1000);
}
Console.WriteLine();
}
finally
{
Monitor.Exit(threadLock);
}
}
}
3.使用[Synchronization]特性进行同步(类级别特性)
//此时Printer类中所有的方法都是线程安全的
[Synchronization]
public class Printer:ContextBoundObject //必须继承ContextBoundObject类
{
private object threadLock = new object();
public void PrintNumbers()
{
Console.WriteLine("-->{0} is executing PrintNumbers()", Thread.CurrentThread.Name);
Console.WriteLine("Your Numbers: ");
for (int i = 0; i < 10; i++)
{
Console.WriteLine("{0}, ", i);
Thread.Sleep(1000);
}
Console.WriteLine();
}
}
C#中的async和await关键字
先创建一个Windows Form应用程序,在窗体上添加一个Button和TextBox控件,代码如下
private void btnCallMethod_Click(object sender, EventArgs e)
{
this.Text = DoWork();
}private string DoWork()
{
Thread.Sleep(5000);
return "Done with work!";
}
当单击了BTN按钮后,需要等待5秒钟才能在TextBox控件中输入文字,主窗体才能够被拖动
使用async和await关键字对上述代码进行修改
private async void btnCallMethod_Click(object sender, EventArgs e)
{
//使用async标记过得方法,如果内部没有await方法的调用,则实际上还是阻塞的同步的
this.Text = await DoWork();
}
//DoWork方法直接返回Task<string>对象(Task.Run()的返回值,该方法接受Func或Action委托)
private Task<string> DoWork()
{
return Task.Run(() =>
{
Thread.Sleep(5000);
return "Done with work!";
});
}
上述异步实现也可以通过手动创建多线程来实现,但是用async和await关键字会更简洁
异步方法里可以有多个await方法
private async void btnCallMethod_Click(object sender, EventArgs e)
{
this.Text = await DoWork();
//第二个await方法,还可以有更多个
await Task.Run(() =>
{
Thread.Sleep(5000);
MessageBox.Show("Hello World1");
});
}private Task<string> DoWork()
{
return Task.Run(() =>
{
Thread.Sleep(5000);
return "Done with work!";
});
----------------------参考学习书籍《精通C#》-------------------