1 Introduction to ParallelProgramming

Parallel programming has been supported in .NET since the start and it has gained a strong
footing since the introduction of the Task Parallel Library (TPL) from .NET framework 4.0
onward.
Multithreading is a subset of parallel programming and is one of the least understood
aspects of programming; it's one that many new developers struggle to understand. C# has
evolved significantly since its inception. It has very strong support, not only for
multithreading but also for asynchronous programming. Multithreading in C# goes way
back to C# version 1.0. C# is primarily synchronous, but with the strong async support that
has been added from C# 5.0 onward, it has become the first choice for application
programmers. Whereas multithreading only deals with how to parallelize within processes,
parallel programming also deals with inter-process communication scenarios.
Prior to the introduction of the TPL, we relied
on Thread , BackgroundWorker , and ThreadPool to provide us with multithreading
capabilities. At the time of C# v1.0, it relied on threads to split up work and free up the user
interface (UI), thereby allowing the user to develop responsive applications. This model is
now referred to as classic threading. With time, this model made way for another model of
programming, called TPL, which relies on tasks and still uses threads internally.
In this chapter, we will learn about various concepts that will help you learn about writing
multithreaded code from scratch.

We will cover the following topics:
Basic concepts of multi-core computing, starting with an introduction to the
concepts and processes related to the operating system (OS)
Threads and the difference between multithreading and multitasking
Advantages and disadvantages of writing parallel code and scenarios in which
parallel programming is useful

并行编程自.NET推出以来就已经得到支持,并且自从.NET框架4.0引入任务并行库(TPL)以来,它已经获得了坚实的基础。
多线程是并行编程的一个子集,也是编程中理解最少的方面之一;这是许多新开发人员难以理解的一个方面。C#自诞生以来已经发生了巨大的变化。它不仅对多线程有非常强大的支持,还对异步编程有非常强大的支持。C#中的多线程可以追溯到C# 1.0版本。C#主要是同步的,但是从C# 5.0开始增加了强大的异步支持,它已经成为应用程序程序员的首选。虽然多线程只涉及如何在进程内并行化,但并行编程还涉及进程间通信场景。
在引入TPL之前,我们依赖于Thread、BackgroundWorker和ThreadPool来提供多线程功能。在C# v1.0时代,它依靠线程来分配工作并释放用户界面(UI),从而允许用户开发响应式应用程序。现在这个模型被称为经典线程模型。随着时间的推移,这个模型为另一个编程模型让路,这个模型称为TPL,它依赖于任务,并在内部仍然使用线程。
在这一章中,我们将学习各种概念,这将帮助你学习从头开始编写多线程代码。

我们将讨论以下主题:
多核计算的基本概念,从操作系统(OS)相关的概念和过程开始介绍
线程以及多线程与多任务之间的区别
编写并行代码的优势和劣势,以及并行编程有用的场景

Technical requirements
All the examples demonstrated in this book have been created in Visual Studio 2019 using
C# 8. All the source code can be found on GitHub at https: /​/​github. ​com/
PacktPublishing/​Hands-​On-​Parallel-​Programming-​with-​C-​8-​and-​. ​NET-​Core-​3/​tree/
master/​Chapter01 

技术要求
本书中展示的所有示例都是在Visual Studio 2019中使用C# 8创建的。所有源代码都可以在GitHub上找到,网址为:https://github.com/PacktPublishing/Hands-On-Parallel-Programming-with-C-8-and-.NET-Core-3/tree/master/Chapter01

Preparing for multi-core computing
In this section, we will introduce the core concepts of the OS, starting with the process,
which is where threads live and run. Then, we will consider how multitasking evolved
with the introduction of hardware capabilities, which make parallel programming possible.
After that, we will try to understand the different ways of creating a thread with code.

准备多核计算
在这一部分,我们将介绍操作系统(OS)的核心概念,从进程开始讲起,进程是线程驻留和运行的地方。然后,我们将考虑随着硬件能力引入,多任务是如何演变的,这些硬件能力使得并行编程成为可能。之后,我们将尝试理解用代码创建线程的不同方式。

Processes
In layman's terms, the word process refers to a program in execution. In terms of the OS,
however, a process is an address space in the memory. Every application, whether it is a
Windows, web, or mobile application, needs processes to run. Processes provide security
for programs against other programs that run on the same system so that data that's
allocated to one cannot be accidentally accessed by another. They also provide isolation so
that programs can be started and stopped independently of each other and independently
of the underlying OS.

进程
用外行的话来说,进程这个词指的是正在执行的程序。然而,在操作系统方面,进程是内存中的一个地址空间。每个应用程序,无论是Windows、Web还是移动应用程序,都需要进程来运行。进程为程序提供了安全性,防止同一系统上运行的其他程序意外访问分配给一个程序的数据。它们还提供了隔离性,使得程序可以独立于彼此以及独立于底层操作系统启动和停止。

Some more information about the OS
The performance of applications largely depends on the quality and configuration of the
hardware. This includes the following:
CPU speed
Amount of RAM
Hard disk speed (5400/7200 RPM)
Disk type, that is, HDD or SSD
Over the last few decades, we have seen huge jumps in hardware technology. For example,
microprocessors used to have a single core, which is a chip with one central processing
unit (CPU). By the turn of the century, we saw the advent of multi-core processors, which
are chips with two or more processors, each with its own cache.

关于操作系统的更多信息
应用程序的性能在很大程度上取决于硬件的质量和配置。这包括以下方面:
CPU速度
RAM的数量
硬盘速度(5400/7200 RPM)
磁盘类型,即HDD或SSD
在过去的几十年里,我们见证了硬件技术的巨大飞跃。例如,微处理器过去只有一个核心,也就是一个单核心处理单元(CPU)的芯片。到了世纪之交,我们看到了多核处理器的出现,这些是带有两个或更多处理器的芯片,每个处理器都有自己的缓存。

Multitasking
Multitasking refers to the ability of a computer system to run more than one process
(application) at a time. The number of processes that can be run by a system is directly
proportional to the number of cores in that system. Therefore, a single-core processor can
only run one task at a time, a dual-core processor can run two tasks at a time, and a quad-
core processor can run four tasks at a time. If we add the concept of CPU scheduling to this,
we can see that the CPU runs more applications at a time by scheduling or switching them
based on CPU scheduling algorithms.

多任务
多任务指的是计算机系统同时运行多个进程(应用程序)的能力。系统可以运行的进程数量与该系统中的核心数量成正比。因此,单核处理器一次只能运行一个任务,双核处理器可以同时运行两个任务,四核处理器可以同时运行四个任务。如果我们将CPU调度的概念加入其中,我们可以看出,CPU通过基于CPU调度算法对它们进行调度或切换,从而同时运行更多的应用程序。

Hyper-threading
Hyper-threading (HT) technology is a proprietary technology that was developed by Intel
that improves the parallelization of computations that are performed on x86 processors. It
was first introduced in Xeon server processors in 2002. HT-enabled single-processor chips
run with two virtual (logical) cores and are capable of executing two tasks at a time. The
following diagram shows the difference between single- and multi-core chips:

The following are a few examples of processor configurations and the number of tasks that
they can perform:
A single processor with a single-core chip: One task at a time
A single processor with an HT-enabled single-core chip: Two tasks at a time
A single processor with a dual-core chip: Two tasks at a time
A single processor with an HT-enabled dual-core chip: Four tasks at a time
A single processor with a quad-core chip: Four tasks at a time
A single processor with an HT-enabled quad-core chip: Eight tasks at a time
The following is a screenshot of a CPU resource monitor for an HT-enabled quad-core
processor system. On the right-hand side, you can see that there are eight available CPUs:

