多线程模式与平台互操作性编程解析
1. 背景工作者模式
背景工作者模式为调用长时间运行的方法提供了一种异步模式,即使原设计中未实现该模式。以下是设置该模式的步骤:
1.
注册长时间运行的方法
:将长时间运行的方法注册到
BackgroundWorker
的
DoWork
事件中。例如,长时间运行的任务是调用
CalculatePi()
。
2.
接收进度或状态通知
:若要接收进度或状态通知,需将监听器连接到
BackgroundWorker.ProgressChanged
事件,并将
BackgroundWorker.WorkerReportsProgress
设置为
true
。
3.
注册完成方法
:将一个方法(如
Complete()
)注册到
BackgroundWorker.RunWorkerCompleted
事件。
4.
支持取消操作
:将
WorkerSupportsCancellation
属性设置为
true
,以支持取消操作。调用
BackgroundWorker.CancelAsync
会设置
DoWorkEventArgs.CancellationPending
标志。
5.
检查取消标志
:在
DoWork
提供的方法(如
CalculatePi()
)中,检查
DoWorkEventArgs.CancellationPending
属性,若为
true
则退出方法。
6.
启动工作
:设置完成后,调用
BackgroundWorker.RunWorkerAsync()
并提供一个状态参数,该参数将传递给指定的
DoWork()
方法。
graph LR
A[注册DoWork事件] --> B[设置进度通知]
B --> C[注册RunWorkerCompleted事件]
C --> D[支持取消操作]
D --> E[检查取消标志]
E --> F[启动工作]
该模式的优点是提供了明确的进度通知支持,但缺点是
DoWork()
方法必须符合
System.ComponentModel.DoWorkEventHandler
委托,若不符合则需要包装函数。
2. 异常处理
当后台工作线程执行时发生未处理的异常,
RunWorkerCompleted
委托的
RunWorkerCompletedEventArgs
参数的
Error
属性将设置为该异常。因此,在
RunWorkerCompleted
回调中检查
Error
属性可以处理异常。
static void Complete(object sender, RunWorkerCompletedEventArgs eventArgs)
{
Console.WriteLine();
if (eventArgs.Cancelled)
{
Console.WriteLine("Cancelled");
}
else if (eventArgs.Error != null)
{
Console.WriteLine("ERROR: {0}", eventArgs.Error.Message);
}
else
{
Console.WriteLine("Finished");
}
resetEvent.Set();
}
3. Windows UI 编程
Windows 操作系统使用单线程、基于消息处理的用户界面,这意味着同一时间只能有一个线程访问用户界面,代码应通过 Windows 消息泵来处理其他线程的交互。
3.1 Windows Forms
在 Windows Forms 编程中,检查线程是否可以调用 UI 涉及调用组件的
InvokeRequired
属性,以确定是否需要进行封送处理。若返回
true
,则需要封送处理,可通过调用
Invoke()
实现。
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
class Program : Form
{
private System.Windows.Forms.ProgressBar _ProgressBar;
[STAThread]
static void Main()
{
Application.Run(new Program());
}
public Program()
{
InitializeComponent();
Task.Factory.StartNew(Increment);
}
void UpdateProgressBar()
{
}
private void Increment()
{
for (int i = 0; i < 100; i++)
{
UpdateProgressBar();
Thread.Sleep(100);
}
}
private void InitializeComponent()
{
if (_ProgressBar.InvokeRequired)
{
MethodInvoker updateProgressBar = UpdateProgressBar;
_ProgressBar.BeginInvoke(updateProgressBar);
}
else
{
_ProgressBar.Increment(1);
}
if (InvokeRequired)
{
Invoke(new MethodInvoker(Close));
}
else
{
Close();
}
_ProgressBar = new ProgressBar();
SuspendLayout();
_ProgressBar.Location = new Point(13, 17);
_ProgressBar.Size = new Size(267, 19);
ClientSize = new Size(292, 53);
Controls.Add(this._ProgressBar);
Text = "Multithreading in Windows Forms";
ResumeLayout(false);
}
}
3.2 Windows Presentation Foundation (WPF)
在 WPF 平台上,实现相同的封送检查需要不同的方法。WPF 的
System.Windows.Application
类包含一个静态成员属性
Current
,类型为
DispatcherObject
。调用调度器的
CheckAccess()
方法与 Windows Forms 中控件的
InvokeRequired
功能相同。
using System;
using System.Windows;
using System.Windows.Threading;
public static class UIAction
{
public static void Invoke<T>(Action<T> action, T parameter)
{
Invoke(() => action(parameter));
}
public static void Invoke(Action action)
{
DispatcherObject dispatcher = Application.Current;
if (dispatcher == null || dispatcher.CheckAccess() || dispatcher.Dispatcher == null)
{
action();
}
else
{
SafeInvoke(action);
}
}
private static void SafeInvoke(Action action)
{
Exception exceptionThrown = null;
Action target = () =>
{
try
{
action();
}
catch (Exception exception)
{
exceptionThrown = exception;
}
};
Application.Current.Dispatcher.Invoke(target);
if (exceptionThrown != null)
{
throw exceptionThrown;
}
}
}
4. 控制 COM 线程模型
使用
STAThreadAttribute
可以控制 COM 线程模型。在 .NET 中,若程序不调用 COM 组件,COM 的线程规则和复杂性将消失。处理 COM 互操作性的一般方法是在进程的
Main
方法上使用
System.STAThreadAttribute
,将所有 .NET 组件置于主单线程单元中。这样可以避免跨单元边界调用大多数 COM 组件,且除非进行 COM 互操作调用,否则不会进行单元初始化。
5. 多线程模式选择
除了 TPL 提供的模式外,还有多种异步编程模式可供选择,如 APM、EAP 和背景工作者模式。一般来说,选择 API 提供的模式(如 APM 或 EAP)比使用 TPL 异步执行方法更好。在 EAP 情况下,若 TPL 可用,建议结合使用。选择背景工作者模式还是 TPL 则需要更细致的考虑。若
BackgroundWorker
能满足需求,开发者偏好可以作为决定因素;若需要额外功能,TPL 更合适。此外,若所有注册的监听器都通过匿名方法和闭包实现,TPL 可能更易于维护;否则,可考虑使用
BackgroundWorker
。
6. 平台互操作性与不安全代码
C# 提供了三种方式来访问内存地址和指针:
1.
平台调用(P/Invoke)
:用于调用非托管 DLL 暴露的 API。
2.
不安全代码
:允许直接访问内存指针和地址。
3.
COM 互操作性
:本文未涉及。
6.1 平台调用
平台调用(P/Invoke)允许开发者调用非托管代码。以下是使用 P/Invoke 的步骤:
6.1.1 声明外部函数
使用
extern
修饰符在类的上下文中声明目标 API。例如:
using System;
using System.Runtime.InteropServices;
class VirtualMemoryManager
{
[DllImport("kernel32.dll", EntryPoint="GetCurrentProcess")]
internal static extern IntPtr GetCurrentProcessHandle();
}
extern
方法总是静态的,且不包含实现。
DllImport
属性指向实现,至少需要指定定义函数的 DLL 名称。
6.1.2 参数数据类型
确定目标 DLL 和导出函数后,最困难的步骤是识别或创建与外部函数中未托管类型对应的托管数据类型。例如,对于
VirtualAllocEx()
API:
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect);
在 C# 中的声明如下:
using System;
using System.Runtime.InteropServices;
class VirtualMemoryManager
{
[DllImport("kernel32.dll")]
internal static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
IntPtr dwSize,
AllocationType flAllocationType,
uint flProtect);
}
由于托管代码中的基本数据类型(如
int
)大小固定,而未托管代码中的内存指针会根据处理器不同而变化,因此需要将
HANDLE
和
LPVOID
等类型映射到
System.IntPtr
。
6.1.3 使用 ref 而非指针
未托管代码常使用指针作为按引用传递的参数,在 P/Invoke 中,可将相应参数映射到
ref
或
out
。例如:
class VirtualMemoryManager
{
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool VirtualProtectEx(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize, uint flNewProtect,
ref uint lpflOldProtect);
}
通常建议使用
ref
而非
out
用于 P/Invoke 类型的参数。
6.1.4 使用 StructLayoutAttribute 进行顺序布局
对于一些没有对应托管类型的 API,需要在托管代码中重新声明类型。例如,声明未托管的
COLORREF
结构体:
[StructLayout(LayoutKind.Sequential)]
struct ColorRef
{
public byte Red;
public byte Green;
public byte Blue;
#pragma warning disable 414
private byte Unused;
#pragma warning restore 414
public ColorRef(byte red, byte green, byte blue)
{
Blue = blue;
Green = green;
Red = red;
Unused = 0;
}
}
通过这些步骤,开发者可以在 C# 中调用非托管代码,实现平台互操作性。
综上所述,多线程编程和平台互操作性是 C# 编程中的重要主题。背景工作者模式为异步执行长时间运行的任务提供了一种有效的方式,同时需要注意异常处理和 UI 线程的访问规则。在进行平台互操作性编程时,P/Invoke 是一种强大的工具,但需要注意参数数据类型的映射和类型声明。通过合理选择编程模式和正确使用相关技术,开发者可以提高程序的性能和可维护性。
多线程模式与平台互操作性编程解析
7. 确定计算机是否为虚拟机的示例程序
通过前面介绍的 P/Invoke 和不安全代码的知识,我们可以编写一个小的程序来确定计算机是否为虚拟机。以下是实现该功能的步骤:
1.
调用操作系统 DLL 分配内存
:调用操作系统的 DLL 来请求分配一块用于执行指令的内存区域。
2.
写入汇编指令
:将一些汇编指令写入分配的内存区域。
3.
注入地址位置
:将一个地址位置注入到汇编指令中。
4.
执行汇编代码
:执行写入的汇编代码。
这个示例程序充分展示了 C# 的强大功能,以及从 C# 和托管代码中仍然可以访问非托管代码的能力。
8. 总结与建议
在多线程编程和平台互操作性方面,我们有多种模式和技术可供选择。下面是一个总结表格,对比不同模式和技术的特点:
| 模式/技术 | 特点 | 适用场景 |
| — | — | — |
| APM(异步编程模型) | 通常由底层库暴露,用于异步调用长时间运行的方法 | 底层库编程,对性能要求较高 |
| EAP(基于事件的异步模式) | 适用于高级编程,提供取消和进度通知支持 | 高级编程,需要进度和取消功能 |
| 背景工作者模式 | 允许对长时间运行的方法施加异步模式 | 开发者偏好,功能需求相对简单 |
| TPL(任务并行库) | 提供强大的并行和异步编程功能 | 需要额外功能,注册监听器使用匿名方法和闭包 |
| P/Invoke(平台调用) | 用于调用非托管 DLL 暴露的 API | 需要调用非托管代码 |
| 不安全代码 | 允许直接访问内存指针和地址 | 对性能有极致要求,需要操作内存 |
graph LR
A[多线程编程] --> B[APM]
A --> C[EAP]
A --> D[背景工作者模式]
A --> E[TPL]
F[平台互操作性] --> G[P/Invoke]
F --> H[不安全代码]
根据上述总结,在选择模式和技术时,我们可以参考以下建议:
1.
优先选择 API 提供的模式
:如 APM 或 EAP,它们是专门为异步编程设计的,具有更好的兼容性和性能。
2.
结合 TPL 和 EAP
:如果 TPL 可用,建议结合使用 TPL 和 EAP,以充分利用它们的优势。
3.
根据需求选择背景工作者模式或 TPL
:如果
BackgroundWorker
能满足需求,可根据开发者偏好选择;如果需要额外功能,TPL 是更好的选择。
4.
谨慎使用 P/Invoke 和不安全代码
:P/Invoke 和不安全代码可以提供强大的功能,但也增加了代码的复杂性和风险,需要谨慎使用。
总之,多线程编程和平台互操作性是 C# 编程中复杂而重要的部分。通过深入理解不同的模式和技术,并根据实际需求进行合理选择和使用,开发者可以编写出高效、稳定的程序。同时,在使用 P/Invoke 和不安全代码时,要注意内存管理和异常处理,以避免潜在的问题。希望这些内容能帮助开发者在实际项目中更好地应用这些技术。
超级会员免费看
1619

被折叠的 条评论
为什么被折叠?



