五、多线程编程
当今世界,大家对多任务都很熟悉,简单的说就是你可以并行做多件事。考虑一个常见的场景。例如,当我在笔记本电脑上用 Microsoft Word 写这一章时,我正在 Windows Media Player 上听一段非常平静的音乐。同样,您可以在 C# 应用中同时执行不同的方法。要实现这个概念,您需要熟悉多线程。
在早期,计算机只有一个处理器,但是现在,情况已经发生了很大的变化。当今世界的大多数计算机都有多个处理器。例如,在撰写本文时,我正在使用一个带有四个逻辑处理器的双核系统;然而,在当今世界,这并不被认为是一台超高速计算机,因为有大量处理器(显然是昂贵的)和更强大的计算能力的机器。不过,如果另一台超高速电脑通过网络连接到我的电脑,我可以在上面执行一些工作。所以,使用其他机器的计算能力是可能的。但事实是,除非您将代码构建为在多个处理器上运行,否则您没有充分利用机器的计算潜力。在本章中,你将熟悉多线程,并学习如何有效地使用它。我们开始吧。
线程编程基础
到目前为止,您所看到的大多数程序都有一个单一的顺序控制流(即,一旦程序开始执行,它就按顺序遍历所有语句,直到结束)。因此,在任何特定时刻,只有一条语句正在执行。线程类似于程序。它只有一个控制流。它在起点和终点之间也有一个主体,它按顺序执行命令。每个程序至少有一个线程。
在 C# 中,一个程序中可以有多个控制流。在这些情况下,每个控制流被称为一个线程,这些线程可以并行运行。在多线程环境中,每个线程都有一个独特的执行流。这是一种编程范式,其中一个程序被分成多个可以并行实现的子程序(或部分)。但是,如果计算机只有一个处理器,它如何并行执行多项任务呢?处理器在这些子程序(或代码段)之间切换得非常快,因此在人眼看来,它们似乎都在同时执行。
简单来说,当操作系统在不同的应用之间划分处理器执行时间时,该场景是多任务处理,当操作系统在单个应用内的不同线程之间划分执行时间时,该场景被称为多线程。这就是为什么多线程被认为是一种特殊的多任务处理。
在这种情况下,在任何操作系统理论书籍中回顾进程和线程之间的区别是很重要的。供您参考,表 5-1 强调了一些关键区别。
表 5-1
进程和线程之间的比较
|过程
|
线
|
| — | — |
| 分配单位。 | 执行单位。 |
| 建筑构造。 | 编码结构不影响架构。 |
| 每个进程都有一个或多个线程。 | 每个线程属于一个进程。 |
| 由于上下文切换,进程间通信(通常称为 IPC)的开销很大。 | 线程间通信成本较低,可以使用进程内存,并且可能不需要上下文切换。 |
| 安全:一个进程不能破坏另一个进程。 | 不安全:一个线程可以写入另一个线程使用的内存。 |
管理多线程环境可能很有挑战性,但您可以更快地完成任务,并显著减少总体空闲时间。通常,在自动化环境中,计算机的输入比用户的键盘输入快得多。或者,当您通过网络传输数据时,网络传输速率可能比接收计算机的消耗速率慢。如果您需要等待每个任务完成后才能开始下一个任务,则总的空闲时间会更长。在这种情况下,多线程环境总是更好的选择。C# 可以帮助你有效地建模多线程环境。
图 5-1 展示了多线程程序中的一个常见场景,其中main thread
创建了另外两个线程——threadOne
和threadTwo
——并且所有线程都在并发运行。
图 5-1
在多线程程序中,主线程创建两个以上的线程,并且它们都是并发运行的
POINTS TO REMEMBER
多线程的核心目标是你可以在单独的线程中执行独立的代码段,这样你就可以更快地完成任务。
在。NET Framework 中,您可以同时拥有前台和后台线程。创建线程时,默认情况下它是前台线程。但是您可以将前台线程转换为后台线程。关键区别在于,当前台线程终止时,关联的后台线程也会停止。
问答环节
5.1 在图 5-1 中,我看到了术语“上下文切换”在这个上下文中是什么意思?
通常,许多线程可以在您的计算机上并行运行。计算机允许一个线程在一个处理器中运行一段时间,然后它可以突然切换到另一个处理器。这个决定是由不同的因素做出的。正常情况下,所有线程都有相同的优先级,它们之间的切换执行得很好。线程之间的切换称为上下文切换。它还使您能够存储当前线程(或进程)的状态,以便以后可以从这一点继续执行。
5.2 与单线程环境相比,多线程环境的主要优势是什么?
在单线程环境中,如果线程被阻塞,整个程序就会暂停,而在多线程环境中则不是这样。此外,您可以通过有效利用 CPU 来减少总的空闲时间。例如,当程序的一部分通过网络发送大量数据时,程序的另一部分可以接受用户输入,而程序的另一部分可以验证该输入并准备发送下一个数据块。
5.3 我有一个多核系统,但是多线程还能帮我吗?
曾几何时,大多数计算机只有一个内核;并发线程共享 CPU 周期,但是它们不能并行运行。使用多线程的概念,您可以通过有效使用 CPU 来减少总的空闲时间。但是如果你有多个处理器,你可以同时运行多个线程。因此,您可以进一步提高程序的速度。
5.4 多线程程序可以有多个并发运行的部分。这些部分中的每一个都是线程,每个线程可以有一个单独的执行流。这是正确的吗?
是的。
用 C# 编写多线程程序
在用 C# 编写多线程程序之前,首先要记住的是从
using System.Threading;
这个名称空间包含有不同方法的Thread
类。您将在接下来的演示中看到其中的一些方法。现在到了下一步。要运行一个方法,比如说Method1()
,在一个单独的线程中,您需要编写如下代码。
Thread threadOne = new Thread(Method1);
threadOne.Start();
注意前面的两行。如果将鼠标悬停在 Visual Studio 中的 Thread 类型上,您会看到 Thread 类有四个不同的构造函数,如下所示。
public Thread(ThreadStart start)
public Thread(ParameterizedThreadStart start)
public Thread(ThreadStart start, int maxStackSize)
public Thread(ParameterizedThreadStart start, int maxStackSize)
ThreadStart
和ParameterizedThreadStart
是代表。现在让我们详细研究一下这些代表。从 Visual Studio IDE 中,您可以获得对ThreadStart
委托的以下描述。
//
// Summary:
// Represents the method that executes on a //System.Threading.Thread.
[ComVisible(true)]
public delegate void ThreadStart();
类似地,Visual Studio IDE 显示了对ParameterizedThreadStart
委托的以下描述。
//
// Summary:
// Represents the method that executes on a //System.Threading.Thread.
//
// Parameters:
// obj:
// An object that contains data for the thread procedure.
[ComVisible(false)]
public delegate void ParameterizedThreadStart(object obj);
这些描述显示了以下几点。
-
两个委托都有
void
返回类型。 -
The ThreadStart
delegate 没有参数,而ParameterizedThreadStart
可以接受对象参数。
您将很快对这两个代理进行实验。但到目前为止,您已经学会了在不同的线程中运行一个方法;这些方法应该匹配任一委托签名。
最后一点:在演示 1 和演示 2 中,我使用了最简单的Start()
方法,它没有任何参数。稍后,您还会注意到该方法的另一个重载版本的使用,它可以接受一个对象参数。因此,根据您的需要,您可以使用以下任何一种方法:
public void Start();
public void Start(object? parameter);
使用ThreadStart
委托
让我们从ThreadStart
代表开始。假设你有一个叫做Method1
的方法,如下。
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}}
因为Method1
不接受任何参数,并且它有一个void
返回类型,它与ThreadStart
委托签名匹配。在第一章中,你学到了如果你写下以下内容,
ThreadStart delegateObject = new ThreadStart(Method1);
它相当于写作
ThreadStart delegateObject = Method1;
因此,当您在线程构造函数中传递一个ThreadStart
委托对象时,您可以编写如下代码。
Thread threadOne = new Thread(new ThreadStart(Method1));
这相当于写作
Thread threadOne = new Thread(Method1);
最后,值得注意以下几点。
-
在接下来的例子中,
Method1()
是一个静态方法。在这种情况下,您可以引用该方法,而无需实例化任何对象。 -
一旦调用了
Start()
方法,线程就被创建并开始执行。 -
如果你在一个已经运行的线程上调用
Start()
方法,你会遇到一个运行时错误,说,System.Threading.ThreadStateException
: 线程正在运行或者终止;它无法重启。
我们再来看最后一点。通过编程,一个线程可以有几种状态。Start
方法可以将当前实例的状态更改为ThreadState.Running
。在 Visual Studio2019 IDE 中,如果将鼠标悬停在ThreadState
定义处,会看到图 5-2 所示的枚举,描述了不同的线程状态。
图 5-2
C# 中线程的不同状态
这些都是不言自明的,但是你可能会对一个叫做WaitSleepJoin
的感兴趣。由于调用了Sleep()
或Join()
,或者请求了一个锁,线程可以进入这种阻塞状态;例如,当你调用Wait()
、Monitor.Enter()
等带有适当参数的时候。您很快就会了解到这一点。
演示 1
在下面的演示中,有两个静态方法:Method1
和Method2
。这些方法与ThreadStart
代表的签名相匹配。正如在“线程编程的基础”一节中所讨论的,我在单独的线程中运行它们。
POINTS TO REMEMBER
在本章中,对于一些初始演示,您会看到方法体中的硬编码行,例如
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
或者,
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
理想情况下,您不应该像这样硬编码线程细节,因为在多线程环境中,Method1()可以在不同的线程中执行。但是如果你设置了一个线程名,那么你可以写类似下面的代码。
Console.WriteLine("-{0} from Method1() prints {1}", Thread.CurrentThread.Name, i);
或者,如果您喜欢使用字符串插值,您可以编写如下代码。
Console.WriteLine($"{Thread.CurrentThread.Name} from MyMethod() prints {i}");
在这里,我一步一步来。我还没有从 Thread 类中引入 Name 属性。为了简单起见,我使用 threadOne 对象执行 Method1(),使用 threadTwo 对象执行 Method2(),以此类推。
这是完整的演示。
using System;
using System.Threading;
namespace ThreadProgrammingEx1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Thread Demonstration-1****");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
/* Thread threadOne = new Thread(new ThreadStart(Method1));*/
Thread threadTwo = new Thread(Method2);
// Same as
/* Thread threadTwo = new Thread(new ThreadStart(Method2));*/
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***Thread Demonstration-1****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
Control comes at the end of Main() method.
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
-ThreadOne from Method1() prints 9
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
这是另一个可能的输出。
***Thread Demonstration-1****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
Control comes at the end of Main() method.
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
-ThreadOne from Method1() prints 9
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
分析
我提出了两种可能的输出结果:它可能因你的情况而异。这在线程编程中很常见,因为您的操作系统根据设计采用了上下文切换。稍后,您将看到可以使用一种特殊的机制来控制执行顺序。
演示 2
在演示 1 中,原始线程(对于Main()
方法)在衍生线程(对于Method1
和Method2
)之前结束。但是在真实的应用中,您可能不希望父线程在子线程之前完成(尽管程序会继续运行,直到其前台线程处于活动状态)。
在简单的场景中,您可以使用Sleep(int millisecondsTimeout)
方法。这是一种常用的static
方法。它会导致当前执行的线程暂停一段指定的时间。int
参数提示您需要将毫秒作为参数传递。如果您希望当前线程暂停 1 秒钟,您可以将 1000 作为参数传递给Sleep
方法。但是Sleep
方法不如Join()
有效,?? 也在Thread
类中定义。这是因为Join()
方法可以帮助你阻塞一个线程,直到另一个线程完成它的执行。在下面的演示中,我使用了这种方法,您会看到下面几行带有支持注释的代码。
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
这些语句是在Main()
方法内部编写的。一旦原始线程通过这些语句,它就等待threadOne
和threadTwo
完成它们的任务,并有效地加入子线程的执行。
现在浏览完整的演示并查看输出,然后是一个简短的分析。
using System;
using System.Threading;
namespace ThreadProgrammingEx2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Thread Demonstration-2****");
Console.WriteLine("***Exploring Join() method.It helps to make a thread wait for another running thread to finish it's job.***");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
//Thread threadOne = new Thread(new ThreadStart(Method1));
Thread threadTwo = new Thread(Method2);
// Same as
//Thread threadTwo = new Thread(new ThreadStart(Method2));
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***Thread Demonstration-2****
***Exploring Join() method.It helps to make a thread wait for another running thread to finish it's job.***
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
-ThreadOne from Method1() prints 9
Control comes at the end of Main() method.
分析
在这个演示中,您看到了在Main()
方法中使用了Join()
方法。原始线程保持活动状态,直到其他线程完成执行。所以,"Control comes at the end of Main() method."
语句总是出现在输出的末尾。
值得注意的是
-
Start
和Join
方法都有不同的重载版本。 -
你遇到一个运行时错误,说,
System.Threading.ThreadStateException:
*‘线程没有启动。’*如果你在一个没有启动的线程上调用Join()
。
问答环节
5.5 如何穿线。Sleep()不同于 Thread。Join()?
Sleep()
方法有两种变体。
public static void Sleep(int millisecondsTimeout)
and
public static void Sleep(TimeSpan timeout)
使用 Sleep()方法,可以将当前线程挂起一段特定的时间。
Join() has three variations.
public void Join();
public bool Join(int millisecondsTimeout);
public bool Join(TimeSpan timeout);
基本思想是,通过使用 Join(),可以阻塞调用线程,直到该实例所代表的线程终止。(虽然可以在重载版本的 Join()内部指定超时限制。)
使用sleep()
,如果你指定的时间不必要的长,线程将处于暂停状态,即使其他线程已经完成了它们的执行。但是通过使用 Join(),您可以等待其他线程完成,然后立即继续。
另一个有趣的区别是Sleep()
是一个静态方法,你在当前线程上调用这个方法。但是Join()
是一个实例方法,当你写类似下面这样的东西时,从调用者的角度来看,你传递了某个其他线程(而不是调用线程)的实例,并等待那个线程先完成。
// Waiting for threadOne to finish
threadOne.Join();
使用ParameterizedThreadStart
委托
您已经看到了ThreadStart
委托的用法。您无法处理可以接受参数的方法,但是带参数的方法在编程中非常常见。接下来,您将看到ParameterizedThreadStart
委托的使用。您已经知道它可以接受一个object
参数,并且它的返回类型是 void。因为参数是一个对象,所以您可以将它用于任何类型,只要您可以将强制转换正确地应用于正确的类型。
演示 3
在本演示中,您有以下方法。
static void Method3(Object number)
{
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---ThreadThree from Method3() prints 3.0{0}", i);
}
}
您可以看到,尽管该方法有一个object
参数,但我将它转换为一个int
,然后使用它将所需的数据打印到控制台窗口。在这个演示中,有三种方法:Method1
、Method2
和Method3
。Method1
和Method2
在之前的演示中。新增Method3
是为了在下面的例子中演示ThreadStart
委托和ParameterizedThreadStart
委托的用法。
using System;
using System.Threading;
namespace UsingParameterizedThreadStart_delegate
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***ParameterizedThreadStart delegate is used in this demonstration****");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
//Thread threadOne = new Thread(new ThreadStart(Method1));
Thread threadTwo = new Thread(Method2);
// Same as
//Thread threadTwo = new Thread(new ThreadStart(Method2));
Thread threadThree = new Thread(Method3);
// Same as
//Thread threadThree = new Thread(new ParameterizedThreadStart(Method3));
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
Console.WriteLine("Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.");
// threadThree starts
threadThree.Start(15);
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
// Waiting for threadthree to finish
threadThree.Join();
Console.WriteLine("Main() method ends now.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
/*
The following method has an object parameter
This method matches the ParameterizedThreadStart delegate signature;because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method3(Object number)
{
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---ThreadThree from Method3() prints 3.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***ParameterizedThreadStart delegate is used in this demonstration****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.
--ThreadTwo from Method2() prints 2.00
-ThreadOne from Method1() prints 5
--ThreadTwo from Method2() prints 2.01
-ThreadOne from Method1() prints 6
--ThreadTwo from Method2() prints 2.02
-ThreadOne from Method1() prints 7
--ThreadTwo from Method2() prints 2.03
-ThreadOne from Method1() prints 8
---ThreadThree from Method3() prints 3.00
--ThreadTwo from Method2() prints 2.04
-ThreadOne from Method1() prints 9
---ThreadThree from Method3() prints 3.01
--ThreadTwo from Method2() prints 2.05
---ThreadThree from Method3() prints 3.02
--ThreadTwo from Method2() prints 2.06
---ThreadThree from Method3() prints 3.03
---ThreadThree from Method3() prints 3.04
---ThreadThree from Method3() prints 3.05
---ThreadThree from Method3() prints 3.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
---ThreadThree from Method3() prints 3.07
--ThreadTwo from Method2() prints 2.09
---ThreadThree from Method3() prints 3.08
---ThreadThree from Method3() prints 3.09
---ThreadThree from Method3() prints 3.010
---ThreadThree from Method3() prints 3.011
---ThreadThree from Method3() prints 3.012
---ThreadThree from Method3() prints 3.013
---ThreadThree from Method3() prints 3.014
Main() method ends now.
分析
与演示 2 一样,本例中使用了Join()
方法。因此,行"Main() method ends now."
位于输出的末尾。还要注意,这次我使用了下面一行:threadThree.Start(15);
这里我使用了重载版本的Start()
方法,它可以接受一个对象参数。
问答环节
5.6 我知道通过使用 ParameterizedThreadStart
委托,我可以使用可以接受 object
参数的方法。但是如何使用除 object 之外接受参数的其他方法呢?
因为参数是一个对象,所以几乎可以在任何情况下使用它,并且可能需要正确应用强制转换。例如,在演示 3 中,我将一个int
传递给了Method3
的参数,该参数被隐式地转换为一个object
,后来我对对象参数应用了强制转换,以取回所需的int
。
5.7 使用 ParameterizedThreadStart
委托,我可以处理接受多个参数的方法吗?
是的,你可以。演示 4 向您展示了这样一种用法。
演示 4
在这个例子中,您会看到下面这个名为Boundaries
的类,它有一个带有两个int
参数的公共构造函数。
class Boundaries
{
public int lowerLimit;
public int upperLimit;
public Boundaries( int lower, int upper)
{
lowerLimit = lower;
upperLimit = upper;
}
}
并且有一个名为Method4
的静态方法匹配ParameterizedThreadStart
委托的签名。该方法定义如下。
static void Method4(Object limits)
{
Boundaries boundaries = (Boundaries)limits;
int lowerLimit = boundaries.lowerLimit;
int upperLimit = boundaries.upperLimit;
for (int i = lowerLimit; i < upperLimit; i++)
{
Console.WriteLine("---ThreadFour from Method4() prints 4.0{0}", i);
}
}
在Main
里面是下面几行代码。
Thread threadFour = new Thread(Method4);
threadFour.Start(new Boundaries(0, 10));
您可以看到我正在创建一个 Boundaries 类对象,并将 0 和 10 作为参数传递。以类似的方式,您可以传递任意多的参数来构造一个对象,然后将它传递给与ParameterizedThreadStart
委托匹配的方法。
using System;
using System.Threading;
namespace ThreadProgrammingEx4
{
class Boundaries
{
public int lowerLimit;
public int upperLimit;
public Boundaries( int lower, int upper)
{
lowerLimit = lower;
upperLimit = upper;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Thread Demonstration-4****");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
//Thread threadOne = new Thread(new ThreadStart(Method1));
Thread threadTwo = new Thread(Method2);
// Same as
//Thread threadTwo = new Thread(new ThreadStart(Method2));
Thread threadThree = new Thread(Method3);
// Same as
//Thread threadThree = new Thread(new ParameterizedThreadStart(Method3));
Thread threadFour = new Thread(Method4);
// Same as
//Thread threadThree = new Thread(new ParameterizedThreadStart(Method4));
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
Console.WriteLine("Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.");
// threadThree starts
threadThree.Start(15);
Console.WriteLine("Starting threadFour shortly.Here we use ParameterizedThreadStart delegate.");
// threadFour starts
threadFour.Start(new Boundaries(0,10));
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
// Waiting for threadthree to finish
threadThree.Join();
Console.WriteLine("Main() method ends now.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
/*
The following method has an object parameter
This method matches the ParameterizedThreadStart delegate signature;because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method3(Object number)
{
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---ThreadThree from Method3() prints 3.0{0}", i);
}
}
/*
The following method also has one parameter.This method matches the ParameterizedThreadStart delegate signature; because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method4(Object limits)
{
Boundaries boundaries = (Boundaries)limits;
int lowerLimit = boundaries.lowerLimit;
int upperLimit = boundaries.upperLimit;
for (int i = lowerLimit; i < upperLimit; i++)
{
Console.WriteLine("---ThreadFour from Method4() prints 4.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***Thread Demonstration-4****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
-ThreadOne from Method1() prints 9
---ThreadThree from Method3() prints 3.00
---ThreadThree from Method3() prints 3.01
---ThreadThree from Method3() prints 3.02
---ThreadThree from Method3() prints 3.03
---ThreadThree from Method3() prints 3.04
---ThreadThree from Method3() prints 3.05
---ThreadThree from Method3() prints 3.06
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
---ThreadThree from Method3() prints 3.07
Starting threadFour shortly.Here we use ParameterizedThreadStart delegate.
---ThreadThree from Method3() prints 3.08
---ThreadThree from Method3() prints 3.09
---ThreadThree from Method3() prints 3.010
---ThreadThree from Method3() prints 3.011
---ThreadThree from Method3() prints 3.012
---ThreadThree from Method3() prints 3.013
---ThreadThree from Method3() prints 3.014
---ThreadFour from Method4() prints 4.00
---ThreadFour from Method4() prints 4.01
---ThreadFour from Method4() prints 4.02
---ThreadFour from Method4() prints 4.03
---ThreadFour from Method4() prints 4.04
---ThreadFour from Method4() prints 4.05
---ThreadFour from Method4() prints 4.06
---ThreadFour from Method4() prints 4.07
---ThreadFour from Method4() prints 4.08
Main() method ends now.
---ThreadFour from Method4() prints 4.09
分析
我没有用Join()
代替threadFour
,所以主线程有可能在threadFour
完成它的任务之前就结束了。
问答环节
5.8 parameterized threadstart 委托不处理具有非 void 返回类型的方法。但是,如果我需要获得退货信息,我应该如何进行?
你可以用不同的方式处理它。例如,在第六章中,你将学习不同的技术来实现异步编程。在那一章中,您将看到一个基于任务的异步模式演示,其中有一个返回string
结果的方法。如果您想处理一个返回不同数据类型的方法,比如说一个int
,您可以使用类似的方法。
现在,您可以使用 lambda 表达式来获得您想要的结果。演示 5 展示了这样一个例子。(作为变体,我在这个例子中使用了字符串插值来打印控制台消息。)
演示 5
这个演示是一个例子,在这个例子中,您可以使用 lambda 表达式来执行在不同线程中运行的两个不同的方法(使用返回类型)。
using System;
using System.Threading;
namespace ThreadProgrammingEx5
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Dealing methods with return types.These methods run in different threads.***");
int myInt = 0;//Initial value
Console.WriteLine($"Inside Main(),ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}");
Thread threadOne = new Thread(
() => {
Console.WriteLine($"Method1() is executing in ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}");
// Do some activity/task
myInt = 5;//An arbitrary value
});
string myStr = "Failure"; // Initial value
Thread threadTwo = new Thread(
() => {
Console.WriteLine($"Method2() is executing in ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}");
// Do some activity/task
myStr = "Success.";
});
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
Console.WriteLine($"Method1() returns {myInt}");
Console.WriteLine($"Method2() returns {myStr} ");
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
}
}
输出
这是一种可能的输出。
***Dealing methods with return types.These methods run in different threads.***
Inside Main(),ManagedThreadId:1
Starting threadOne shortly.
Starting threadTwo shortly.
Method1() is executing in ManagedThreadId:3
Method2() is executing in ManagedThreadId:4
Method1() returns 5
Method2() returns Success.
Control comes at the end of Main() method.
Note
ManagedThreadId 只为特定的托管线程获取唯一标识符*。当您在机器上运行应用时,您可能会注意到一个不同的值。不要觉得既然你已经创建了 n 个线程,你应该只看到 1 和 n 之间的线程 id。可能还有其他线程也在后台运行。*
*### 问答环节
5.9 在本章中,你使用的术语是****。这是什么意思?****
**当您执行程序时,一个线程会自动启动。这是主线。这些演示中的Main()
方法正在创建主线程,它在Main()
方法结束时终止。当我使用Thread
类创建其他线程时,我将它们称为子线程。在这种情况下,需要注意的是Thread.CurrentThread
属性可以帮助您获得关于线程的信息;例如,您可以使用下面几行代码来获取线程的名称(可以在前面设置)、ID 和优先级。
Console.WriteLine("Inside Main,Thread Name is:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Inside Main,ManagedThreadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Inside Main,Thread Priority is: {0}", Thread.CurrentThread.Priority);
在编写时,C# 中的一个线程可以有以下优先级: Lowest
、BelowNormal
、Normal
、AboveNormal
、and Highest
。图 5-3 显示了 Visual Studio 的部分屏幕截图,其中显示了关于ThreadPriority
枚举的信息。
图 5-3
C# 中不同的线程优先级
演示 6
这个演示展示了我们刚刚讨论过的Thread
类中的Name
、Priority
和ManagedThreadId
属性的用法。
using System;
using System.Threading;
namespace UsingMainThread
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Working on the main thread and a child Thread only.****");
Thread.CurrentThread.Name = "Main Thread";
Thread threadOne = new Thread(Method1);
threadOne.Name = "Child Thread-1";
threadOne.Priority = ThreadPriority.AboveNormal;
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Inside Main,Thread Name is:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Inside Main,ManagedThreadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Inside Main,Thread Priority is: {0}", Thread.CurrentThread.Priority);
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
static void Method1()
{
Console.WriteLine("Inside Method1(),Thread Name is:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Inside Method1(),ManagedThreadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Inside Method1(),Thread Priority is:{0}", Thread.CurrentThread.Priority);
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Using Method1(), printing the value {0}", i);
}
}
}
}
输出
这是一个可能的输出。
***Working on the main thread and a child Thread only.****
Starting threadOne shortly.
Inside Main,Thread Name is:Main Thread
Inside Main,ManagedThreadId is:1
Inside Method1(),Thread Name is:Child Thread-1
Inside Method1(),ManagedThreadId is:5
Inside Method1(),Thread Priority is:AboveNormal
Using Method1(), printing the value 0
Using Method1(), printing the value 1
Using Method1(), printing the value 2
Using Method1(), printing the value 3
Using Method1(), printing the value 4
Inside Main,Thread Priority is: Normal
Control comes at the end of Main() method.
分析
尽管子线程的优先级高于主线程,但这并不保证子线程会在主线程之前完成。有几个其他因素可能会决定这个输出。
问答环节
5.10“它不能保证子线程会在主线程之前完成。还有其他几个因素可能决定这一产出”。你能详细说明一下吗?
从概念上讲,优先级决定了一个线程获得 CPU 时间的频率。理论上,高优先级线程比低优先级线程获得更多的 CPU 时间,在抢占式调度中,它们可以抢占低优先级线程。但是,你需要考虑许多其他因素。例如,可能会发生高优先级线程正在等待获取共享资源,因此被阻塞的情况;在这种情况下,低优先级线程可以有机会完成它的任务。
考虑另一种情况,低优先级线程正在执行一个非常短的任务,而高优先级线程正在执行一个非常长的任务。如果低优先级线程有机会执行,它会在高优先级线程之前完成。
最后,任务调度在操作系统中的实现方式也很重要,因为 CPU 分配也取决于此。这就是为什么你不应该完全依赖优先级来预测产出。
如何终止一个线程?
通过使用在Thread
类中定义的Abort()
方法,您可以终止一个线程。
下面是一些示例代码。
threadOne.Abort();
Abort()
方法有两个不同的重载版本,如下。
public void Abort();
public void Abort(object stateInfo);
前台线程与后台线程
Thread 类有一个名为IsBackground
的属性,描述如下。
//
// Summary:
// Gets or sets a value indicating whether or not a thread is a // background thread.
//
// Returns:
// true if this thread is or is to become a background thread; // otherwise, false.
//
// Exceptions:
// T:System.Threading.ThreadStateException:
// The thread is dead.
public bool IsBackground { get; set; }
默认情况下,线程是前台线程。当您将IsBackground
属性设置为true
时,您可以将前台线程转换为后台线程。以下代码片段可以帮助您更好地理解这一点。(我做了两个Thread
类对象:threadFour
和threadFive
。稍后我会做一个threadFive
后台线程。我用注释标记了这个部分的预期输出)。
Thread threadFour = new Thread(Method1);
Console.WriteLine("Is threadFour is a background thread?:{0} ", threadFour.IsBackground); // False
Thread threadFive = new Thread(Method1);
threadFive.IsBackground = true;
Console.WriteLine("Is threadFive is a background thread?:{0} ", threadFive.IsBackground); // True
如果您想要一个完整的演示,请考虑下面的例子。
演示 7
在Main()
中,我只创建了一个线程。我将其命名为Child Thread-1
,并将IsBackground
属性设置为 true。现在运行这个程序,并遵循输出和相应的讨论。
using System;
using System.Threading;
namespace TestingBackgroundThreads
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Comparing a foreground threads with a background thread****");
Thread.CurrentThread.Name = "Main Thread";
Console.WriteLine($"{Thread.CurrentThread.Name} has started.");
Thread childThread = new Thread(MyMethod);
childThread.Name = "Child Thread-1";
Console.WriteLine("Starting Child Thread-1 shortly.");
// threadOne starts
childThread.Start();
childThread.IsBackground = true;
Console.WriteLine("Control comes at the end of Main() method.");
//Console.ReadKey();
}
static void MyMethod()
{
Console.WriteLine($"{Thread.CurrentThread.Name} enters into MyMethod()");
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name} from MyMethod() prints {i}");
//Taking a small sleep
Thread.Sleep(100);
}
Console.WriteLine($"{Thread.CurrentThread.Name} exits from MyMethod()");
}
}
}
输出
这是一个可能的输出。
***Comparing a forground threads with a background thread****
Main Thread has started.
Starting Child Thread-1 shortly.
Control comes at the end of Main() method.
Child Thread-1 enters into MyMethod()
Child Thread-1 from MyMethod() prints 0
但是如果您注释掉前面示例中的下面一行,如下所示,
//childThread.IsBackground = true;
您可能会得到以下输出。
***Comparing a forground threads with a background thread****
Main Thread has started.
Starting Child Thread-1 shortly.
Control comes at the end of Main() method.
Child Thread-1 enters into MyMethod()
Child Thread-1 from MyMethod() prints 0
Child Thread-1 from MyMethod() prints 1
Child Thread-1 from MyMethod() prints 2
Child Thread-1 from MyMethod() prints 3
Child Thread-1 from MyMethod() prints 4
Child Thread-1 from MyMethod() prints 5
Child Thread-1 from MyMethod() prints 6
Child Thread-1 from MyMethod() prints 7
Child Thread-1 from MyMethod() prints 8
Child Thread-1 from MyMethod() prints 9
Child Thread-1 exits from MyMethod()
这告诉你子线程(又名工作线程)能够完成它的任务;当您不使它成为后台线程时,它可以在主线程完成其执行后继续其任务。
附加说明
我还在将IsBackground
属性设置为true
时注释了下面一行。
//Console.ReadKey();
这是因为我不想等待用户输入。我希望一旦主线程死亡,子线程立即终止。
Note
在许多上下文中(特别是在 UI 应用中),您会看到术语工作线程。它描述了不同于当前线程的另一个线程。从技术上来说,它是一个在后台运行的线程,尽管没有人声称这是真正的定义。微软写道,“工作线程通常用于处理后台任务,用户不必等待就可以继续使用你的应用。诸如重新计算和后台打印之类的任务就是工作线程的典型例子。”(见 https://docs.microsoft.com/en-us/cpp/parallel/multithreading-creating-worker-threads?view=vs-2019
)。
微软说,在 C# 的环境中,“默认情况下,一个. NET 程序是由一个单独的线程启动的,通常被称为主线程。但是,它可以创建额外的线程来与主线程并行或并发地执行代码。这些线程通常被称为工作线程。(参见 https://docs.microsoft.com/en-us/dotnet/standard/threading/threads-and-threading
)。
线程安全
有时多个线程需要访问共享资源。控制这些情况是棘手的;例如,考虑当一个线程试图从文件中读取数据,而另一个线程仍在同一文件中写入或更新数据。如果你不能管理正确的顺序,你可能会得到令人惊讶的结果。在这些情况下,同步的概念很有用。
非同步版本
为了理解同步方法的必要性,让我们从一个没有实现这个概念的程序开始。在下面的演示中,一个名为SharedResource
的类包含一个名为SharedMethod()
的公共方法。让我们假设在这个方法中,有可以在多个线程之间共享的资源。为了简单起见,我用一些简单的语句来表示线程的入口和出口。为了精确地观察效果,我在方法体中放了一个简单的Sleep
语句。它增加了将执行切换到另一个线程的可能性。
我在Main
方法中创建了两个子线程:Child Thread-1
和Child Thread-2
。请注意演示中的以下代码行。
SharedResource sharedObject = new SharedResource();
Thread threadOne = new Thread(sharedObject.SharedMethod);
threadOne.Name = "Child Thread-1";
Thread threadTwo = new Thread(sharedObject.SharedMethod);
threadTwo.Name = "Child Thread-2";
一旦您运行这个程序的非同步版本,您可能会在可能的输出中注意到下面几行。
子线程-1 已经进入共享位置。
子线程-2 已经进入共享位置。
子线程-1 退出。
子线程-2 退出。
从这个输出片段中,您可以看到Child Thread-1
首先进入了共享位置。但是在它完成执行之前,Child Thread-2
也已经进入了共享位置。
当您处理共享资源(或共享位置)时,您需要非常小心,因此如果有任何线程正在那里工作,您可能希望限制任何其他线程进入该位置。
演示 8
这个完整的例子描述了这种情况。
using System;
using System.Threading;
namespace ExploringTheNeedofSynchronizationInDotNetCore
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Thread Synchronization.****");
Console.WriteLine("***We are beginning with a non-synchronized version.****");
Thread.CurrentThread.Name = "Main Thread";
Console.WriteLine("Main thread has started already.");
SharedResource sharedObject = new SharedResource();
Thread threadOne = new Thread(sharedObject.SharedMethod);
threadOne.Name = "Child Thread-1";
Thread threadTwo = new Thread(sharedObject.SharedMethod);
threadTwo.Name = "Child Thread-2";
// Child Thread-1 starts.
threadOne.Start();
// Child Thread-2 starts.
threadTwo.Start();
// Waiting for Child Thread-1 to finish.
threadOne.Join();
// Waiting for Child Thread-2 to finish.
threadTwo.Join();
Console.WriteLine("The {0} exits now.", Thread.CurrentThread.Name);
Console.ReadKey();
}
}
class SharedResource
{
public void SharedMethod()
{
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
}
}
输出
这是一个完全可能的输出。
***Exploring Thread Synchronization.****
***We are beginning with a non-synchronized version.****
Main thread has started already.
Child Thread-1 has entered in the shared location.
Child Thread-2 has entered in the shared location.
Child Thread-1 exits.
Child Thread-2 exits.
The Main Thread exits now.
Note
每次在系统中运行时,此输出可能会有所不同。为了获得相同的输出,您可能需要多次执行该应用。
同步版本
我相信你现在明白同步版本的必要性了。所以,让我们实现同步的概念,更新前面的演示。
演示 9
在本演示中,您将看到锁的使用。这种锁定机制通常防止由于同时访问共享位置中的多个线程而导致的共享资源的意外修改;当你成功地实现了这一点,你就可以说你的应用是线程安全的。
首先,我来解释一些常用术语。这些术语经常在类似的上下文中使用。您想要防止多个线程同时访问的代码段被称为临界段。在任何给定的时刻,您只允许一个线程在临界区工作。这个原理被称为互斥。实施这一原则的机制通常被称为互斥。
当线程获得锁时,它可以进入临界区。一旦它的工作完成,它就从这个位置退出并释放锁。现在,另一个线程可以获得锁并继续。但是如果一个线程想进入临界区,看到锁当前被另一个线程持有,它就不能进入。线程需要暂停活动,直到锁被释放。
你如何创建一个锁?这非常简单,通常用同一个对象/类中的私有实例变量来完成,如下所示。
private object myLock = new object(); // You can use any object.
如评论所说,你可以随心所欲的做锁。例如,如果您处理一个静态方法,您甚至可以编写如下代码。
private static StringBuilder strLock = new StringBuilder();
为了在前面的演示中实现线程安全,您可以如下修改SharedResource
类。(注意以粗体显示的新引入的行。)我还在Main
方法内部做了一些修改,以表明它是一个同步版本。因此,我将替换上一个演示中的下面一行
Console.WriteLine("***We are beginning with a non-synchronized version.****");
在接下来的演示中。
Console.WriteLine("***Here we have a synchronized version.We are using the concept of lock.****");
class SharedResource
{
private object myLock = new object();
public void SharedMethod()
{
lock (myLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
}
}
输出
这一次,您会得到以下输出。
***Exploring Thread Synchronization.****
***Here we have a synchronized version.We are using the concept of lock.****
Main thread has started already.
Child Thread-1 has entered in the shared location.
Child Thread-1 exits.
Child Thread-2 has entered in the shared location.
Child Thread-2 exits.
The Main Thread exits now.
你需要记住,当你如下使用 lock 语句时,myLock
是一个引用类型的表达式。
lock(myLock){ // Some code},
例如,在这个例子中,myLock
是一个Object
实例,它只是一个引用类型。但是,如果在如下上下文中使用值类型,而不是使用引用类型,
private int myLock = new int();//not correct
您将得到以下错误。
Error CS0185 'int' is not a reference type as required by the lock statement
使用 Monitor 类的另一种方法
在Monitor
类,
中,成员实现同步。既然你已经看到了locks
的使用,值得注意的是它内部包装了Monitor
的Entry
和Exit
方法。因此,您可以替换下面的代码段
lock (myLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
使用Monitor
的Entry
和Exit
方法的等价代码段,如下所示。
// lock internally wraps Monitor's Entry and Exit method in a try...// finally block.
try
{
Monitor.Enter(myLock);
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
finally
{
Monitor.Exit(myLock);
}
除了这些方法之外,Monitor
类还有其他方法可以发送通知。例如,在这个类中,您可以看到具有不同重载版本的Wait
、Pulse
和PulseAll
方法。以下是对这些方法的简单描述。
-
Wait()
:
使用这种方法,一个线程可以等待其他线程的通知。 -
Pulse()
:
使用这种方法,一个线程可以向另一个线程发送通知。 -
PulseAll()
:
使用这种方法,一个线程可以通知一个进程内的所有其他线程。
除了这些方法,还有另一个有趣的重载版本的方法叫做TryEnter
。
这是这种方法的最简单形式,带有来自 Visual Studio 的说明。
//
// Summary:
// Attempts to acquire an exclusive lock on the specified object.
//
// Parameters:
// obj:
// The object on which to acquire the lock.
//
// Returns:
// true if the current thread acquires the lock; otherwise, false.
//
// Exceptions:
// T:System.ArgumentNullException:
// The obj parameter is null.
public static bool TryEnter(object obj);
如果调用线程可以获得所需对象的锁,TryEnter
方法返回布尔值true
;否则,它将返回false
。使用此方法的不同重载版本,您可以指定一个时间限制,在该时间限制内,您可以尝试获取所需对象的独占锁。
僵局
死锁是一种情况或条件,其中至少有两个进程或线程在等待对方完成或释放控制,以便每个进程或线程都可以完成其工作。这可能导致它们都无法启动(并且它们进入挂起状态。)你可能经常听说这些现实生活中的例子。
没有经验,你找不到工作;没有工作就无法获得经验。
或者,
两个好朋友吵架后,他们都希望对方能重新开始友谊。
POINTS TO REMEMBER
如果没有同步,您可能会看到令人惊讶的输出(例如,一些损坏的数据),但是如果同步使用不当,您可能会遇到死锁。
死锁的类型
理论上,有不同类型的死锁。
-
资源死锁。假设两个进程(P1 和 P2)拥有两个资源(分别是 R1 和 R2)。P1 请求资源 R2 和 P2 请求资源 R1 来完成他们的工作。操作系统通常处理这种类型的死锁。
-
同步死锁。假设流程 P1 仅在 P2 完成特定操作(a2)后等待执行操作(a1),P2 仅在 P1 完成 a1 后等待完成操作 a2。
-
通信死锁。与前面的场景类似,您可以用消息替换动作/资源的概念(即,两个流程等待从对方接收消息以进一步处理)。
在这一章中,我们关注的是多线程环境,所以我们只讨论在 C# 应用中由多线程引起的死锁。
演示 10
下面的程序会导致死锁。这个程序中使用了两个锁。这些锁分别被称为myFirstLock
和mySecondLock
。出于演示目的,本例中显示了错误的设计;你看Child Thread-1
试图获得myFirstLock
,然后mySecondLock
和Child Thread-2
试图以相反的顺序获得锁。因此,当两个线程同时锁定它们的第一个锁时,它们会遇到死锁的情况。
同样,下面是一个不正确的实现,仅用于演示目的。
using System;
using System.Threading;
namespace DeadlockDemoInDotNetCore
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Deadlock with an incorrect design of application.****");
Thread.CurrentThread.Name = "Main Thread";
Console.WriteLine("Main thread has started already.");
SharedResource sharedObject = new SharedResource();
Thread threadOne = new Thread(sharedObject.SharedMethodOne);
threadOne.Name = "Child Thread-1";
Thread threadTwo = new Thread(sharedObject.SharedMethodTwo);
threadTwo.Name = "Child Thread-2";
// Child Thread-1 starts.
threadOne.Start();
// Child Thread-2 starts.
threadTwo.Start();
// Waiting for Child Thread-1 to finish.
threadOne.Join();
// Waiting for Child Thread-2 to finish.
threadTwo.Join();
Console.WriteLine("The {0} exits now.", Thread.CurrentThread.Name);
Console.ReadKey();
}
}
class SharedResource
{
private object myFirstLock = new object();
private object mySecondLock = new object();
public void SharedMethodOne()
{
lock (myFirstLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into first part of SharedMethodOne. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodOne--first part.\n");
lock (mySecondLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into last part of SharedMethodOne. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodOne--last part.\n");
}
}
}
public void SharedMethodTwo()
{
lock (mySecondLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into first part of SharedMethodTwo. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodTwo--first part.\n");
lock (myFirstLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into last part of SharedMethodTwo. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodTwo--last part.\n");
}
}
}
}
}
输出
当您的程序挂起时,您只能在输出中看到以下几行。
***Exploring Deadlock with an incorrect design of application.****
Main thread has started already.
Child Thread-1 has entered into first part of SharedMethodOne.
Child Thread-2 has entered into first part of SharedMethodTwo.
Child Thread-1 exits SharedMethodOne--first part.
Child Thread-2 exits SharedMethodTwo--first part.
Note
第一次运行可能不会遇到死锁,但会一直执行程序;最终会出现你看到死锁的情况。
调查 Visual Studio 中的死锁状态
在这种挂起状态下,进入调试 ➤ 断开所有。然后进入调试 ➤ 窗口 ➤ 线程。
你会看到一个类似图 5-4 的屏幕。请注意,您可以看到主线程和子线程的状态。让我们打开所有的窗户(见图 5-4 、 5-5 和 5-6 )。
图 5-4 为主线程窗口。
图 5-4
主线程窗口处于死锁状态
图 5-5 为子线程 1 窗口。
图 5-5
处于死锁状态的子线程 1 窗口
图 5-6 为子线程 2 窗口。
图 5-6
子线程 2 窗口处于死锁状态
如果将窗口分割成垂直和水平两部分,可以一次看到全部,如图 5-7 所示。
图 5-7
死锁状态下主线程、子线程 1、子线程 2 窗口一览
Note
要垂直拆分一个窗口,你可以去窗口 ➤ 新窗口创建一个标签的克隆。请注意名为 Program.cs:1 和 Program2.cs:2 的选项卡。右击其中任何一个,然后选择新建垂直选项卡组。类似地,要水平分割窗口,选择新建水平选项卡组。在图 5-7 中,我垂直划分了窗口,然后水平划分了其中一个。
您应该能够清楚地看到线程被子线程中的锁语句卡住了。作为副作用,Main
螺纹也卡在了threadOne.Join().
上
在多线程程序的执行过程中,如果 Visual Studio 向您显示挂起状态,您可以用类似的方式调查原因。还需要注意的是,死锁可能发生在许多不同的情况下,但是本节的重点是锁。
这是线程编程的基础。像任何高级主题一样,深入的讨论需要更多的页面。不过,你现在应该对基本面有一个公平的想法。
最后的话
现在你可能明白为什么从下面这行代码开始了。
using System.Threading;
是因为这个命名空间有Thread
类,是线程编程的基础。Thread
是一个sealed
类,它有很多成员,包括属性、方法、构造函数和析构函数。到目前为止,您已经看到了以下内容。
-
使用以下两个构造函数:
public Thread(ThreadStart start); public Thread(ParameterizedThreadStart start);
-
使用以下属性:
public bool IsBackground { get; set; } public static Thread CurrentThread { get; } public string Name { get; set; } public int ManagedThreadId { get; }
-
使用以下方法:
public void Start(); public void Join(); public void Abort();
还有许多其他成员也是有用的。我建议你看看它们。如果您使用的是 Visual Studio IDE,只需右键单击Thread
类,然后选择转到定义(F2) 来查看它们的定义。
同样值得注意的是,Thread
类的一些成员被弃用,(例如,Suspend
和Resume)
)。Microsoft 建议您为应用使用其他方法,而不是这些方法。例如,在Suspend
方法定义中,您会看到如下内容。
[Obsolete("Thread.Suspend has been deprecated. Please use other classes in System.Threading, such as Monitor, Mutex, Event, and Semaphore, to synchronize Threads or protect resources. http://go.microsoft.com/fwlink/?linkid=14202", false)]
[SecuritySafeCritical]
public void Suspend();
这也告诉您,Monitor
、Mutex
、Event
和Semaphore
是您在程序中实现同步时的重要类。对这些类更详细的描述超出了本书的范围。
最后,当您希望并发执行而不是顺序执行时,您可以使用多线程,并且您可能认为创建线程可以提高应用的性能。但这可能并不总是对的!您应该限制应用中的线程数量,以避免线程间过多的上下文切换。上下文切换带来的开销会降低应用的整体性能。
这些是 C# 中线程编程的基础。多线程是一个复杂的话题,它有几个方面。一整本书都可以用来讨论这个话题。不过,我相信这一章会让你对基本原理有一个清晰的概念。在下一章中,我们将讨论异步编程,你将在类似的环境中学习一些有趣的概念。
摘要
本章讨论了以下关键问题。
-
什么是线程,它与进程有什么不同?
-
如何创建线程?
-
如何使用不同的线程类构造函数?
-
使用
ParameterizedThreadStart
委托,如何使用接受多个参数的方法? -
如何使用重要的
Thread
类成员? -
如何区分前台线程和后台线程?
-
什么是同步,为什么需要同步?
-
在 C# 中如何使用 lock 语句实现线程安全?
-
如何使用 Monitor 的
Entry
和Exit
方法实现另一种锁定语句的方法? -
什么是死锁,如何检测系统中的死锁?***
六、异步编程
异步编程是艰难和具有挑战性的,但也是有趣的。也被称为异步。整体概念不是一天进化出来的;这需要时间。async 和 await 关键字第一次出现在 C# 5.0 中是为了使它更容易。在此之前,不同的程序员使用不同的技术实现了这个概念。每种技术都有其优缺点。本章的目标是向你介绍异步编程,并通过一些常见的实现方法。
概观
我们先来讨论一下什么是异步编程。简单地说,你在你的应用中取一个代码段,并在一个单独的线程上运行它。关键优势是什么?简单的答案是,您可以释放原始线程,让它继续完成剩余的任务,并在一个单独的线程中执行不同的任务。这种机制帮助您开发现代应用;例如,当您实现一个高度响应的用户界面时,这些概念非常有用。
POINTS TO REMEMBER
概括地说,您会注意到异步编程中有三种不同的模式。
-
**IAsyncResult 模式:**这也被称为异步编程模型(APM) 。在这个模式中,您可以看到支持异步行为的
IAsyncResult
接口。在同步模型中,如果您有一个名为 XXX()的同步方法,在异步版本中,您会看到相应的同步方法使用了BeginXXX()
和EndXXX()
方法。比如在同步版本中,如果Read()
方法支持读操作,在异步编程中,你看到BeginRead()
和EndRead()
方法异步支持相应的读操作。使用这个概念,您可以在演示 5、6 和 7 中看到BeginInvoke
和EndInvoke
方法。然而,新的开发不推荐这种模式。 -
基于事件的异步模式 : 这种模式伴随而来。NET 框架 2.0。它基于一种事件机制。这里您可以看到带有
Async
后缀的方法名、一个或多个事件以及EventArg
派生类型。新开发不推荐使用这种模式。 -
基于任务的异步模式 : 这最早出现在。NET 框架 4。这是目前异步编程的推荐做法。在 C# 中,你经常会看到这种模式中的
async
和await
关键字。
为了更好地理解异步编程,让我们从它的对应物开始讨论:同步编程。同步方法很简单,代码路径也很容易理解;但是你需要等待来自特定代码段的结果,在那之前,你只是无所事事。考虑一些典型案例;例如,当您知道一段代码试图打开一个可能需要时间加载的网页时,或者当一段代码正在运行一个长时间运行的算法时,等等。如果您遵循同步方法,当您执行长时间运行的操作时,您必须闲置,因为您不能做任何有用的事情。
这就是为什么为了支持现代需求和构建高响应性的应用,对异步编程的需求越来越大。
使用同步方法
演示 1 执行一个简单的程序。让我们从同步方法开始。有两个简单的方法:Method1()
和Method2()
。在Main()
方法内部,这些方法被同步调用(即先调用Method1()
,再调用Method2()
)。)我使用了简单的 sleep 语句,因此这些方法执行的任务需要花费大量的时间来完成。一旦您运行应用并注意到输出,您会看到只有在Method1()
完成执行后,Method2()
才开始执行。在这些方法完成它们的执行之前,Main()
方法不能完成。
Note
在本章中,你会看到这些方法略有不同。我试图维护相似的方法(或操作),以便您可以比较异步编程的不同技术。出于演示的目的,Method1()
需要更多的时间来完成,因为它执行了一个冗长的操作(我在其中强制了一个相对较长的睡眠)。Method2()
执行一个小任务,所以我在里面放了一个短睡眠。此外,为了简单起见,我使用了简称。
演示 1
这是完整的演示。
using System;
using System.Threading;
namespace SynchronousProgrammingExample
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-1.A Synchronous Program Demo.***");
Method1();
Method2();
Console.WriteLine("End Main().");
Console.ReadKey();
}
// Method1
private static void Method1()
{
Console.WriteLine("Method1() has started.");
// Some big task
Thread.Sleep(1000);
Console.WriteLine("Method1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是输出。
***Demonstration-1.A Synchronous Program Demo.***
Method1() has started.
Method1() has finished.
Method2() has started.
Method2() has finished.
End Main().
使用线程类
如果您仔细观察演示 1 中的方法,您会发现它们并不相互依赖。如果您可以并行执行它们,您的应用的响应时间将会得到改善,并且您可以减少总的执行时间。让我们找到一些更好的方法。
你在第五章中学到了线程,所以你可以实现多线程的概念。演示 2 向您展示了一个使用线程的显而易见的解决方案。我保留了注释代码供您参考。这个演示的重点是在一个新线程中替换Method1()
。
演示 2
using System;
using System.Threading;
namespace UsingThreadClass
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Asynchronous Programming Demonstration-1.***");
//Method1();
// Old approach.Creating a separate thread for the following // task(i.e Method1.)
Thread newThread = new Thread(()=>
{
Console.WriteLine("Method1() has started on a separate thread.");
// Some big task
Thread.Sleep(1000);
Console.WriteLine("Method1() has finished.");
}
);
newThread.Start();
Thread.Sleep(10);
Method2();
Console.WriteLine("End Main().");
Console.ReadKey();
}
// Method1
//private static void Method1()
//{
// Console.WriteLine("Method1() has started.");
// // Some big task
// Thread.Sleep(1000);
// Console.WriteLine("Method1() has finished.");
//}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是一种可能的输出。
***Asynchronous Programming Demonstration-1.***
Method1() has started on a separate thread.
Method2() has started.
Method2() has finished.
End Main().
Method1() has finished.
分析
注意,虽然Method1()
被提前调用,但是Method2
不需要等待Method1()
完成执行。此外,由于Method2()
做得很少(睡眠时间为 100 毫秒),它能够在Method1()
完成执行之前完成。还要注意,因为主线程没有被阻塞,所以它能够继续执行。
问答环节
6.1 为什么在 Main 中执行 Method2()之前使用 sleep 语句?
接得好。这不是必须的,但是在某些情况下,您可能会注意到,即使您试图在当前线程中的Method2()
之前启动Method1()
在一个单独的线程上执行,这也不会发生,因此,您可能会注意到以下输出。
***Asynchronous Programming Demonstration-1.***
Method2() has started.
Method1() has started in a separate thread.
Method2() has finished.
End Main().
Method1() has finished.
在这个例子中,这个简单的 sleep 语句可以帮助你增加在Method2()
之前开始Method1()
的概率。
使用线程池类
通常不鼓励在现实世界的应用中直接创建线程。以下是这背后的一些主要原因。
-
维护太多的线程会导致困难和高成本的操作。
-
大量时间浪费在上下文切换上,而不是做真正的工作。
为了避免直接创建线程,C# 为您提供了使用内置ThreadPool
类的便利。有了这个类,您可以使用现有的线程,这些线程可以重用以满足您的需要。ThreadPool
类在维护应用中的最佳线程数量方面非常有效。如果需要,您可以使用这个工具异步执行一些任务。
ThreadPool
是一个包含static
方法的静态类,其中一些方法还有一个重载版本。图 6-1 是 Visual Studio IDE 的部分截图,展示了ThreadPool
类中的方法。
图 6-1
Visual Studio 2019 IDE 中 ThreadPool 类的截图
在本节中,我们的重点是QueueUserWorkItem
方法。在图 6-1 中,注意这个方法有两个重载版本。要了解关于此方法的更多信息,让我们展开 Visual Studio 中的方法描述。例如,一旦展开此方法的第一个重载版本,就会看到以下内容。
//
// Summary:
// Queues a method for execution. The method executes when a thread // pool thread becomes available.
//
// Parameters:
// callBack:
// A System.Threading.WaitCallback that represents the method to be // executed.
//
// Returns:
// true if the method is successfully queued; System.// NotSupportedException is thrown
// if the work item could not be queued.
//
// Exceptions:
// T:System.ArgumentNullException:
// callBack is null.
//
// T:System.NotSupportedException:
// The common language runtime (CLR) is hosted, and the host does not// support this action.
[SecuritySafeCritical]
public static bool QueueUserWorkItem(WaitCallback callBack);
如果您进一步研究方法参数,您会发现WaitCallBack
是一个具有以下描述的委托。
//
// Summary:
// Represents a callback method to be executed by a thread pool thread.
//
// Parameters:
// state:
// An object containing information to be used by the callback method.
[ComVisible(true)]
public delegate void WaitCallback(object state);
第二个重载版本的QueueUserWorkItem
可以接受一个名为state
的object
参数。内容如下。
public static bool QueueUserWorkItem(WaitCallback callBack, object state);
如果查看细节,您会发现可以通过这个参数将有价值的数据传递给方法。在演示 3 中,我使用了两个重载版本,并且我引入了Method3
,其中我传递了一个对象参数。
演示 3
为了有效地使用QueueUserWorkItem
方法,您需要一个匹配WaitCallBack
委托签名的方法。在下面的演示中,我在ThreadPool
中对两个方法进行排队。在演示 2 中,Method2 不接受任何参数。如果将它传递给QueueUserWorkItem
,会得到如下编译错误。
No overload for 'Method2' matches delegate 'WaitCallback'
让我们用一个虚拟对象参数来修改Method2
,如下(我保留了注释供你参考)。
/* The following method's signature should match the delegate WaitCallback.*/
private static void Method2(Object state)
{
Console.WriteLine("--Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("--Method2() has finished.");
}
接下来,我们来介绍一下使用了Object
参数的Method3,
。Method3
描述如下。
static void Method3(Object number)
{
Console.WriteLine("---Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("---Method3() has finished.");
}
现在来看下面的演示和相应的输出。
using System;
using System.Threading;
namespace UsingThreadPool
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Asynchronous Programming Demonstration.***");
Console.WriteLine("***Using ThreadPool.***");
// Using Threadpool
// Not passing any parameter for Method2
ThreadPool.QueueUserWorkItem(new WaitCallback(Method2));
// Passing 10 as the parameter for Method3
ThreadPool.QueueUserWorkItem(new WaitCallback(Method3), 10);
Method1();
Console.WriteLine("End Main().");
Console.ReadKey();
}
private static void Method1()
{
Console.WriteLine("-Method1() has started.");
// Some big task
Thread.Sleep(1000);
Console.WriteLine("-Method1() has finished.");
}
/* The following method's signature should match the delegate WaitCallback.
It is as follows:
public delegate void WaitCallback(object state)
*/
//private static void Method2()//Compilation error
private static void Method2(Object state)
{
Console.WriteLine("--Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("--Method2() has finished.");
}
/*
The following method has a parameter.This method's signature matches the WaitCallBack delegate signature.Notice that this method also matches the ParameterizedThreadStart delegate signature; because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method3(Object number)
{
Console.WriteLine("---Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("---Method3() has finished.");
}
}
}
输出
这是一个可能的输出。
***Asynchronous Programming Demonstration.***
***Using ThreadPool.***
-Method1() has started.
---Method3() has started.
---Method3() prints 3.00
---Method3() prints 3.01
---Method3() prints 3.02
---Method3() prints 3.03
--Method2() has started.
---Method3() prints 3.04
---Method3() prints 3.05
---Method3() prints 3.06
---Method3() prints 3.07
---Method3() prints 3.08
---Method3() prints 3.09
--Method2() has finished.
---Method3() has finished.
-Method1() has finished.
End Main().
问答环节
6.2 按照简单的委托实例化技术,如果我使用下面的代码行:
ThreadPool.QueueUserWorkItem(Method2);
instead of this line:
ThreadPool.QueueUserWorkItem(new WaitCallback(Method2));
will the application compile and run?
Yes, but since you are learning to use the WaitCallback delegate now, I kept it for your reference.
对线程池使用 Lambda 表达式
如果您喜欢 lambda 表达式,您可以在类似的上下文中使用它。例如,在演示 3 中,您可以使用 lambda 表达式替换Method3
,如下所示。
// Using lambda Expression
// Here the method needs a parameter(input).
// Passing 10 as the parameter for Method3
ThreadPool.QueueUserWorkItem((number) =>
{
Console.WriteLine("---Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("---Method3() has finished.");
}, 10
);
在演示 3 中,您可以注释掉下面的代码行,并用前面显示的 lambda 表达式替换Method3
。
ThreadPool.QueueUserWorkItem(new WaitCallback(Method3), 10);
如果您再次执行该程序,您会得到类似的输出。演示 4 是完整的实现,供您参考。
演示 4
using System;
using System.Threading;
namespace UsingThreadPoolWithLambdaExpression
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Asynchronous Programming Demonstration.***");
Console.WriteLine("***Using ThreadPool with Lambda Expression.***");
// Using Threadpool
// Not passing any parameter for Method2
ThreadPool.QueueUserWorkItem(Method2);
// Using lambda Expression
// Here the method needs a parameter(input).
// Passing 10 as the parameter for Method3
ThreadPool.QueueUserWorkItem( (number) =>
{
Console.WriteLine("--Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("--Method3() has finished.");
}, 10
);
Method1();
Console.WriteLine("End Main().");
Console.ReadKey();
}
private static void Method1()
{
Console.WriteLine("-Method1() has started.");
// Some task
Thread.Sleep(500);
Console.WriteLine("-Method1() has finished.");
}
/* The following method's signature should match the delegate WaitCallback.
It is as follows:
public delegate void WaitCallback(object state)
*/
//private static void Method2()//Compilation error
private static void Method2(Object state)
{
Console.WriteLine("--Method2() has started.");
// Some task
Thread.Sleep(100);
Console.WriteLine("--Method2() has finished.");
}
}
}
输出
这是一个可能的输出。
***Asynchronous Programming Demonstration.***
***Using ThreadPool with Lambda Expression.***
-Method1() has started.
--Method3() has started.
---Method3() prints 3.00
--Method2() has started.
---Method3() prints 3.01
---Method3() prints 3.02
---Method3() prints 3.03
---Method3() prints 3.04
---Method3() prints 3.05
---Method3() prints 3.06
---Method3() prints 3.07
---Method3() prints 3.08
---Method3() prints 3.09
--Method2() has finished.
--Method3() has finished.
-Method1() has finished.
End Main().
Note
这一次,您看到了 lambda 表达式在ThreadPool
类中的使用。在演示 2 中,您看到了 lambda 表达式在Thread
类中的使用。
使用 IAsyncResult 模式
IAsyncResult
接口帮助您实现异步行为。让我们回忆一下我早些时候告诉你的话。在同步模型中,如果有一个名为 XXX 的同步方法,在异步版本中,BeginXXX
和EndXXX
方法就是对应的同步方法。让我们仔细看看。
使用异步委托进行轮询
到目前为止,您已经看到了委托的许多不同用法。在本节中,您将了解另一种用法,即通过使用委托,您可以异步调用方法。轮询是一种重复检查条件的机制。在演示 5 中,让我们检查一个委托实例是否完成了它的任务。
演示 5
有两种方法叫做Method1
和Method2
。让我们再次假设Method1
比Method2
花费更多的时间来完成它的任务。简单来说,Sleep()
语句在这些方法内部传递。在这个例子中,Method1
接收一个休眠 3000 毫秒的参数,Method2
休眠 100 毫秒。
现在看看代码的重要部分。首先,创建一个委托实例来匹配Method1
签名。Method1
如下。
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
为了匹配签名,如下声明Method1Delegate
。
public delegate void Method1Delegate(int sleepTimeinMilliSec);
稍后,如下实例化它。
Method1Delegate method1Del = Method1;
到目前为止,一切都很简单。现在我们来看代码中最重要的一行,如下所示。
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
你还记得在委托的上下文中,你可以使用Invoke()
方法吗?但是上一次,代码遵循同步路径。现在我们正在探索异步编程,您会看到BeginInvoke
和EndInvoke
方法的使用。当 C# 编译器看到 delegate 关键字时,它会为动态生成的类提供这些方法。
BeginInvoke
方法的返回类型是IAsyncResult
。如果您将鼠标悬停在BeginInvoke
上或者注意它的结构,您会看到虽然Method1
只接受一个参数,但是BeginInvoke
方法总是接受两个额外的参数——一个类型为AsyncCallback
,一个类型为object
。我很快会讨论它们。
在这个例子中,我只使用第一个参数,并将 3000 毫秒作为Method1
的参数。但是对于BeginInvoke
的最后两个参数,我通过了null
。
BeginInvoke
的结果很重要。我将结果保存在一个IAsyncResult
对象中。IAsyncResult
具有以下只读属性。
public interface IAsyncResult
{
bool IsCompleted { get; }
WaitHandle AsyncWaitHandle { get; }
object AsyncState { get; }
bool CompletedSynchronously { get; }
}
目前,我关注的是isCompleted
属性。如果您进一步扩展这些定义,您会看到isCompleted
的定义如下。
//
// Summary:
// Gets a value that indicates whether the asynchronous operation has// completed.
//
// Returns:
// true if the operation is complete; otherwise, false.
bool IsCompleted { get; }
很明显,您可以使用这个属性来验证委托是否已经完成了它的工作。
在下面的例子中,我检查另一个线程中的委托是否已经完成了它的工作。如果工作没有完成,我会在控制台窗口中打印星号(*),并强制主线程短暂休眠。这就是您在本演示中看到下面这段代码的原因。
while (!asyncResult.IsCompleted)
{
// Keep working in main thread
Console.Write("*");
Thread.Sleep(5);
}
最后,EndInvoke
方法接受一个类型为IAsyncResult
的参数。我通过asyncResult
作为这个方法中的一个参数。
现在进行完整的演示。
using System;
using System.Threading;
namespace PollingDemo
{
class Program
{
public delegate void Method1Delegate(int sleepTimeinMilliSec);
static void Main(string[] args)
{
Console.WriteLine("***Polling Demo.***");
Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Synchronous call
//Method1(3000);
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
Method2();
while (!asyncResult.IsCompleted)
{
// Keep working in main thread
Console.Write("*");
Thread.Sleep(5);
}
method1Del.EndInvoke(asyncResult);
Console.ReadKey();
}
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是一种可能的输出。
***Polling Demo.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Method1() has finished.
问答环节
6.3 上一个案例中,Method1 带一个参数, BeginInvoke
带三个参数。如果 Method1
接受 n
数量的参数,那么 BeginInvoke
就会有 n+2
参数。
是的,初始的参数集是基于您的方法的,但是对于最后两个参数,一个是类型AsyncCallback
的,最后一个是类型object
的。
POINTS TO REMEMBER
-
这种类型的例子在。NET 框架 4.7.2。如果你在。NET Core 3.0,你得到一个异常说“系统。PlatformNotSupportedException:此平台不支持操作。其中一个主要原因是异步委托实现依赖于中不存在的远程处理功能。NET 核心。关于这一点的讨论是在
https://github.com/dotnet/runtime/issues/16312
。 -
如果您不想在控制台窗口中检查和打印星号(*),您可以在主线程完成执行后简单地调用委托类型的
EndInvoke()
方法。EndInvoke()
方法一直等到代理完成它的工作。 -
If you don’t explicitly examine whether the delegate finishes its execution or not, or you simply forget to call
EndInvoke()
, the thread of the delegate is stopped after the main thread dies. For example, if you comment out the following segment of code from the prior example,//while (!asyncResult.IsCompleted) //{ // Keep working in main thread // Console.Write("*"); // Thread.Sleep(5); //} //method1Del.EndInvoke(asyncResult); //Console.ReadKey();
并再次运行应用,您可能看不到
"Method1() has finished."
语句。 -
BeginInvoke
通过使用EndInvoke
帮助调用线程稍后获得异步方法调用的结果。
使用 IAsyncResult 的 AsyncWaitHandle 属性
现在我将向您展示一种使用另一个属性AsyncWaitHandle
的替代方法,这个属性在IAsyncResult
中也是可用的。如果看到IAsyncResult
的内容,发现AsyncWaitHandle
返回WaitHandle
,有如下描述。
//
// Summary:
// Gets a System.Threading.WaitHandle that is used to wait for an // asynchronous operation to complete.
//
// Returns:
// A System.Threading.WaitHandle that is used to wait for an // asynchronous operation to complete.
WaitHandle AsyncWaitHandle { get; }
Visual Studio IDE 确认WaitHandle
是一个等待对共享资源进行独占访问的抽象类。在WaitHandle
中,你可以看到有五个不同重载版本的WaitOne()
方法。
public virtual bool WaitOne(int millisecondsTimeout);
public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);
public virtual bool WaitOne(TimeSpan timeout);
public virtual bool WaitOne(TimeSpan timeout, bool exitContext);
public virtual bool WaitOne();
通过使用WaitHandle,
,你可以等待一个委托线程完成它的工作。在演示 6 中,使用了第一个重载版本,并提供了一个可选的超时值,单位为毫秒。如果等待成功,控制从while
循环中退出;但是如果超时发生,WaitOne()
返回 false,并且while
循环继续并在控制台中打印星号(*)。
演示 6
using System;
using System.Threading;
namespace UsingWaitHandle
{
class Program
{
public delegate void Method1Delegate(int sleepTimeinMilliSec);
static void Main(string[] args)
{
Console.WriteLine("***Polling and WaitHandle Demo.***");
Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Synchronous call
//Method1(3000);
// Asynchrous call using a delegate
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
Method2();
// while (!asyncResult.IsCompleted)
while (true)
{
// Keep working in main thread
Console.Write("*");
/* There are 5 different overload method for WaitOne().Following method blocks the current thread until the current System.Threading.WaitHandle receives a signal, using a 32-bit signed integer to specify the time interval in milliseconds.
*/
if (asyncResult.AsyncWaitHandle.WaitOne(10))
{
Console.Write("\nResult is available now.");
break;
}
}
method1Del.EndInvoke(asyncResult);
Console.WriteLine("\nExiting Main().");
Console.ReadKey();
}
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
// It will have a different thread id
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Main thread id and this thread id will be same
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是一种可能的输出。
***Polling and WaitHandle Demo.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
*************************************************************************************************************************************************************
Method1() has finished.
*
Result is available now.
Exiting Main().
分析
如果将这个演示与上一个进行比较,您会注意到这里您等待异步操作以不同的方式完成。这次你没有使用IsCompleted
属性,而是使用了IAsyncResult
的AsyncWaitHandle
属性。
使用异步回调
让我们回顾一下前两次演示中的BeginInvoke
方法。
// Asynchrous call using a delegate
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
这意味着为最后两个方法参数传递了两个null
值。如果您将鼠标悬停在这些先前演示的行上,您会注意到在本例中,BeginInvoke
期望一个IAsyncCallback
委托作为第二个参数,一个object
作为第三个参数。
让我们调查一下IAsyncCallback
代表。Visual Studio IDE 说此委托是在系统命名空间中定义的;它有以下描述。
//
// Summary:
// References a method to be called when a corresponding asynchronous // operation completes.
//
// Parameters:
// ar:
// The result of the asynchronous operation.
[ComVisible(true)]
public delegate void AsyncCallback(IAsyncResult ar);
你可以使用一个callback
方法来执行一些有用的东西(例如,家务工作)。AsyncCallback
委托有一个void
返回类型,它接受一个IAsyncResult
参数。让我们定义一个可以匹配这个委托签名的方法,并在Method1Del
实例完成执行后调用这个方法。
下面是一个示例方法,将在接下来的演示中使用。
// Method3: It's a callback method.
// This method will be invoked when Method1Delegate completes its work.
private static void Method3(IAsyncResult asyncResult)
{
if (asyncResult != null) // if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
Console.WriteLine("Method3() has finished.");
}
}
演示 7
现在查看完整的实现。
using System;
using System.Threading;
namespace UsingAsynchronousCallback
{
class Program
{
public delegate void Method1Delegate(int sleepTimeinMilliSec);
static void Main(string[] args)
{
Console.WriteLine("***Using Asynchronous Callback.***");
Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Synchronous call
//Method1(3000);
// Asynchrous call using a delegate
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000,Method3, null);
Method2();
while (!asyncResult.IsCompleted)
{
// Keep working in main thread
Console.Write("*");
Thread.Sleep(5);
}
method1Del.EndInvoke(asyncResult);
Console.WriteLine("Exit Main().");
Console.ReadKey();
}
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
//Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
/* Method3: It's a callback method.This method will be invoked when Method1Delegate completes its work.*/
private static void Method3(IAsyncResult asyncResult)
{
if (asyncResult != null)//if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
Console.WriteLine("Method3() has finished.");
}
}
}
}
输出
这是一个可能的输出。
***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Method1() has finished.
Method3() has started.
Inside Method3(),Thread id 3 .
Exit Main().
Method3() has finished.
分析
请注意,Method3 仅在 Method1()完成执行后才开始工作。还要注意的是Method1()
和Method3()
的线程 ID 是相同的。这是因为Method3()
是从运行 Method1()的线程中调用的。
问答环节
6.4 什么是回调方法?
通常,它是一个仅在特定操作完成后调用的方法。您经常会在异步编程中看到这种方法的使用,在异步编程中,您不知道某个操作的确切完成时间,但希望在前一个任务结束后开始某个特定的任务。例如,在前面的示例中,如果 Method1()在执行期间分配了资源,Method3 可以执行一些清理工作。
我发现 Method3()没有从主线程调用。这是意料之中的吗?
是的。这里您使用了回调方法。在这个例子中,Method3()是回调方法,它只能在 Method1()完成工作后开始执行。因此,从运行 Method1()的同一个线程中调用 Method3()是有意义的。
我可以在这个例子中使用 lambda 表达式吗?
接得好。为了获得类似的输出,在前面的演示中,没有创建一个新方法Method3()
,而是使用下面的代码行,
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, Method3, null);
您可以使用 lambda 表达式替换它,如下所示。
IAsyncResult asyncResult = method1Del.BeginInvoke(3000,
(result) =>
{
if (result != null)//if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
Console.WriteLine("Method3() has finished.");
}
},
null);
6.7 当您在 BeginInvoke
方法中使用回调方法 Method3 时,您传递的不是一个对象作为最终参数,而是一个空值。这有什么具体原因吗?
不,我没有在这些演示中使用该参数。因为它是一个对象参数,你可以传递任何对你有意义的东西。使用回调方法时,可以传递委托实例。它可以帮助您的回调方法分析异步方法的结果。
但是为了简单起见,让我们修改前面的演示并传递一个字符串消息作为BeginInvoke
中的最后一个参数。假设您正在修改现有的代码行,
IAsyncResult asyncResult = method1Del.BeginInvoke(3000,Method3, null);
有了下面这个。
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, Method3, "Method1Delegate, thank you for using me." );
To accommodate this change, let’s modify the Method3() method too.The newly added lines are shown in bold.
private static void Method3(IAsyncResult asyncResult)
{
if (asyncResult != null) // if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
// For Q&A
string msg = (string)asyncResult.AsyncState;
Console.WriteLine("Method3() says : '{0}'",msg);
Console.WriteLine("Method3() has finished.");
}
}
如果您再次运行该程序,这一次您可能会看到以下输出。
***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Method1() has finished.
Method3() has started.
Inside Method3(),Thread id 3 .
Exit Main().
Method3() says : 'Method1Delegate, thank you for using me.'
Method3() has finished.
POINTS TO REMEMBER
您已经看到了使用委托实现轮询、等待句柄和异步回调。这个编程模型也存在于。NET 框架;比如HttpWebRequest
级的BeginGetResponse
、BeginGetRequestStream
,或者SqlCommand
级的BeginExecuteNonQuery()
、BeginExecuteReader()
、BeginExecuteXmlReader()
。这些方法也有重载。
使用基于事件的异步模式(EAP)
在本节中,您将看到 EAP 的用法。起初,基于事件的模式似乎很难理解。根据应用的复杂性,这种模式可以有多种形式。
以下是这种模式的一些关键特征。
-
一般来说,异步方法是其同步版本的精确副本,但是当您调用它时,它在一个单独的线程上启动,然后立即返回。这种机制允许在后台运行预期操作的同时继续调用线程。这些操作的例子可以是长时间运行的过程,例如加载大图像、下载大文件、连接和建立到数据库的连接,等等。EAP 在这些情况下是有帮助的。例如,一旦长时间运行的下载操作完成,就可以引发一个事件来通知信息。事件的订阅者可以根据该通知立即采取行动。
-
您可以同时执行多个操作,并在每个操作完成时收到通知。
-
使用这种模式,您可以利用多线程,但同时也隐藏了整体的复杂性。
-
在最简单的情况下,你的方法名会有一个 Async 后缀来告诉其他人你正在使用一个异步版本的方法。同时,您有一个带有 完成 后缀的相应事件。在理想情况下,您应该有一个相应的 cancel 方法,并且它应该支持显示进度条/报告。支持取消操作的方法也可以命名为method nameasync cancel(或者简称为 CancelAsync )。
-
像
SoundPlayer
、PictureBox
、WebClient
和BackgroundWorker
这样的组件是这种模式的常见代表。
演示 8 是WebClient
的一个简单应用。我们开始吧。
演示 8
在程序的开始,您会看到我需要包含一些特定的名称空间。我用注释来说明为什么这些对于这个演示是必要的。
在这个案例研究中,我想将一个文件下载到我的本地系统中。但是我没有使用来自互联网的真实的URL
,而是将源文件存储在我的本地系统中。这可以给你带来两大好处。
-
运行此应用不需要互联网连接。
-
由于您没有使用互联网连接,下载操作会相对较快。
现在看看下面的代码块,您将在完整的示例中看到它。
WebClient webClient = new WebClient();
// File location
Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
// Target location for download
string targetLocation = @"C:\TestData\downloaded_file.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
到目前为止,事情简单明了。但是我想让你注意下面几行代码。
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
你可以看到在第一行中,我使用了一个在WebClient
中定义的叫做DownloadFileAsync
的方法。在 Visual Studio 中,方法描述告诉我们以下内容。
// Summary:
// Downloads, to a local file, the resource with the specified URI. // This method does not block the calling thread.
//
// Parameters:
// address:
// The URI of the resource to download.
//
// fileName:
// The name of the file to be placed on the local computer.
//
// Exceptions:
// T:System.ArgumentNullException:
// The address parameter is null. -or- The fileName parameter is null.
//
// T:System.Net.WebException:
// The URI formed by combining System.Net.WebClient.BaseAddress and // address is invalid.
// -or- An error occurred while downloading the resource.
//
// T:System.InvalidOperationException:
// The local file specified by fileName is in use by another thread.
public void DownloadFileAsync(Uri address, string fileName);
从方法总结中,您了解到当您使用此方法时,调用线程不会被阻塞。(实际上,DownloadFileAsync
是DownloadFile
方法的异步版本,在WebClient.
中也有定义)
现在来看下一行代码。
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
Visual Studio 对DownloadFileCompleted
事件的描述如下。
/ Summary:
// Occurs when an asynchronous file download operation completes.
public event AsyncCompletedEventHandler DownloadFileCompleted;
它对AsyncCompletedEventHandler
的描述如下。
// Summary:
// Represents the method that will handle the MethodNameCompleted event // of an asynchronous operation.
//
// Parameters:
// sender:
// The source of the event.
//
// e:
// An System.ComponentModel.AsyncCompletedEventArgs that contains the // event data.
public delegate void AsyncCompletedEventHandler(object sender, AsyncCompletedEventArgs e);
您可以订阅DownloadFileCompleted
事件来显示下载操作完成的通知。为此,使用以下方法。
private static void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
Console.WriteLine("Successfully downloaded the file now.");
}
Note
DownloadCompleted
方法匹配AsyncCompletedEventHandler
委托的签名。
既然您已经掌握了委托和事件的概念,您知道您可以替换这一行代码
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
使用下面的代码行。
webClient.DownloadFileCompleted += DownloadCompleted;
为了更好的可读性,我保留了长版本。
现在查看完整的示例和输出。
using System;
// For AsyncCompletedEventHandler delegate
using System.ComponentModel;
using System.Net; // For WebClient
using System.Threading; // For Thread.Sleep() method
namespace UsingWebClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-.Event Based Asynchronous Program Demo.***");
// Method1();
#region The lenghty operation(download)
Console.WriteLine("Starting a download operation.");
WebClient webClient = new WebClient();
// File location
Uri myLocation = new Uri(@"C:\TestData\OriginalFile.txt");
// Target location for download
string targetLocation = @"C:\TestData\DownloadedFile.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
#endregion
Method2();
Console.WriteLine("End Main()...");
Console.ReadKey();
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Some small task
// Thread.Sleep(10);
Console.WriteLine("Method2() has finished.");
}
private static void Completed(object sender, AsyncCompletedEventArgs e)
{
Console.WriteLine("Successfully downloaded the file now.");
}
}
}
输出
这是一个可能的输出。
***Demonstration-.Event Based Asynchronous Program Demo.***
Starting a download operation.
Method2() has started.
Method2() has finished.
End Main()...
Successfully downloaded the file now.
分析
您可以看到下载操作是在Method2()
开始执行之前开始的。然而,Method2()
在下载操作完成之前完成了它的任务。如果你有兴趣看Original.txt
的内容,如下。
Dear Reader,
This is my test file.It is originally stored at C:\TestData in my system.
您可以测试一个类似的文件及其内容,以便在您的终端上进行快速验证。
附加说明
当你引入一个进度条时,你可以使这个例子更好。您可以使用 Windows 窗体应用来获得对进度条的内置支持。我们先忽略Method2
,把重点放在异步下载操作上。你可以做一个基本的表单,如图 6-2 所示,包含三个简单的按钮和一个进度条。(您需要首先将这些控件拖放到您的表单上。我假设你知道这些活动)。
图 6-2
一个简单的 UI 应用,演示基于事件的异步
下面这段代码是不言自明的。
using System;
using System.ComponentModel;
using System.Net;
using System.Threading;
using System.Windows.Forms;
namespace UsingWebClientWithWinForm
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void StartDownload_Click(object sender, EventArgs e)
{
WebClient webClient = new WebClient();
Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
string targetLocation = @"C:\TestData\downloaded_file.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
Thread.Sleep(3000);
MessageBox.Show("Method1() has finished.");
}
private void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
MessageBox.Show("Successfully downloaded the file now.");
}
private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage;
}
private void ResetButton_Click(object sender, EventArgs e)
{
progressBar.Value = 0;
}
private void ExitButton_Click(object sender, EventArgs e)
{
this.Close();
}
}
}
输出
一旦点击StartDownload button
,就会得到如图 6-3 所示的输出。
图 6-3
UI 应用运行时的运行时屏幕截图
问答环节
基于事件的异步程序有哪些优点和缺点?
以下是与这种方法相关的一些常见的优点和缺点。
赞成的意见
- 您可以调用一个长时间运行的方法并立即获得一个返回。当方法完成时,您会收到一个通知。
骗局
-
因为您已经分离了代码,所以理解、调试和维护通常很困难。
-
当您订阅了一个事件,但后来忘记取消订阅时,可能会出现一个大问题。这个错误会导致应用中的内存泄漏,影响可能非常严重;例如,您的系统可能会挂起或没有响应,您可能需要经常重新启动系统。
了解任务
要理解基于任务的异步模式,首先要知道的是,任务只是您想要执行的一个工作单元。您可以在同一个线程或不同的线程中完成这项工作。通过使用任务,您可以更好地控制线程;例如,您可以在任务完成后执行后续工作。父任务可以创建子任务,因此您可以组织层次结构。当你级联你的消息时,这种层次结构是重要的;例如,在您的应用中,您可能决定一旦父任务被取消,子任务也应该被取消。
您可以用不同的方式创建任务。在下面的演示中,我用三种不同的方式创建了三个任务。请注意下面这段带有支持性注释的代码。
#region Different ways to create and execute task
// Using constructor
Task taskOne = new Task(MyMethod);
taskOne.Start();
// Using task factory
TaskFactory taskFactory = new TaskFactory();
// StartNew Method creates and starts a task.
// It has different overloaded version.
Task taskTwo = taskFactory.StartNew(MyMethod);
// Using task factory via a task
Task taskThree = Task.Factory.StartNew(MyMethod);
#endregion
您可以看到所有三个任务都在执行相同的操作。它们中的每一个都在执行MyMethod()
,描述如下。
private static void MyMethod()
{
Console.WriteLine("Task.id={0} with Thread id {1} has started.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
// Some task
Thread.Sleep(100);
Console.WriteLine("MyMethod for Task.id={0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
你可以看到在MyMethod()
内部,为了区分任务和线程,它们对应的 id 被打印在控制台中。
最后一件事。您可以看到方法名作为参数被传递到了StartNew()
方法中。这个方法在编写时有 16 个重载版本,我使用的是如下定义的那个。
//
// Summary:
// Creates and starts a task.
//
// Parameters:
// action:
// The action delegate to execute asynchronously.
//
// Returns:
// The started task.
//
// Exceptions:
// T:System.ArgumentNullException:
// The action argument is null.
public Task StartNew(Action action);
因为在这种情况下MyMethod()
匹配Action
委托的签名,所以对StartNew
使用这种方法没有问题。
演示 9
现在进行完整的演示和输出。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CreatingTasks
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using different ways to create tasks.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
#region Different ways to create and execute task
// Using constructor
Task taskOne = new Task(MyMethod);
taskOne.Start();
// Using task factory
TaskFactory taskFactory = new TaskFactory();
// StartNew Method creates and starts a task.
// It has different overloaded version.
Task taskTwo = taskFactory.StartNew(MyMethod);
// Using task factory via a task
Task taskThree = Task.Factory.StartNew(MyMethod);
#endregion
Console.ReadKey();
}
private static void MyMethod()
{
Console.WriteLine("Task.id={0} with Thread id {1} has started.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("MyMethod for Task.id={0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
}
}
输出
这是一个可能的输出。
***Using different ways to create tasks.****
Inside Main().Thread ID:1
Task.id=2 with Thread id 6 has started.
Task.id=1 with Thread id 5 has started.
Task.id=3 with Thread id 4 has started.
MyMethod for Task.id=1 and Thread id 5 is completed.
MyMethod for Task.id=3 and Thread id 4 is completed.
MyMethod for Task.id=2 and Thread id 6 is completed.
问答环节
6.9 StartNew()
只能用于匹配动作委托签名的方法。这是正确的吗?
不。我在一个接受参数的StartNew
重载中使用了它,参数是匹配动作委托签名的方法的名称。但是,还有其他过载版本的StartNew
;例如,考虑以下情况。
public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, TaskCreationOptions creationOptions);
Or,
public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, CancellationToken cancellationToken);
6.10 在之前的一个 Q & A 中,我看到了 TaskCreationOptions
的用法。这是什么意思?
这是一个enum
。您可以使用它来设置任务的行为。下面描述了这个enum
,并包括您拥有的不同选项。
public enum TaskCreationOptions
{
None = 0,
PreferFairness = 1,
LongRunning = 2,
AttachedToParent = 4,
DenyChildAttach = 8,
HideScheduler = 16,
RunContinuationsAsynchronously = 64,
}
在接下来的演示中,您将看到一个叫做TaskContinuationOptions
的重要enum
的使用,它也有助于设置任务行为。
使用基于任务的异步模式(TAP)
TAP 最早出现在 C# 4.0 中。是 C# 5.0 中出现的async/await
的基础。TAP 引入了Task
类及其通用变体,当异步代码块的返回值不是问题时,使用Task<TResult>. Task
,但是当您希望返回值进一步处理时,您应该使用Task<TResult>
通用版本.
。让我们用这个概念来实现使用Method1()
和Method2()
的 TAP。
演示 10
这是一个完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingTAP
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Task-based Asynchronous Pattern.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
Task taskForMethod1 = new Task(Method1);
taskForMethod1.Start();
Method2();
Console.ReadKey();
}
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
}
}
输出
这是一个可能的输出。
***Using Task-based Asynchronous Pattern.****
Inside Main().Thread ID:1
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() is completed.
Method1() has completed its job now.
您刚刚看到了一个基于任务的异步模式的示例演示。我不关心Method1
的返回值。但是假设你想看Method1
执行成功与否。为了简单起见,我使用一条string
消息来表示成功完成。这次你会看到任务的一个普通变体Task<string>
。对于 lambda 表达式爱好者,我在这个例子中用 lambda 表达式修改了Method1
。为了满足关键需求,我调整了返回类型。这次我添加了另一个叫做Method3()
的方法。出于比较的目的,最初这个方法将被注释掉,程序将被执行,输出将被分析。稍后我将取消对它的注释,并使用该方法创建一个任务层次结构。一旦完成,程序将被再次执行,你会注意到当Method1()
完成它的工作时Method3()
执行。为了更好的理解,我保留了评论。
现在来看一下接下来的演示。
演示 11
这是一个完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingTAPDemo2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Task-based Asynchronous Pattern.Using lambda expression into it.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
// Task taskForMethod1 = new Task(Method1);
// taskForMethod1.Start();
Task<string> taskForMethod1 = Method1();
// Wait for task to complete.It’ll be no more //asynchonous now.
// taskForMethod1.Wait();
// Continue the task
// The taskForMethod3 will continue once taskForMethod1 is // finished
// Task taskForMethod3 = taskForMethod1.ContinueWith(Method3, TaskContinuationOptions.OnlyOnRanToCompletion);
Method2();
Console.WriteLine("Task for Method1 was a : {0}", taskForMethod1.Result);
Console.ReadKey();
}
// Using lambda expression
private static Task<string> Method1()
{
return Task.Run(() =>
{
string result = "Failure";
try
{
Console.WriteLine("Inside Method1(),Task.id={0}", Task.CurrentId);
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
//Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
result = "Success";
}
catch (Exception ex)
{
Console.WriteLine("Exception caught:{0}", ex.Message);
}
return result;
}
);
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
private static void Method3(Task task)
{
Console.WriteLine("Method3 starts now.");
Console.WriteLine("Task.id is:{0} with Thread id is :{1} ", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(20);
Console.WriteLine("Method3 for Task.id {0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
}
}
输出
***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2() has started.
Inside Method2(),Thread id 1 .
Inside Method1(),Task.id=1
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() is completed.
Method1() has completed its job now.
Task for Method1 was a : Success
分析
你注意到我没有对taskForMethod1
使用Start()
方法吗?相反,我使用了Task
类中的Run()
方法来执行Method1()
。我为什么这么做?嗯,在Task
类里面,Run
是一个静态方法。Visual Studio 中的方法总结告诉我们关于这个Run
方法的如下内容:"Queues the specified work to run on the thread pool and returns a System.Threading.Tasks.Task
1 object that represents that work."`在编写的时候,这个方法有八个重载版本,如下。
public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<TResult> function);
public static Task<TResult> Run<TResult>(Func<TResult> function, CancellationToken cancellationToken);
public static Task Run(Func<Task> function);
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);
现在检查这个例子中的另一个要点。如果取消对下面一行的注释
// Task taskForMethod3 = taskForMethod1.ContinueWith(Method3, TaskContinuationOptions.OnlyOnRanToCompletion);
并再次运行该应用,您可以得到类似下面的输出。
***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2() has started.
Inside Method1(),Task.id=1
Method1() has started.
Inside Method1(),Thread id 4 .
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
Task for Method1 was a : Success
Method3 starts now.
Task.id is:2 with Thread id is :5
Method3 for Task.id 2 and Thread id 5 is completed.
ContinueWith()
方法有助于继续任务。你可能还会注意到下面的部分。
TaskContinuationOptions.OnlyOnRanToCompletion
它只是声明当taskForMethod1
完成它的工作时,任务将继续。同样,您可以通过使用TaskContinuationOptions
enum
来选择其他选项,其描述如下。
public enum TaskContinuationOptions
{
None = 0,
PreferFairness = 1,
LongRunning = 2,
AttachedToParent = 4,
DenyChildAttach = 8,
HideScheduler = 16,
LazyCancellation = 32,
RunContinuationsAsynchronously = 64,
NotOnRanToCompletion = 65536,
NotOnFaulted = 131072,
OnlyOnCanceled = 196608,
NotOnCanceled = 262144,
OnlyOnFaulted = 327680,
OnlyOnRanToCompletion = 393216,
ExecuteSynchronously = 524288
}
问答环节
6.11 我可以一次分配多项任务吗?
是的,你可以。在前面修改过的例子中,假设您有一个名为Method4
的方法,描述如下。
private static void Method4(Task task)
{
Console.WriteLine("Method4 starts now.");
Console.WriteLine("Task.id is:{0} with Thread id is :{1} ", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(10);
Console.WriteLine("Method4 for Task.id {0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
你可以写下面几行。
Task<string> taskForMethod1 = Method1();
Task taskForMethod3 = taskForMethod1.ContinueWith(Method3, TaskContinuationOptions.OnlyOnRanToCompletion);
taskForMethod3 = taskForMethod1.ContinueWith(Method4, TaskContinuationOptions.OnlyOnRanToCompletion);
这意味着一旦 taskForMethod1 完成了任务,您就会看到 taskForMethod3 的后续工作,它执行 Method3 和 Method4。
还需要注意的是,延续工作可以有延续工作。例如,让我们假设您想要以下内容。
-
一旦 taskForMethod1 完成,然后继续 taskForMethod3。
-
一旦 taskForMethod3 完成,就只能继续 taskForMethod4
你可以写类似下面的东西。
// Method1 starts
Task<string> taskForMethod1 = Method1();
// Task taskForMethod3 starts after Task taskForMethod1
Task taskForMethod3 = taskForMethod1.ContinueWith(Method3,
TaskContinuationOptions.OnlyOnRanToCompletion);
// Task taskForMethod4 starts after Task taskForMethod3
Task taskForMethod4 = taskForMethod3.ContinueWith(Method4, TaskContinuationOptions.OnlyOnRanToCompletion);
使用 async 和 await 关键字
使用async
和await
关键字使得点击模式非常灵活。本章使用了两种方法,其中第一种方法是长时间运行的方法,比第二种方法需要更多的时间来完成。我继续用同样的Method1()
和Method2()
方法.
进行案例研究
在接下来的演示中,我使用async
和 await 关键字。我从一个非 lambda 版本开始,但是在分析部分,我给出了 lambda 表达式*代码的变体。*首先我们来看Method1() again
。
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
当您使用 lambda 表达式和一个async/await
对时,您的代码可能如下所示。
// Using lambda expression
private static async Task ExecuteMethod1()
{
await Task.Run(() =>
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
);
}
你有没有注意到同步版本和异步版本非常相似?但是许多早期实现异步编程的解决方案并不是这样的。(我也相信它们是复杂的。)
等待是做什么的?当你分析代码时,你会发现一旦你得到一个await
,调用线程就会跳出这个方法,继续做别的事情。在接下来的演示中,Task.Run is used;
它导致异步调用在一个单独的线程上继续。然而,需要注意的是,这个并不意味着延续工作应该在一个新的线程上完成,因为你可能不总是关心不同的线程*;例如,当您的呼叫等待通过网络建立连接以下载某些内容时。*
在非 lambda 版本中,我使用下面的代码块。
private static async Task ExecuteTaskOne()
{
await Task.Run(Method1);
}
在Main()
内部,ExecuteTaskOne()
不调用Method1()
,而是异步执行Method1()
。我通过了Run
方法里面的Method1
。我在这里使用了最短的重载版本的Run
方法。由于Method1
匹配一个Action
委托的签名(记住这个委托封装了任何没有参数和void
返回类型的方法),您可以在Task
类的Run
方法中将它作为参数传递。
演示 12
这是完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingAsyncAwaitDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
/*
* This call is not awaited.So,the current method
* continues before the call is completed.
*/
ExecuteTaskOne();//Async call,this call is not awaited
Method2();
Console.ReadKey();
}
private static async Task ExecuteTaskOne()
{
await Task.Run(Method1);
}
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
}
}
输出
这是一个可能的输出。
***Exploring task-based asynchronous pattern(TAP) using async and await.****
Inside Main().Thread ID:1
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
分析
您可以看到Method1()
开始得更早,但是Method2()
的执行并没有因此而被阻塞。还要注意,Method2()
在一个主线程中运行,而Method1()
在一个不同的线程中执行。
和前面的例子一样,如果您喜欢 lambda 表达式,您可以替换下面的代码段:
private static async Task ExecuteTaskOne()
{
await Task.Run(Method1);
}
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
有了这个:
// Using lambda expression
private static async Task ExecuteMethod1()
{
await Task.Run(() =>
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
);
}
在演示 12 中,您可以直接调用ExecuteMethod1()
方法来获得类似的输出,而不是调用ExecuteTaskOne()
。
在前面的示例中,您会看到下面一行的警告消息:ExecuteMethod1();
,它陈述了以下内容。
Warning CS4014 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
如果你将鼠标悬停在这里,你会得到两个建议。第一个建议您应用丢弃,如下所示:
_ = ExecuteMethod1(); // applying discard
Note
从 C #7.0 开始支持丢弃。它们是应用中临时的、虚拟的和未使用的变量。因为这些变量可能不在分配的存储中,所以它们可以减少内存分配。这些变量可以增强可读性和可维护性。使用下划线(_)表示应用中被丢弃的变量。
下面使用第二个建议,并在该行之前插入await
。
await ExecuteMethod1();
在这种情况下,编译器会引发另一个错误。
Error CS4033 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
要消除这个错误,您需要使包含方法async
(即,从如下行开始:
static async Task Main(string[] args)
在应用了async/await
对之后,Main()
方法可能如下所示。
class Program
{
// static void Main(string[] args)
static async Task Main(string[] args)
{
Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
await ExecuteMethod1();
// remaining code
这种全面的讨论提醒您一起应用 async/await,并正确放置它们。
我用另一个演示来结束这一章,在这个演示中,我稍微修改了应用的调用序列。我用的Method3(),
和Method2()
差不多。该方法从ExecuteTaskOne()
调用,其结构如下。
private static async Task ExecuteTaskOne()
{
Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
int value=await Task.Run(Method1);
Console.WriteLine("Inside ExecuteTaskOne(), after await() call.");
// Method3 will be called if Method1 executes successfully
if (value != -1)
{
Method3();
}
}
这段代码简单地说,我想从Method1()
获取返回值,并基于该值决定是否调用Method3()
。这次,Method1()
的返回类型不是void
;相反,它返回一个int
(0 表示成功完成;否则为–1)。这个方法用如下所示的try-catch
块进行了重构。
private static int Method1()
{
int flag = 0;
try
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
catch (Exception e)
{
Console.WriteLine("Caught Exception {0}", e);
flag = -1;
}
return flag;
}
现在来看看下面的例子。
演示 13
这是完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingAsyncAwaitDemo3
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
Console.WriteLine("***This is a modified example with three methods.***");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
/*
* This call is not awaited.So,the current method
* continues before the call is completed.
*/
_=ExecuteTaskOne();//Async call,this call is not awaited
Method2();
Console.ReadKey();
}
private static async Task ExecuteTaskOne()
{
Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
int value=await Task.Run(Method1);
Console.WriteLine("Inside ExecuteTaskOne(), after await() call.");
// Method3 will be called if Method1 executes successfully
if (value != -1)
{
Method3();
}
}
private static int Method1()
{
int flag = 0;
try
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
//Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
catch (Exception e)
{
Console.WriteLine("Caught Exception {0}", e);
flag = -1;
}
return flag;
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
private static void Method3()
{
Console.WriteLine("Method3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method3() is completed.");
}
}
}
输出
***Exploring task-based asynchronous pattern(TAP) using async and await.****
***This is a modified example with three methods.***
Inside Main().Thread ID:1
Inside ExecuteTaskOne(), prior to await() call.
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
Inside ExecuteTaskOne(), after await() call.
Method3() has started.
Inside Method3(),Thread id 4 .
Method3() is completed.
分析
密切注意输出。你可以看到Method3()
需要等待Method1()
的完成,但是Method2()
可以在Method1()
结束执行之前完成它的执行。这里,如果Method1()
的返回值不等于–1,则Method3()
可以继续。这个场景类似于您在演示 11 中看到的ContinueWith()
方法。
最重要的是,再次注意下面的代码行。
int value=await Task.Run(Method1);
它只是将代码段分为两部分:对 await
的前调用和对 await
的后调用。这个语法类似于任何同步调用,但是通过使用await
(在一个async
方法中),您应用了一个暂停点并使用了异步编程的力量。
我用微软的一些有趣的笔记来结束这一章。当您进一步探索 async/await 关键字时,它们非常方便。记住以下几点。
-
await 运算符不能出现在 lock 语句的正文中。
-
您可能会在一个
async
方法的主体中看到多个await
。在一个async
方法中没有await
不会引发任何编译时错误。相反,您会得到一个警告,并且该方法以同步方式执行。注意下面类似的警告:Warning CS1998 This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread
.
最后的话
又一个大篇章!希望我能够揭开异步编程不同方法的神秘面纱。尽管在未来的开发中不再推荐使用IAsyncResult
模式和event-based asynchrony
,但我在本章中讨论了它们,以帮助您理解遗留代码,并向您展示异步程序的发展。毫无疑问,你将来会发现它很有用。
现在你已经准备好跳入异步编程的汪洋大海,探索剩下的边角案例了,没有自我实践是无法掌握的。所以,继续努力。
到目前为止,您已经看到了许多基于委托、事件和 lambda 表达式的应用!现在让我们进入最后一章,关于数据库编程。它有点不同,但非常有用和有趣。
摘要
本章讨论了以下关键问题。
-
什么是异步程序?它与同步程序有什么不同?
-
如何使用
Thread
类编写异步程序? -
什么是线程池?如何使用
ThreadPool
类编写异步程序? -
如何在异步程序中使用 lambda 表达式?
-
如何按照基于事件的异步模式编写异步程序?
-
什么是任务?如何在你的程序中使用
Task
类? -
如何按照基于任务的异步模式编写异步程序?
-
如何使用
async/await
关键字编写一个异步程序? -
你如何在你的应用中使用丢弃?
-
当你在程序中使用
async/await
关键字时,有哪些重要的限制?