You might be wondering how much you can improve the performance of your computer
simply by moving from a single-core to a multi-core processor. At the time of writing, most
of the fastest supercomputers are built on the Multiple Instruction, Multiple Data
(MIMD) architecture, which was one of the classifications of computer architecture
proposed by Michael J. Flynn in 1966.
Let's try to understand this classification.

超线程
超线程(HT)技术是英特尔开发的一种专有技术,它改善了在x86处理器上执行的计算的并行化。这项技术最初于2002年在Xeon服务器处理器中引入。启用HT的单处理器芯片以两个虚拟(逻辑)核心运行,并能够同时执行两个任务。下图展示了单核和多核芯片之间的区别:

以下是一些处理器配置的例子以及它们能够执行的任务数量:
单处理器单核心芯片:一次一个任务
单处理器启用HT的单核心芯片:一次两个任务
单处理器双核心芯片:一次两个任务
单处理器启用HT的双核心芯片:一次四个任务
单处理器四核心芯片:一次四个任务
单处理器启用HT的四核心芯片:一次八个任务
下图是一个启用HT的四核心处理器系统的CPU资源监视器的屏幕截图。在右侧,你可以看到有八个可用的CPU:

你可能会好奇,仅仅通过从单核升级到多核处理器,你能提高多少计算机的性能。在撰写本文时,大多数最快的超级计算机都是基于多重指令、多重数据(MIMD)架构构建的,这是1966年由迈克尔·J·弗林提出的计算机架构分类之一。
让我们来理解这个分类。

Flynn's taxonomy
Flynn classified computer architectures into four categories based on the number of
concurrent instruction (or control) streams and data streams:
Single Instruction, Single Data (SISD): In this model, there is a single control
unit and a single instruction stream. These systems can only execute one
instruction at a time without any parallel processing. All single-core processor
machines are based on the SISD architecture.
Single Instruction, Multiple Data (SIMD): In this model, we have a single
instruction stream and multiple data streams. The same instruction stream is
applied to multiple data streams in parallel. This is handy in speculative-
approach scenarios where we have multiple algorithms for data and we don't
know which one will be faster. It provides the same input to all the algorithms
and runs them in parallel on multiple processors.
Multiple Instructions, Single Data (MISD): In this model, multiple instructions
operate on one data stream. Therefore, multiple operations can be applied in
parallel on the same data source. This is generally used for fault tolerance and in
space shuttle flight control computers.
Multiple Instructions, Multiple Data (MIMD): In this model, as the name
suggests, we have multiple instruction streams and multiple data streams. Due to
this, we can achieve true parallelism, where each processor can run different
instructions on different data streams. Nowadays, this architecture is used by
most computer systems.
Now that we've covered the basics, let's move our discussion to threads.

弗林分类法
弗林根据并发指令(或控制)流和数据流的数量将计算机架构分为四类:
单指令单数据(SISD):在这种模型中,有一个单一的控制单元和单一的指令流。这些系统一次只能执行一个指令,没有任何并行处理。所有单核处理器机器都基于SISD架构。
单指令多数据(SIMD):在这种模型中,我们有一个单一的指令流和多个数据流。相同的指令流被并行应用到多个数据流上。这在推测性方法的场景中很有用,我们有多个算法用于数据,我们不知道哪个会更快。它为所有算法提供相同的输入,并在多个处理器上并行运行它们。
多指令单数据(MISD):在这种模型中,多个指令操作一个数据流。因此,可以对同一数据源并行应用多个操作。这通常用于容错以及航天飞机飞行控制计算机中。
多指令多数据(MIMD):在这种模型中,顾名思义,我们有多条指令流和多个数据流。因此,我们可以实现真正的并行性,每个处理器可以在不同的数据流上运行不同的指令。如今,大多数计算机系统都使用这种架构。
现在我们已经介绍了基础知识,让我们继续讨论线程。

Threads
A thread is a unit of execution inside a process. At any point, a program may consist of one
or more threads for better performance. GUI-based Windows applications, such as
legacy Windows Forms (WinForms) or Windows Presentation Foundation (WPF), have a
dedicated thread for managing the UI and handling user actions. This thread is also called
the UI thread, or the foreground thread. It owns all the controls that are created as part of
the UI.

线程
线程是进程内部的一个执行单元。在任何时刻,一个程序可能由一个或多个线程组成,以获得更好的性能。基于GUI的Windows应用程序,如传统的Windows窗体(WinForms)或Windows呈现基础(WPF),都有一个专门的线程来管理用户界面和处理用户操作。这个线程也被称为UI线程,或者前台线程。它拥有所有作为用户界面一部分创建的控件。

Types of threads
There are two different types of managed threads, that is, a foreground thread and
a background thread. The difference between these is as follows:
Foreground threads: These have a direct impact on an application's lifetime. The
application keeps running until there is a foreground thread.
Background threads: These have no impact on the application's lifetime. When
the application exits, all the background threads are killed.
An application may comprise any number of foreground or background threads. While
active, a foreground thread keeps the application running; that is, the application's lifetime
depends on the foreground thread. The application stops completely when the last
foreground thread is stopped or aborted. When the application exits, the system stops all
the background threads.

线程类型
有两种不同类型的托管线程,即前台线程和后台线程。它们之间的差异如下:
前台线程:这些线程直接影响应用程序的生命周期。只要存在前台线程,应用程序就会继续运行。
后台线程:这些线程不影响应用程序的生命周期。当应用程序退出时,所有的后台线程都会被终止。
一个应用程序可以包含任意数量的前台线程或后台线程。在活动状态下,前台线程会保持应用程序运行;也就是说,应用程序的生命周期依赖于前台线程。当最后一个前台线程停止或异常终止时,应用程序完全停止。当应用程序退出时,系统会停止所有后台线程。

Apartment state
Another important aspect of threads to understand is the apartment state. This is the area
inside a thread where Component Object Model (COM) objects reside.
COM is an object-oriented system for creating binary software that the
user can interact with and is distributed and cross-platform. COM has
been used to create Microsoft OLE and ActiveX technologies.
As you may be aware, all Windows forms controls are wrapped over COM objects.
Whenever you create a .NET WinForms application, you are actually hosting COM
components. A thread apartment is a distinct area inside the application process where
COM objects are created. The following diagram demonstrates the relationship between the
thread apartment and COM objects:

As you can see from the preceding diagram, every thread has thread apartments where
COM objects reside.
A thread can belong to one of two apartment states:
Single-Threaded Apartment (STA): The underlying COM object can be accessed
via a single thread only
Multi-Threaded Apartment (MTA): The underlying COM object can be accessed
via multiple threads at a time
The following list highlights some important points regarding thread apartment states:
Processes can have multiple threads, either foreground or background.
Each thread can have one apartment, either STA or MTA.
Every apartment has a concurrency model, either single-threaded or
multithreaded. We can change the thread state programmatically as well.
An application process may have more than one STA, but a maximum of one
MTA.
An example of an STA application is a Windows application, and an example of
an MTA application is a web application.
COM objects are created in apartments. One COM object can only lie in one
thread apartment, and apartments cannot be shared.

An application can be forced to start in STA mode by using the STAThread attribute over
the main methods. The following is an example of the Main method of a legacy WinForm:
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application. EnableVisualStyles() ;
Application. SetCompatibleTextRenderingDefault(false) ;
Application. Run(new Form1() ) ;
}
}
The STAThread attribute is also present in WPF but is hidden from users. The following is
the code for the compiled App. g. cs class, which can be found in the obj/Debug directory
of your WPF project after compilation:
/// <summary>
/// App
/// </summary>
public partial class App : System. Windows. Application {
/// <summary>
/// InitializeComponent
/// </summary>
[System. Diagnostics. DebuggerNonUserCodeAttribute() ]
[System. CodeDom. Compiler. GeneratedCodeAttribute(
"PresentationBuildTasks", "4. 0. 0. 0") ]
public void InitializeComponent() {
#line 5 ". . \. . \App. xaml"
this. StartupUri = new System. Uri("MainWindow. xaml",
System. UriKind. Relative) ;
#line default
#line hidden
}
/// <summary>
/// Application Entry Point.
/// </summary>
[System. STAThreadAttribute() ]
[System. Diagnostics. DebuggerNonUserCodeAttribute() ]
[System. CodeDom. Compiler. GeneratedCodeAttribute(
"PresentationBuildTasks", "4. 0. 0. 0") ]
public static void Main() {
WpfApp1. App app = new WpfApp1. App() ;
app. InitializeComponent() ;
app. Run() ;
}
}
As you can see, the Main method is decorated with the STAThread attribute.

线程公寓状态
理解线程的另一个重要方面是线程公寓状态。这是线程内部组件对象模型(COM)对象所在的区域。
COM是一个面向对象的系统,用于创建用户可以与之交互的二进制软件,它是分布式和跨平台的。COM已被用来创建Microsoft OLE和ActiveX技术。
您可能已经知道,所有Windows窗体控件都是基于COM对象包装的。每当您创建一个.NET WinForms应用程序时,实际上您是在托管COM组件。线程公寓是应用程序进程内的一个独特区域,COM对象在此创建。下面的图表展示了线程公寓与COM对象之间的关系:

从上面的图表可以看出,每个线程都有线程公寓,COM对象就驻留在其中。
线程可以属于两种公寓状态之一:
单线程公寓(STA):底层COM对象只能通过单个线程访问
多线程公寓(MTA):底层COM对象可以通过多个线程同时访问
以下列表突出了关于线程公寓状态的一些要点:
进程可以有多个线程,无论是前台还是后台。
每个线程可以有一个公寓,要么是STA,要么是MTA。
每个公寓都有一个并发模型,要么是单线程的,要么是多线程的。我们也可以以编程方式更改线程状态。
一个应用程序进程可以有多个STA,但最多只能有一个MTA。
STA应用程序的一个例子是Windows应用程序,MTA应用程序的一个例子是web应用程序。
COM对象是在公寓中创建的。一个COM对象只能位于一个线程公寓中,公寓不能共享。

通过在主方法上使用STAThread属性,可以强制应用程序以STA模式启动。以下是传统WinForm的主方法示例:
```csharp
static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}
```
WPF中的STAThread属性也存在,但对用户隐藏。以下是编译后在WPF项目的obj/Debug目录中找到的App.g.cs类的代码:
```csharp
/// <summary>
/// App
/// </summary>
public partial class App : System.Windows.Application {
    /// <summary>
    /// InitializeComponent
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute(
        "PresentationBuildTasks", "4.0.0.0")]
    public void InitializeComponent() {
        #line 5 "..\..\App.xaml"
        this.StartupUri = new System.Uri("MainWindow.xaml",
            System.UriKind.Relative);
        #line default
        #line hidden
    }
    /// <summary>
    /// 应用程序入口点。
    /// </summary>
    [System.STAThreadAttribute()]
    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute(
        "PresentationBuildTasks", "4.0.0.0")]
    public static void Main() {
        WpfApp1.App app = new WpfApp1.App();
        app.InitializeComponent();
        app.Run();
    }
}
```
如您所见,Main方法用STAThread属性进行了修饰。

Multithreading
Parallel execution of code in .NET is achieved through multithreading. A process (or
application) can utilize any number of threads, depending on its hardware capabilities.
Every application, including console, legacy WinForms, WPF, and even web applications, is
started by a single thread by default. We can easily achieve multithreading by creating
more threads programmatically as and when they are required.
Multithreading typically functions using a scheduling component known as a thread
scheduler, which keeps track of when a thread should run out of active threads inside a
process. Every thread that's created is assigned a System. Threading. ThreadPriority ,
which can have one of the following valid values. Normal is the default priority that's
assigned to any thread:
Highest
AboveNormal
Normal
BelowNormal
Lowest
Every thread that runs inside a process is assigned a time slice by the OS based on the
thread priority scheduling algorithm. Every OS can have a different scheduling algorithm
for running threads, so the order of execution may vary in different operating systems. This
makes it more difficult to troubleshoot threading errors. The most common scheduling
algorithm is as follows:
Find the threads with the highest priority and schedule them to run. 1.
If there is more than one thread with the highest priority, each thread is assigned 2.
a fixed time slices in which they can execute.

Once the highest-priority threads finish executing, the lower-priority threads 3.
start to be allocated to time slices in which it can begin executing.
If a new highest-priority thread is created, low-priority threads are pushed back 4.
again.
Time slicing refers to switching the execution between the active threads. It can vary,
depending on the hardware configuration. A single-core processor machine can only run
one thread at a time, so the thread scheduler carries out the time slicing. The time slice
largely depends on the clock speed of the CPU, but there still aren't many performance
gains that can be achieved via multithreading in such systems. Moreover, context switching
comes with performance overheads. If the work that's allocated to a thread spans multiple
time slices, then the thread needs to be switched in and out of memory. Every time it
switches out, it needs to bundle and save its state (data) and reload it when it switches back
in.
Concurrency is a concept that's primarily used in the context of multi-core processors. A
multi-core processor has a higher number of CPUs available, as we discussed previously,
and therefore different threads can be run simultaneously on different CPUs. A higher
number of processors means a higher degree of concurrency.
There are multiple ways that threads can be created in programs. These include the
following:
The Thread class
The ThreadPool Class
The BackgroundWorker Class
Asynchronous delegates
TPL
We will cover asynchronous delegates and TPL in depth during the course of this book, but
in this chapter, we will provide an explanation of the remaining three methods.

多线程
在.NET中,代码的并行执行是通过多线程实现的。一个进程(或应用程序)可以根据其硬件能力使用任意数量的线程。每个应用程序,包括控制台、传统的WinForms、WPF,甚至是web应用程序,默认都是以单个线程启动的。我们可以通过编程方式在需要时创建更多线程来实现多线程。
多线程通常使用称为线程调度器的调度组件来工作,该组件负责跟踪进程内活动线程何时应该运行。每个创建的线程都被分配了一个System.Threading.ThreadPriority,它可以具有以下有效值之一。Normal是分配给任何线程的默认优先级:
Highest
AboveNormal
Normal
BelowNormal
Lowest
进程内的每个线程都由操作系统根据线程优先级调度算法分配一个时间片。每个操作系统都可能有不同的线程运行调度算法,因此在不同的操作系统中执行顺序可能会有所不同。这使得排查线程错误更加困难。最常见的调度算法如下:
找到具有最高优先级的线程并安排它们运行。1.
如果有多个具有最高优先级的线程,每个线程被分配2.
一个固定的时间片,在这段时间内它们可以执行。

一旦最高优先级的线程完成执行,较低优先级的线程3.
开始被分配到时间片中,它们可以开始执行。
如果创建了一个新的最高优先级的线程,低优先级的线程4.
再次被推后。
时间切片是指在活动线程之间切换执行的过程。这可能会有所不同,取决于硬件配置。单核处理器的机器一次只能运行一个线程,因此线程调度器执行时间切片。时间片的大小主要取决于CPU的时钟速度,但在此类系统中通过多线程获得的性能提升仍然不多。此外,上下文切换还伴随着性能开销。如果分配给线程的工作跨越多个时间片,那么线程需要在内存中进行切换。每次切换出时,它需要捆绑并保存其状态(数据),并在切换回来时重新加载。
并发主要是在多核处理器的背景下使用的概念。如前所述,多核处理器有更多可用的CPU,因此不同的线程可以同时在不同的CPU上运行。处理器数量越多,并发程度越高。
在程序中创建线程有多种方法。这些方法包括:
Thread类
ThreadPool类
BackgroundWorker类
异步委托
TPL
我们将在本书的课程中深入讲解异步委托和TPL,但在这一章中,我们将解释剩下的三种方法。

Thread class
The simplest and easiest way of creating threads is via the Thread class, which is defined in
the System. Threading namespace. This approach has been used since the arrival of .NET
version 1.0 and it works with .NET core as well. To create a thread, we need to pass a
method that the thread needs to execute. The method can either be parameter-less or
parameterized. There are two delegates that are provided by the framework to wrap these
functions:
System. Threading. ThreadStart
System. Threading. ParameterizedThreadStart
We will learn both of these through examples. Before showing you how to create a thread, I
will try to explain how a synchronous program works. Later on, we will introduce
multithreading so that we understand the asynchronous way of execution. An example of
how to create a thread is as follows:
using System;
namespace Ch01
{
class _1Synchronous
{
static void Main(string[] args)
{
Console. WriteLine("Start Execution! ! ! ") ;
PrintNumber10Times() ;
Console. WriteLine("Finish Execution") ;
Console. ReadLine() ;
}
private static void PrintNumber10Times()
{
for (int i = 0; i < 10; i++)
{
Console. Write(1) ;
}
Console. WriteLine() ;
}
}
}

In the preceding code, everything runs in the main thread. We have called the
PrintNumber10Times method from within the Main method, and since the Main method
is invoked by the main GUI thread, the code runs synchronously. This can cause
unresponsive behavior if the code runs for a long time as the main thread will be busy
during execution.
The output of the code is as follows:
In the following timeline, we can see that everything happens in the Main Thread:
The preceding diagram shows sequential code execution on the Main thread.
Now, we can make the program multithreaded by creating a thread to do the printing. The
main thread prints the statements that are written in the Main method:
using System;
namespace Ch01
{
class _2ThreadStart
{
static void Main(string[] args)
{
Console. WriteLine("Start Execution! ! ! ") ;
//Using Thread without parameter
CreateThreadUsingThreadClassWithoutParameter() ;
Console. WriteLine("Finish Execution") ;
Console. ReadLine() ;
}
private static void CreateThreadUsingThreadClassWithoutParameter()
{
System. Threading. Thread thread;
thread = new System. Threading. Thread(new
System. Threading. ThreadStart(PrintNumber10Times) ) ;

thread. Start() ;
}
private static void PrintNumber10Times()
{
for (int i = 0; i < 10; i++)
{
Console. Write(1) ;
}
Console. WriteLine() ;
}
}
}
In the preceding code, we have delegated the execution of PrintNumber10Times() to a
new thread that has been created via the Thread class. The Console. WriteLine
statements in the Main method are still executed via the main thread,
but PrintNumber10Times is not called via the child thread.
The output of the code is as follows:
The timeline for this process is as follows. You can see that Console. WriteLine executes
on the Main Thread and that the loop executes on the Child Thread:
The preceding diagram is an example of multithreaded execution.

If we compare the outputs, we can see that the program finishes everything in the main
thread and then starts to print the number 10 times. The operations in this example are very
small and thus work in a deterministic manner. If there are time-consuming statements in
the main thread before Finish Execution is printed, however, the results can vary. We will
look at how multithreading works and how it is related to CPU speed and numbers later on
in this chapter in order to fully understand this idea.
Here is another example to show you how to pass data to the thread using
the System. Threading. ParameterizedThreadStart delegate:
using System;
namespace Ch01
{
class _3ParameterizedThreadStart
{
static void Main(string[] args)
{
Console. WriteLine("Start Execution! ! ! ") ;
//Using Thread with parameter
CreateThreadUsingThreadClassWithParameter() ;
Console. WriteLine("Finish Execution") ;
Console. ReadLine() ;
}
private static void CreateThreadUsingThreadClassWithParameter()
{
System. Threading. Thread thread;
thread = new System. Threading. Thread(new
System. Threading. ParameterizedThreadStart(PrintNumberNTimes) ) ;
thread. Start(10) ;
}
private static void PrintNumberNTimes(object times)
{
int n = Convert. ToInt32(times) ;
for (int i = 0; i < n; i++)
{
Console. Write(1) ;
}
Console. WriteLine() ;
}
}
}

The output of the preceding code is as follows:
Using the Thread class has some advantages and disadvantages. Let's try to understand
them.

线程类
创建线程最简单且最简单的方法是通过定义在 System.Threading 命名空间中的 Thread 类。这种方法自 .NET 1.0 版本以来一直在使用,并且也适用于 .NET Core。要创建一个线程,我们需要传递一个线程需要执行的方法。该方法可以是无参数的,也可以是有参数的。框架提供了两个委托来包装这些函数:
System.Threading.ThreadStart
System.Threading.ParameterizedThreadStart
我们将通过示例学习这两种方法。在向您展示如何创建线程之前,我将尝试解释同步程序是如何工作的。稍后,我们将引入多线程,以便我们了解异步执行方式。创建线程的示例如下:
using System;
namespace Ch01
{
class _1Synchronous
{
static void Main(string[] args)
{
Console.WriteLine("开始执行! ! ! ");
PrintNumber10Times();
Console.WriteLine("结束执行");
Console.ReadLine();
}
private static void PrintNumber10Times()
{
for (int i = 0; i < 10; i++)
{
Console.Write(1);
}
Console.WriteLine();
}
}
}

在之前的代码中,所有操作都在主线程中运行。我们在Main方法中调用了PrintNumber10Times方法,由于Main方法是被主GUI线程调用的,因此代码同步运行。如果代码运行时间较长,这可能导致无响应行为,因为主线程在执行期间会一直忙碌。
代码的输出如下:


在以下时间线中,我们可以看到一切都发生在主线程中:


前面的图示显示了主线程上的序列代码执行。
现在,我们可以通过创建一个新的线程来进行打印,使程序变为多线程。主线程打印在Main方法中写的语句:
```csharp
using System;
namespace Ch01
{
class _2ThreadStart
{
static void Main(string[] args)
{
Console. WriteLine("开始执行!!!") ;
//使用无参数的Thread
CreateThreadUsingThreadClassWithoutParameter() ;
Console. WriteLine("完成执行") ;
Console. ReadLine() ;
}
private static void CreateThreadUsingThreadClassWithoutParameter()
{
System. Threading. Thread thread;
thread = new System. Threading. Thread(new
System. Threading. ThreadStart(PrintNumber10Times) ) ;

thread. Start() ;
}
private static void PrintNumber10Times()
{
for (int i = 0; i < 10; i++)
{
Console. Write(1) ;
}
Console. WriteLine() ;
}
}
}
```
在上述代码中,我们通过Thread类将PrintNumber10Times()的执行委托给了一个新创建的线程。Main方法中的Console. WriteLine语句仍然通过主线程执行,但PrintNumber10Times不是通过子线程调用的。
代码的输出如下:
此过程的时间线如下。您可以看到Console. WriteLine在主线程上执行,循环在子线程上执行:


前面的图示是多线程执行的一个例子。

如果我们比较输出,我们可以看到程序完成了主线程中的所有操作,然后开始打印数字10次。此示例中的操作非常小,因此以确定性方式工作。然而,如果在“完成执行”之前打印的主线程中有耗时的语句,结果可能会有所不同。我们将在本章后面的内容中进一步了解多线程的工作方式以及它与CPU速度和数量的关系,以便完全理解这个概念。
下面是另一个示例,演示了如何使用System. Threading. ParameterizedThreadStart委托将数据传递给线程:
```csharp
using System;
namespace Ch01
{
class _3ParameterizedThreadStart
{
static void Main(string[] args)
{
Console. WriteLine("开始执行!!!") ;
//使用带参数的Thread
CreateThreadUsingThreadClassWithParameter() ;
Console. WriteLine("完成执行") ;
Console. ReadLine() ;
}
private static void CreateThreadUsingThreadClassWithParameter()
{
System. Threading. Thread thread;
thread = new System. Threading. Thread(new
System. Threading. ParameterizedThreadStart(PrintNumberNTimes) ) ;
thread. Start(10) ;
}
private static void PrintNumberNTimes(object times)
{
int n = Convert. ToInt32(times) ;
for (int i = 0; i < n; i++)
{
Console. Write(1) ;
}
Console. WriteLine() ;
}
}
}
```
上述代码的输出如下:
使用Thread类有一些优点和缺点。让我们来了解一下它们。

Advantages and disadvantages of threads
The Thread class has the following advantages:
Threads can be utilized to free up the main thread.
Threads can be used to break up a task into smaller units that can be executed
concurrently.
The Thread class has the following disadvantages:
With more threads, the code becomes difficult to debug and maintain.
Thread creation puts a load on the system in terms of memory and CPU
resources.
We need to do exception handling inside the worker method as any unhandled
exceptions can result in the program crashing.

线程的优点和缺点

线程类具有以下优点:
- 线程可以用来释放主线程。
- 线程可用于将任务分解为更小的单元,这些单元可以并发执行。

线程类具有以下缺点:
- 随着线程数量的增加,代码变得难以调试和维护。
- 创建线程会在内存和CPU资源方面给系统带来负担。
- 我们需要在工作方法内部进行异常处理,因为任何未处理的异常都可能导致程序崩溃。

The ThreadPool class
Thread creation is an expensive operation in terms of both memory and CPU resources. On
average, every thread consumes around 1 MB of memory and a few hundred microseconds
of CPU time. Application performance is a relative concept, so it will not necessarily
improve by creating a large number of threads. Conversely, creating a large number of
threads can sometimes decrease application performance drastically. We should always
aim to create an optimal number of threads, depending on the target system's CPU load,
that is, other programs running on the system. This is because every program gets a time
slice by the CPU, which is then distributed among the threads inside the application. If you
create too many threads, they may not be able to do any constructive work before being
swapped out of memory to give the time slice other similar-priority threads.

Finding the optimal number of threads can be tricky as it can vary from one system to
another, depending on the configuration and the number of applications that are running
concurrently on the system. What may be an optimal number on one system may cause a
negative impact on another. Rather than finding the optimal number of threads ourselves,
we can leave it to the Common Language Runtime (CLR). The CLR has an algorithm to
determine the optimal number based on the CPU load at any point in time. It maintains a
pool of threads, known as the ThreadPool . The ThreadPool resides in a process and each
application has its own pool of threads. The advantage of thread pooling is that it maintains
an optimal number of threads and assigns them to a task. When the work is finished, the
threads are returned to the pool, where they can be assigned to the next work item, thereby
preventing the cost of creating and destroying threads.
The following is a list of the optimal number of threads that can be created in different
frameworks inside ThreadPool :
25 per core in .NET Framework 2.0
250 per core in .NET Framework 3.5
1,023 in .NET Framework 4.0 in a 32-bit environment
32,768 in .NET Framework 4.0 onward, as well as in .NET core in a 64-bit
environment
While working with an investment bank, we came across a scenario where
a trade process was taking almost 1,800 seconds to book close to 1,000
trades synchronously. After trying various optimal numbers, we finally
switched to ThreadPool and made the process multithreaded. With .NET
Framework version 2.0, the application finished in close to 72 seconds.
With version 3.5, the same application finished in just a few seconds. This
is a typical example of using the framework that's been provided rather
than reinventing the wheel. You can get much-needed performance gains
just by updating the framework.
We can create a thread via ThreadPool by calling ThreadPool. QueueUserWorkItem , as
shown in the following example.

Here is the method that we want to call in parallel:
private static void PrintNumber10Times(object state)
{
for (int i = 0; i < 10; i++)
{
Console. Write(1) ;
}
Console. WriteLine() ;
}
Here is how we can create a thread using ThreadPool. QueueUserWorkItem while
passing the WaitCallback delegate:
private static void CreateThreadUsingThreadPool()
{
ThreadPool. QueueUserWorkItem(new WaitCallback(PrintNumber10Times) ) ;
}
Here is a call from the Main method:
using System;
using System. Threading;
namespace Ch01
{
class _4ThreadPool
{
static void Main(string[] args)
{
Console. WriteLine("Start Execution! ! ! ") ;
CreateThreadUsingThreadPool() ;
Console. WriteLine("Finish Execution") ;
Console. ReadLine() ;
}
}
}
The output of the preceding code is as follows:

Every thread pool maintains a minimum and a maximum number of threads. These values
can be modified by calling the following static methods:
ThreadPool. SetMinThreads
ThreadPool. SetMaxThreads
A thread is created via System. Threading . The Thread class doesn't
belong to the ThreadPool .
Let's take a look at the advantages and disadvantages associated with using the
ThreadPool class and when to avoid using it.

线程池类

线程创建在内存和CPU资源方面都是一种昂贵的操作。平均而言,每个线程大约消耗1MB的内存和几百微秒的CPU时间。应用程序性能是一个相对概念,因此通过创建大量线程并不一定会提高性能。相反,创建大量线程有时会大幅降低应用程序性能。我们应该始终根据目标系统的CPU负载(即系统上运行的其他程序)来创建最优数量的线程。这是因为每个程序都会获得CPU的时间片,然后在应用程序内的线程之间进行分配。如果你创建了太多线程,它们可能无法在被内存换出以给其他相同优先级的线程提供时间片之前做任何有建设性的工作。

找到最优线程数可能很棘手,因为它可能因系统而异,这取决于配置以及同时在系统上运行的应用程序数量。在一个系统上的最优数量可能会对另一个系统产生负面影响。我们可以将确定最优线程数的任务留给公共语言运行时(CLR)。CLR有一个算法可以根据任何时候的CPU负载来确定最优数量。它维护一个线程池,称为ThreadPool。ThreadPool驻留在一个进程中,每个应用程序都有自己的线程池。线程池的优势在于它维护了一个最优数量的线程并将它们分配给任务。当工作完成时,线程返回到池中,可以被分配给下一个工作项,从而避免了创建和销毁线程的成本。

以下是在不同版本的.NET框架中ThreadPool内可以创建的最优线程数列表:
- .NET Framework 2.0中每个核心25个
- .NET Framework 3.5中每个核心250个
- .NET Framework 4.0在32位环境中1,023个
- .NET Framework 4.0及更高版本以及.NET core在64位环境中32,768个

在与一家投资银行合作时,我们遇到了一个交易过程几乎需要1,800秒来同步记录近1,000笔交易的情况。在尝试了各种最优数量后,我们最终转向使用ThreadPool并使过程多线程化。使用.NET Framework 2.0版本时,应用程序在接近72秒内完成。使用3.5版本时,同一个应用程序仅用了几秒钟就完成了。这是一个典型的使用已有框架而不是重新发明轮子的例子。你可以通过更新框架来获得急需的性能提升。

我们可以通过调用ThreadPool.QueueUserWorkItem来通过ThreadPool创建线程,如下所示的示例所示。

这是我们想要并行调用的方法:
```csharp
private static void PrintNumber10Times(object state)
{
    for (int i = 0; i < 10; i++)
    {
        Console.Write(1);
    }
    Console.WriteLine();
}
```
以下是我们如何在使用ThreadPool.QueueUserWorkItem时创建线程并传递WaitCallback委托:
```csharp
private static void CreateThreadUsingThreadPool()
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(PrintNumber10Times));
}
```
以下是从Main方法中的调用:
```csharp
using System;
using System.Threading;
namespace Ch01
{
    class _4ThreadPool
    {
        static void Main(string[] args)
        {
            Console.WriteLine("开始执行!");
            CreateThreadUsingThreadPool();
            Console.WriteLine("完成执行");
            Console.ReadLine();
        }
    }
}
```
上述代码的输出如下:

每个线程池都维护着线程的最小和最大数量。这些值可以通过调用以下静态方法来修改:
ThreadPool.SetMinThreads
ThreadPool.SetMaxThreads
线程是通过System.Threading创建的。Thread类不属于ThreadPool。
让我们来看一下使用ThreadPool类的优点和缺点,以及何时避免使用它。

Advantages, disadvantages, and when to avoid using ThreadPool
The advantages of the ThreadPool are as follows:
Threads can be utilized to free up the main thread.
Threads are created and maintained in an optimal way by CLR.
The disadvantages of the ThreadPool are as follows:
With more threads, the code becomes difficult to debug and maintain.
We need to do exception handling inside the worker method as any unhandled
exception can result in the program crashing.
Progress reporting, cancellations, and completion logic need to be written from
scratch.
The following are the reasons when we should avoid ThreadPool :
When we need a foreground thread.
When we need to set an explicit priority to a thread.
When we have long-running or blocking tasks. Having a large number of
blocked threads in the pool will prevent new tasks from starting due to the
limited number of threads that are available per process in ThreadPool .
If we need STA threads since ThreadPool threads are MTA by default.
If we need to dedicate a thread to a task by providing it a distinct identity since
we cannot name a ThreadPool thread.

线程池的优点如下:
- 线程可以用来释放主线程。
- 线程的创建和维护由CLR以最优方式进行。

线程池的缺点如下:
- 随着线程数量的增加,代码变得难以调试和维护。
- 我们需要在工作方法内部进行异常处理,因为任何未处理的异常都可能导致程序崩溃。
- 进度报告、取消和完成逻辑需要从头开始编写。

以下是我们应该避免使用线程池的原因:
- 当我们需要前台线程时。
- 当我们需要为线程设置明确优先级时。
- 当我们有长时间运行或阻塞任务时。由于每个进程中可用的线程池中的线程数量有限,拥有大量被阻塞的线程将阻止新任务启动。
- 如果我们需要STA线程,因为线程池线程默认是MTA的。
- 如果我们需要在任务中专用一个线程并为其提供一个独特的身份,因为我们不能命名一个线程池线程。

BackgroundWorker
BackgroundWorker is a construct provided by .NET to create more manageable threads
from a ThreadPool . When explaining GUI-based applications, we saw that the Main
method was decorated with the STAThread attribute. This attribute guarantees control
safety as controls are created in the apartment owned by the thread and cannot be shared
with other threads. In Windows applications, there is the main thread of execution that
owns the UI and controls, which is created when the application starts. It is responsible for
accepting user inputs and painting or repainting the UI based on the actions of the user. For
a great user experience, we should try to make the UI as thread-free as possible and
delegate all time-consuming tasks to worker threads. Some common tasks that are usually
assigned to worker threads are as follows:
Downloading images from a server
Interacting with a database
Interacting with a filesystem
Interacting with web services
Complex local computations
As you can see, most of these are input/output (I/O) operations. I/O operations are carried
out by the CPU. The moment we call a piece of code that encapsulates an I/O operation, the
execution is passed from the thread to the CPU, which performs the task. When it is
complete, the result of the operation is returned to the caller thread. This period from
passing the baton and receiving results is a period of inactivity for the thread as it just has
to wait for the operation to complete. If this occurs in the main thread, the application
becomes unresponsive. For this reason, it makes sense to delegate these tasks to the worker
threads. There are still several challenges to overcome with regard to responsive
applications. Let's look at an example.

BackgroundWorker是.NET提供的一个构造,用于从ThreadPool创建更易于管理的线程。在解释基于GUI的应用程序时,我们看到了Main方法被装饰了STAThread属性。这个属性保证了控件的安全性,因为控件是在线程拥有的公寓中创建的,不能与其他线程共享。在Windows应用程序中,有一个主执行线程拥有UI和控件,这是在应用程序启动时创建的。它负责接受用户输入并根据用户的操作绘制或重绘UI。为了获得良好的用户体验,我们应该尽可能地使UI线程自由,并将所有耗时的任务委托给工作线程。通常分配给工作线程的一些常见任务如下:
从服务器下载图像
与数据库交互
与文件系统交互
与Web服务交互
复杂的本地计算
正如您所看到的,这些大多是输入/输出(I/O)操作。I/O操作由CPU执行。当我们调用封装I/O操作的代码时,执行从线程传递给CPU,后者执行任务。当任务完成时,操作的结果返回给调用者线程。从传递接力棒到接收结果的这段时间是线程的不活动期,因为它只需等待操作完成。如果这发生在主线程中,应用程序会变得无响应。因此,将这些任务委托给工作线程是有意义的。关于响应式应用程序,仍然有几个挑战需要克服。让我们看一个例子。

Case study:
We need to fetch data from a service that streams data. We would like to update the user
with the percentage completion of work. Once the work is complete, we need to update the
user with all the data.
Challenges:
The service call takes time, so we need to delegate the call in a worker thread to avoid UI
freeze.

Solution:
BackgroundWorker is a class provided in System. ComponentModel that can be used to
create a worker thread utilizing ThreadPool , as we discussed previously. This means that
it works in an efficient way. BackgroundWorker also supports progress reporting and
cancellations, apart from notifying the result of the operation.
This scenario can be further explained with the following code:
using System;
using System. ComponentModel;
using System. Text;
using System. Threading;
namespace Ch01
{
class _5BackgroundWorker
{
static void Main(string[] args)
{
var backgroundWorker = new BackgroundWorker() ;
backgroundWorker. WorkerReportsProgress = true;
backgroundWorker. WorkerSupportsCancellation = true;
backgroundWorker. DoWork += SimulateServiceCall;
backgroundWorker. ProgressChanged += ProgressChanged;
backgroundWorker. RunWorkerCompleted +=
RunWorkerCompleted;
backgroundWorker. RunWorkerAsync() ;
Console. WriteLine("To Cancel Worker Thread Press C. ") ;
while (backgroundWorker. IsBusy)
{
if (Console. ReadKey(true) . KeyChar == ' C' )
{
backgroundWorker. CancelAsync() ;
}
}
}
// This method executes when the background worker finishes
// execution
private static void RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (e. Error ! = null)
{
Console. WriteLine(e. Error. Message) ;
}
else

Console. WriteLine($"Result from service call
is {e. Result}") ;
}
// This method is called when background worker want to
// report progress to caller
private static void ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
Console. WriteLine($"{e. ProgressPercentage}% completed") ;
}
// Service call we are trying to simulate
private static void SimulateServiceCall(object sender,
DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
StringBuilder data = new StringBuilder() ;
//Simulate a streaming service call which gets data and
//store it to return back to caller
for (int i = 1; i <= 100; i++)
{
//worker. CancellationPending will be true if user
//press C
if (! worker. CancellationPending)
{
data. Append(i) ;
worker. ReportProgress(i) ;
Thread. Sleep(100) ;
//Try to uncomment and throw error
//throw new Exception("Some Error has occurred") ;
}
else
{
//Cancels the execution of worker
worker. CancelAsync() ;
}
}
e. Result = data;
}
}
}

BackgroundWorker provides an abstraction over raw threads, which gives more control
and options to the user. The best part about using BackgroundWorker is that it uses
an Event-Based Asynchronous Pattern (EAP), which means it is able to interact with the
code more efficiently than raw threads. The code is more or less self-explanatory. In order
to raise progress reporting and cancellation events, you need to set the following
properties to true :
backgroundWorker. WorkerReportsProgress = true;
backgroundWorker. WorkerSupportsCancellation = true;
You need to subscribe to the ProgressChanged event to receive progress, the DoWork
event to pass a method that needs to be invoked by the thread, and
the RunWorkerCompleted event to receive either the final results or any error messages
from the thread's execution:
backgroundWorker. DoWork += SimulateServiceCall;
backgroundWorker. ProgressChanged += ProgressChanged;
backgroundWorker. RunWorkerCompleted += RunWorkerCompleted;
Once this has been set up, you can invoke the worker by calling the following command:
backgroundWorker. RunWorkerAsync() ;
At any point in time, you can cancel the execution of the thread by calling
the backgroundWorker. CancelAsync() method, which sets the CancellationPending
property on the worker thread. We need to write some code that keeps checking this flag
and exits gracefully.
If there are no exceptions, the result of the thread's execution can be returned to the caller
by setting the following:
e. Result = data;
If there are any unhandled exceptions in the program, they are returned to the caller
gracefully. We can do this by wrapping it into RunWorkerCompletedEventArgs and
passing it as a parameter to the RunWorkerCompleted event handler.
We will look at the advantages and disadvantages of using BackgroundWorker in the next
section.

案例研究:
我们需要从一个流式传输数据的服务中获取数据。我们希望用完成工作的百分比来更新用户。一旦工作完成,我们需要用所有数据来更新用户。
挑战:
服务调用需要时间,因此我们需要在工作线程中委托调用以避免UI冻结。

解决方案:
BackgroundWorker是System.ComponentModel中提供的一个类,可以用来创建一个利用ThreadPool的工作线程,就像我们之前讨论的那样。这意味着它的工作方式很高效。BackgroundWorker还支持进度报告和取消操作,除了通知操作结果之外。
这个场景可以通过以下代码进一步解释:
```csharp
using System;
using System.ComponentModel;
using System.Text;
using System.Threading;

namespace Ch01
{
    class _5BackgroundWorker
    {
        static void Main(string[] args)
        {
            var backgroundWorker = new BackgroundWorker();
            backgroundWorker.WorkerReportsProgress = true;
            backgroundWorker.WorkerSupportsCancellation = true;
            backgroundWorker.DoWork += SimulateServiceCall;
            backgroundWorker.ProgressChanged += ProgressChanged;
            backgroundWorker.RunWorkerCompleted += RunWorkerCompleted;
            backgroundWorker.RunWorkerAsync();
            Console.WriteLine("To Cancel Worker Thread Press C.");
            while (backgroundWorker.IsBusy)
            {
                if (Console.ReadKey(true).KeyChar == 'C')
                {
                    backgroundWorker.CancelAsync();
                }
            }
        }

        // 当后台工作线程完成执行时调用的方法
        private static void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                Console.WriteLine(e.Error.Message);
            }
            else
            {
                Console.WriteLine($"Result from service call is {e.Result}");
            }
        }

        // 当后台工作线程想要向调用者报告进度时调用的方法
        private static void ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            Console.WriteLine($"{e.ProgressPercentage}% completed");
        }

        // 我们试图模拟的服务调用
        private static void SimulateServiceCall(object sender, DoWorkEventArgs e)
        {
            var worker = sender as BackgroundWorker;
            StringBuilder data = new StringBuilder();
            // 模拟一个流式服务调用,获取数据并存储以便返回给调用者
            for (int i = 1; i <= 100; i++)
            {
                if (!worker.CancellationPending)
                {
                    data.Append(i);
                    worker.ReportProgress(i);
                    Thread.Sleep(100);
                    // 尝试取消注释并抛出错误
                    // throw new Exception("Some Error has occurred");
                }
                else
                {
                    // 取消工作线程的执行
                    worker.CancelAsync();
                }
            }
            e.Result = data;
        }
    }
}
```

BackgroundWorker提供了对原始线程的抽象,这给用户提供了更多的控制和选择。使用BackgroundWorker的最佳部分是它使用了基于事件的异步模式(EAP),这意味着它能够比原始线程更有效地与代码交互。代码或多或少是自解释的。为了触发进度报告和取消事件,你需要将以下属性设置为true:
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.WorkerSupportsCancellation = true;
你需要订阅ProgressChanged事件以接收进度,订阅DoWork事件以传递需要由线程调用的方法,以及订阅RunWorkerCompleted事件以接收来自线程执行的最终结果或任何错误消息:
backgroundWorker.DoWork += SimulateServiceCall;
backgroundWorker.ProgressChanged += ProgressChanged;
backgroundWorker.RunWorkerCompleted += RunWorkerCompleted;
一旦设置好这些,你可以通过调用以下命令来调用工作线程:
backgroundWorker.RunWorkerAsync();
在任何时候,你都可以通过调用backgroundWorker.CancelAsync()方法来取消线程的执行,这将在工作线程上设置CancellationPending属性。我们需要编写一些代码,不断检查这个标志并优雅地退出。
如果没有异常,线程执行的结果可以通过设置以下内容返回给调用者:
e.Result = data;
如果程序中有任何未处理的异常,它们可以通过将其包装到RunWorkerCompletedEventArgs中并将其作为参数传递给RunWorkerCompleted事件处理程序来优雅地返回给调用者。
我们将在下一节中讨论使用BackgroundWorker的优点和缺点。

Advantages and disadvantages of using BackgroundWorker
The advantages of using BackgroundWorker are as follows:
Threads can be utilized to free up the main thread.
Threads are created and maintained in an optimal way by the ThreadPool
class's CLR.
Graceful and automatic exception handling.
Supports progress reporting, cancellation, and completion logic using events.
The disadvantage of using BackgroundWorker is that, with more threads, the code
becomes difficult to debug and maintain.

使用BackgroundWorker的优点和缺点如下:

优点:
1. 可以利用线程来释放主线程。
2. 线程的创建和维护由ThreadPool类的CLR以最优方式进行。
3. 优雅且自动的异常处理。
4. 支持使用事件进行进度报告、取消和完成逻辑。

缺点:
使用更多的线程会使代码变得难以调试和维护。

Multithreading versus multitasking
We have seen how both multithreading and multitasking work. Both have advantages and
disadvantages and you can use either, depending on your specific use case. The following
are some examples where multithreading can come in handy:
If you need a system that is easy to set up and terminate: Multithreading can be
useful when you have a process that has a large overhead. With threads, all you
need to do is copy the thread stack. Creating a duplicate process, however,
means recreating the entire data process in a separate memory space.
If you require fast task switching: The CPU caches and program context can be
easily maintained between threads in a process. If you have to switch the CPU to
a different process, however, it has to be reloaded.
If you need to share data with other threads: All the threads inside a process
share the same memory pool, which makes it easier for them to share data to
compare processes. If processes want to share data, they need I/O operation and
transport protocols, which is expensive.
In this section, we have discussed the basics of multithreading and multitasking, alongside
various approaches that were used to create threads in older versions of .NET. In the next
section, we will try to understand some scenarios where you can utilize parallel
programming techniques.

多线程与多任务
我们已经了解了多线程和多任务的工作方式。它们各有优缺点,你可以根据你的具体使用情况选择使用。以下是一些可以使用多线程的例子:
如果你需要一个容易设置和终止的系统:当一个进程有很大的开销时,多线程会很有用。对于线程,你只需要复制线程栈。然而,创建一个重复的进程意味着在另一个内存空间中重新创建整个数据进程。
如果你需要快速的任务切换:CPU缓存和程序上下文可以在进程中的线程之间轻松保持。然而,如果必须将CPU切换到不同的进程,它必须被重新加载。
如果你需要与其他线程共享数据:进程中的所有线程共享相同的内存池,这使得它们更容易共享数据相比于进程。如果进程想要共享数据,它们需要I/O操作和传输协议,这是昂贵的。
在本节中,我们讨论了多线程和多任务的基础知识,以及在.NET的旧版本中创建线程的各种方法。在下一节中,我们将尝试理解一些可以利用并行编程技术的场景。

Scenarios where parallel programming can
come in handy
The following are the scenarios in which parallel programming can be useful:
Creating a responsive UI for GUI-based applications: We can delegate all of the
heavy lifting and time-consuming tasks to the worker thread, thereby allowing
the UI thread to process user interactions and the UI repainting tasks.
Processing simultaneous requests: In server-side programming scenarios, we
need to process a large number of concurrent users. We can create a separate
thread to process each request. For example, we can use an ASP.NET request
model, which makes use of ThreadPool and assigns a thread to every request
that hits the server. Then, the thread takes care of processing the request and
returning a response to the client. In a client-side scenario, we can call multiple
mutually exclusive API calls via multithreading to save time.
Making efficient use of CPU: With multi-core processors, only one core is
generally utilized without multithreading and is overburdened. We can make
full use of CPU resources by creating multiple threads, each running on separate
CPUs. Sharing the burden in this way results in improved performance. This is
useful for long-running and complex calculations, which can be performed faster
using a divide-and-conquer strategy.
Speculative approaches: Scenarios involving more than one algorithm, such as
for an input set of numbers, where we want to get a sorted set as quickly as
possible. The only way to do this is to pass the input to all the algorithms and run
them in parallel, and whichever finishes first is accepted, while the rest are
canceled.

并行编程可能派上用场的场景
以下是一些并行编程可能有用的场景:
为基于GUI的应用程序创建响应式UI:我们可以将所有繁重和耗时的任务委托给工作线程,从而允许UI线程处理用户交互和UI重绘任务。
处理同时请求:在服务器端编程场景中,我们需要处理大量并发用户。我们可以为每个请求创建一个单独的线程。例如,我们可以使用ASP.NET请求模型,该模型利用ThreadPool并为击中服务器的每个请求分配一个线程。然后,线程负责处理请求并向客户端返回响应。在客户端场景中,我们可以通过多线程调用多个互斥的API调用来节省时间。
有效利用CPU:在没有多线程的情况下,通常只有一个核心被利用并且负担过重。我们可以通过创建多个线程来充分利用CPU资源,每个线程在不同的CPU上运行。以这种方式分担负担可以提高效率。这对于长时间运行和复杂计算很有用,可以使用分而治之的策略更快地执行。
投机方法:涉及多种算法的场景,例如对于一组输入数字,我们希望尽快获得排序后的集合。唯一的方法是将所有输入传递给所有算法并在并行中运行它们,哪个先完成就接受哪个,其余的则取消。

Advantages and disadvantages of
parallel programming
Multithreading leads to parallelism, which has its own programming and pitfalls. Now that
we have grasped the basic concepts of parallel programming, it is important to understand
its advantages and disadvantages.

The following are the benefits of parallel programming:
Enhanced performance: We can achieve better performance since tasks are
distributed across threads that run in parallel.
Improved GUI responsiveness: Since tasks perform non-blocking I/O, this
means the GUI thread is always free to accept user inputs. This results in better
responsiveness.
The simultaneous and parallelized occurrence of tasks: Since tasks run in
parallel, we can simultaneously run different programming logic.
Better use of cache storage by utilizing resources and better use of CPU
resources. Tasks can run on different cores, thereby ensuring maximizing
throughput.
Parallel programming also has the following disadvantages:
Complex debugging and testing processes: It's not easy to debug threads
without good multithreading tool support as different threads runs in parallel.
Context switching overheads: Every thread works on a slice of time that's been
allocated to it. Once the time slice expires, context switching happens, which also
wastes resources.
High chance of deadlock occurrence: If multiple threads work on a shared
resource, we need to apply locks to achieve thread-safety. This can lead to
deadlocks if multiple threads are simultaneously locking and waiting for shared
resources.
Difficult to program: With code branching, parallel programs can be difficult to
write compared to synchronous versions.
Unpredictable results: Since parallel programming relies on CPU cores, we can
get different results on different configuration machines.
We should always understand that parallel programming is a relative concept and that
something that worked for others may or may not work for you. You are advised to
implement this approach and validate it yourself.

并行编程的优点和缺点
多线程导致了并行性,这有其自身的编程优势和缺陷。现在我们已经掌握了并行编程的基本概念,理解其优点和缺点是很重要的。

以下是并行编程的好处:
提升性能:我们可以实现更好的性能,因为任务分布在并行运行的线程中。
改善GUI响应性:由于任务执行非阻塞I/O,这意味着GUI线程始终可以自由接受用户输入。这导致更好的响应性。
任务的同时和并行发生:由于任务并行运行,我们可以同时运行不同的编程逻辑。
通过利用资源和更好地使用CPU资源,更好地使用缓存存储。任务可以在不同的核心上运行,从而确保最大化吞吐量。
并行编程也有以下缺点:
复杂的调试和测试过程:如果没有良好的多线程工具支持,调试线程并不容易,因为不同的线程并行运行。
上下文切换开销:每个线程都在分配给它的时间片上工作。一旦时间片到期,就会发生上下文切换,这也浪费了资源。
死锁发生的高风险:如果多个线程在共享资源上工作,我们需要应用锁来实现线程安全。如果多个线程同时锁定并等待共享资源,这可能导致死锁。
难以编程:与同步版本相比,带有代码分支的并行程序可能难以编写。
不可预测的结果:由于并行编程依赖于CPU核心,我们可能会在不同的配置机器上得到不同的结果。
我们应该始终理解并行编程是一个相对的概念,对别人有用的东西可能对你有用也可能没用。建议你实施这种方法并自行验证。

Summary
In this chapter, we discussed the scenarios, benefits, and pitfalls of parallel programming.
Computer systems have evolved over the last few decades from single-core processors to
multi-core processors. The hardware in chips has become HT-enabled, thereby increasing
the performance of modern systems.
Before embarking on your journey in parallel programming, it's a good idea to understand
the basic concepts related to the OS, such as processes, tasks, and the difference between
multithreading and multitasking.
In the next chapter, we will focus our discussion entirely on the TPL and its associated
implementations. In the real world, however, there is a lot of legacy code that still relies on
older constructs, so knowledge of these will be handy.
Questions
Multithreading is a superset of parallel programming. 1.
True 1.
False 2.
How many cores will there be in a single-processor dual-core machine with 2.
hyper-threading enabled?
2 1.
4 2.
8 3.
When an application exits, all the foreground threads are killed as well. There is 3.
no separate logic required to close foreground threads on an application's exit.
True 1.
False 2.
Which exception is thrown when a thread has tried to access controls it has not 4.
owned/created?
ObjectDisposedException 1.
InvalidOperationException 2.
CrossThreadException 3.
Which of these provides cancellation support and progress reporting? 5.
Thread 1.
BackgroundWorker 2.
ThreadPool 

### 摘要

在本章中,我们讨论了并行编程的场景、好处和缺陷。计算机系统在过去几十年里从单核处理器发展到了多核处理器。芯片中的硬件已经启用了超线程技术(HT),从而提高了现代系统的性能。

在开始你的并行编程之旅之前,理解与操作系统相关的基本概念是很好的,例如进程、任务以及多线程和多任务之间的区别。
在下一章,我们将完全专注于讨论TPL(Task Parallel Library,任务并行库)及其相关实现。然而,在现实世界中,还有很多遗留代码依赖于较旧的结构,因此了解这些知识将会很有用。

### 问题

1. 多线程是并行编程的超集。
   - 真 1.
   - 假 2.

2. 在启用了2个超线程的单处理器双核机器中将有多少核心?
   - 2 1.
   - 4 2.
   - 8 3.

3. 当应用程序退出时,所有前台线程也会被终止。应用程序退出时不需要单独的逻辑来关闭前台线程。
   - 真 1.
   - 假 2.

4. 当一个线程试图访问它没有拥有/创建的控件时,会抛出哪种异常?
   - ObjectDisposedException 1.
   - InvalidOperationException 2.
   - CrossThreadException 3.

5. 以下哪个提供了取消支持和进度报告?
   - Thread 1.
   - BackgroundWorker 2.
   - ThreadPool 3.

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值