多线程编程中的同步、存储与异步模式解析
在多线程编程领域,存在着诸多复杂的问题和有效的解决方案。下面将详细介绍线程本地存储、定时器以及异步编程模型等关键内容。
线程本地存储
在某些情况下,使用同步锁会导致性能下降和可扩展性受限,或者对特定数据元素进行同步操作可能过于复杂。线程本地存储(Thread Local Storage)是实现数据隔离的一种有效方法,它让每个线程都拥有自己独立的变量实例,从而避免了同步的需求。
ThreadLocal
在 .NET Framework 4 中,可以通过声明
ThreadLocal<T>
类型的字段来使用线程本地存储。以下是示例代码:
using System;
using System.Threading;
class Program
{
public static double Count
{
get { return _Count.Value; }
set { _Count.Value = value; }
}
public static void Main()
{
Thread thread = new Thread(Decrement);
static ThreadLocal<double> _Count =
new ThreadLocal<double>(() => 0.01134);
thread.Start();
// Increment
for (double i = 0; i < short.MaxValue; i++)
{
Count++;
}
thread.Join();
Console.WriteLine("Main Count = {0}", Count);
}
static void Decrement()
{
Count = -Count;
for (double i = 0; i < short.MaxValue; i++)
{
Count--;
}
Console.WriteLine(
"Decrement Count = {0}", Count);
}
}
在上述代码中,执行
Main()
方法的线程和执行
Decrement()
方法的线程对
Count
的操作是相互独立的。
Main()
线程的初始值为 0.01134,最终值为 32767.01134;
Decrement()
线程的初始值为 -0.01134,最终值为 -32767.01134。这表明每个线程都有自己独立的
_Count.Value
实例。
ThreadStaticAttribute
另一种实现线程本地存储的方法是使用
ThreadStaticAttribute
修饰静态字段。示例代码如下:
using System;
using System.Threading;
class Program
{
[ThreadStatic]
static double _Count = 0.01134;
public static double Count
{
get { return Program._Count; }
set { Program._Count = value; }
}
public static void Main()
{
Thread thread = new Thread(Decrement);
thread.Start();
// Increment
for (int i = 0; i < short.MaxValue; i++)
{
Count++;
}
thread.Join();
Console.WriteLine("Main Count = {0}", Count);
}
static void Decrement()
{
for (int i = 0; i < short.MaxValue; i++)
{
Count--;
}
Console.WriteLine("Decrement Count = {0}", Count);
}
}
在这个例子中,执行
Main()
方法的线程和执行
Decrement()
方法的线程对
Count
的操作同样是独立的。不过,需要注意的是,只有执行静态构造函数的线程关联的线程本地存储实例会被初始化为 0.01134,
Decrement()
线程中的
_Count
会被初始化为 0。因此,在每个线程最初调用的方法中初始化线程本地存储字段是一个良好的实践。
是否使用线程本地存储需要进行成本效益分析。例如,对于数据库连接,创建每个线程的连接可能成本较高,而对连接进行加锁同步又会限制可扩展性。此外,线程本地存储还可以用于在不通过参数显式传递数据的情况下,让常用的上下文信息对其他方法可用,从而保持 API 的简洁性。
定时器
在使用定时器类时,可能会意外出现与用户界面相关的线程问题。因为定时器通知回调触发时,执行的线程可能不是用户界面线程,从而无法安全地访问用户界面控件和表单。
常见的定时器类有
System.Windows.Forms.Timer
、
System.Timers.Timer
和
System.Threading.Timer
。
System.Windows.Forms.Timer
是专门为富客户端用户界面设计的,它可以安全地从能够与用户界面交互的线程触发事件。
System.Timers.Timer
是
System.Threading.Timer
的包装器,抽象并增加了一些功能。
System.Threading.Timer
不继承自
System.ComponentModel.Component
,不能作为组件放在组件容器中,但它允许在启动定时器时传递状态参数。
以下是不同定时器的特性对比表格:
| 特性 | System.Timers.Timer | System.Threading.Timer | System.Windows.Forms.Timer |
| — | — | — | — |
| 支持实例化后添加和移除监听器 | Yes | No | Yes |
| 支持在用户界面线程上回调 | Yes | No | Yes |
| 从线程池获取的线程回调 | Yes | Yes | No |
| 支持在 Windows 窗体设计器中拖放 | Yes | No | Yes |
| 适用于多线程服务器环境 | Yes | Yes | No |
| 支持从定时器初始化传递任意状态到回调 | No | Yes | No |
| 实现 IDisposable | Yes | Yes | Yes |
| 支持开关回调和周期性重复回调 | Yes | Yes | Yes |
| 可跨应用程序域边界访问 | Yes | Yes | Yes |
| 支持 IComponent,可托管在 IContainer 中 | Yes | No | Yes |
对于用户界面编程,
System.Windows.Forms.Timer
是一个明显的选择,但需要注意长时间运行的操作可能会延迟定时器到期。在
System.Timers.Timer
和
System.Threading.Timer
之间选择时,如果需要托管在
IContainer
中,则选择
System.Timers.Timer
;如果不需要特定功能,默认选择
System.Threading.Timer
,因为它的实现更轻量级。
以下是使用
System.Timers.Timer
的示例代码:
using System;
using System.Timers;
using System.Threading;
// Because Timer exists in both the System.Timers and
// System.Threading namespaces, you disambiguate "Timer"
// using an alias directive.
class UsingSystemTimersTimer
{
private static int _Count=0;
private static readonly ManualResetEvent _ResetEvent =
new ManualResetEvent(false);
private static int _AlarmThreadId;
public static void Main()
{
using Timer = System.Timers.Timer;
using( Timer timer = new Timer() )
{
// Initialize Timer
timer.AutoReset = true;
timer.Interval = 1000;
timer.Elapsed +=
new ElapsedEventHandler(Alarm);
timer.Start();
// Wait for Alarm to fire for the 10th time.
_ResetEvent.WaitOne();
}
// Verify that the thread executing the alarm
// Is different from the thread executing Main
if(_AlarmThreadId ==
Thread.CurrentThread.ManagedThreadId)
{
throw new ApplicationException(
"Thread Ids are the same.");
}
Console.WriteLine(
"(Alarm Thread Id) {0} != {1} (Main Thread Id)",
_AlarmThreadId,
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(
"Final Count = {0}", _Count);
}
static void Alarm(
object sender, ElapsedEventArgs eventArgs)
{
_Count++;
Console.WriteLine("{0}:- {1}",
_Count,
eventArgs.SignalTime.ToString("T"));
if (_Count >= 9)
{
_AlarmThreadId =
Thread.CurrentThread.ManagedThreadId;
_ResetEvent.Set();
}
}
}
以下是使用
System.Threading.Timer
的示例代码:
using System;
using System.Threading;
class UsingSystemThreadingTimer
{
private static int _Count=0;
private static readonly AutoResetEvent _ResetEvent =
new AutoResetEvent(false);
private static int _AlarmThreadId;
public static void Main()
{
using( Timer timer =
new Timer(Alarm, null, 0, 1000) )
{
// Wait for Alarm to fire for the 10th time.
_ResetEvent.WaitOne();
}
// Verify that the thread executing the alarm
// Is different from the thread executing Main
if(_AlarmThreadId ==
Thread.CurrentThread.ManagedThreadId)
{
throw new ApplicationException(
"Thread Ids are the same.");
}
if(_Count < 9)
{
throw new ApplicationException(
" _Count < 9");
}
Console.WriteLine(
"(Alarm Thread Id) {0} != {1} (Main Thread Id)",
_AlarmThreadId,
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(
"Final Count = {0}", _Count);
}
static void Alarm(object state)
{
_Count++;
Console.WriteLine("{0}:- {1}",
_Count,
DateTime.Now.ToString("T"));
if (_Count >= 9)
{
_AlarmThreadId =
Thread.CurrentThread.ManagedThreadId;
_ResetEvent.Set();
}
}
}
异步编程模型
多线程编程存在诸多复杂性:
1. 监控异步操作状态以确定其是否完成,最好不通过轮询线程状态或阻塞等待。
2. 使用线程池避免启动和销毁线程的高成本,防止创建过多线程导致系统频繁切换线程。
3. 避免死锁,保护数据不被多个线程同时访问。
4. 确保操作的原子性并同步数据访问,通过对操作组添加同步机制,使操作作为一个整体执行,并防止被其他线程不当中断。
当方法执行时间较长时,通常需要进行多线程编程,以异步方式调用该方法。随着开发者编写更多的多线程代码,出现了一些常见的场景和编程模式,其中异步编程模型(APM)尤为突出。对于一个长时间运行的同步方法
X()
,APM 使用
BeginX()
方法异步启动
X()
的等效工作,使用
EndX()
方法结束该工作。
调用 APM
以下是使用
System.Net.WebRequest
类下载网页的示例代码,展示了如何调用 APM:
using System;
using System.IO;
using System.Net;
using System.Linq;
public class Program
{
public static void Main(string[] args)
{
string url = "http://www.intelliTechture.com";
if (args.Length > 0)
{
url = args[0];
}
Console.Write(url);
WebRequest webRequest = WebRequest.Create(url);
IAsyncResult asyncResult =
webRequest.BeginGetResponse(null, null);
// Indicate busy using dots
while (
!asyncResult.AsyncWaitHandle.WaitOne(100))
{
Console.Write('.');
}
WebResponse response =
webRequest.EndGetResponse(asyncResult);
// Retrieve the results when finished
// downloading
using (StreamReader reader =
new StreamReader(response.GetResponseStream()))
{
int length = reader.ReadToEnd().Length;
Console.WriteLine(FormatBytes(length));
}
}
static public string FormatBytes(long bytes)
{
string[] magnitudes =
new string[] { "GB", "MB", "KB", "Bytes" };
long max =
(long)Math.Pow(1024, magnitudes.Length);
return string.Format("{1:##.##} {0}",
magnitudes.FirstOrDefault(
magnitude =>
bytes > (max /= 1024) )?? "0 Bytes",
(decimal)bytes / (decimal)max).Trim();
}
}
APM 的关键在于
BeginX
和
EndX
方法对,它们具有明确的签名。
BeginX
返回一个
System.IAsyncResult
对象,用于访问异步调用的状态,
EndX
方法将该对象作为输入参数。每个
BeginX
调用必须对应一个
EndX
调用。
EndX
方法有四个主要作用:
1. 阻塞后续执行,直到请求的工作成功完成(或出错抛出异常)。
2. 如果方法
X
返回数据,可从
EndX
方法调用中获取该数据。
3. 若执行请求工作时发生异常,在
EndX
调用时会重新抛出异常。
4. 负责清理因调用
X
而需要清理的资源。
APM 签名
BeginX
和
EndX
方法的组合签名应与同步方法的签名匹配。例如,对于虚构的同步方法
bool TryDoSomething(string url, ref string data, out string[] links)
,其 APM 方法的参数映射如下:
System.IAsyncResult BeginTryDoSomething(
String url, ref string data, out string[] links,
System.AsyncCallback callback, object state)
bool EndTryDoSomething (ref string data, out string[] links,
System.IAyncResult result);
bool TryDosomething(
string url, ref string data, out string[] links)
所有输入参数映射到
BeginX
方法,返回参数映射到
EndX
方法的返回参数。
ref
和
out
参数由于会返回结果,也包含在
EndX
方法签名中。
带 AsyncCallback 的延续传递风格(CPS)
BeginX
方法还有两个在同步方法中未包含的参数:回调参数
System.AsyncCallback
和状态参数
object
。以下是示例代码:
using System;
using System.IO;
using System.Net;
using System.Linq;
using System.Threading;
public class Program
{
public static void Main(string[] args)
{
string url = "http://www.intelliTechture.com";
if (args.Length > 0)
{
url = args[0];
}
Console.Write(url);
WebRequest webRequest = WebRequest.Create(url);
WebRequestState state = new WebRequestState(webRequest);
IAsyncResult asyncResult =
webRequest.BeginGetResponse(
GetResponseAsyncCompleted, state);
// Indicate busy using dots
while (
!asyncResult.AsyncWaitHandle.WaitOne(100))
{
Console.Write('.');
}
state.ResetEvent.Wait();
}
private static void GetResponseAsyncCompleted(
IAsyncResult asyncResult)
{
WebRequestState completedState =
(WebRequestState)asyncResult.AsyncState;
HttpWebResponse response =
(HttpWebResponse)completedState.WebRequest
.EndGetResponse(asyncResult);
// Retrieve the results when finished downloading
Stream stream = response.GetResponseStream();
StreamReader reader = new StreamReader(stream);
int length = reader.ReadToEnd().Length;
Console.WriteLine(FormatBytes(length));
completedState.ResetEvent.Set();
completedState.Dispose();
}
static public string FormatBytes(long bytes)
{
string[] magnitudes =
new string[] { "GB", "MB", "KB", "Bytes" };
long max =
(long)Math.Pow(1024, magnitudes.Length);
return string.Format("{1:##.##} {0}",
magnitudes.FirstOrDefault(
magnitude =>
bytes > (max /= 1024) )?? "0 Bytes",
(decimal)bytes / (decimal)max).Trim();
}
}
class WebRequestState : IDisposable
{
public WebRequestState(WebRequest webRequest)
{
WebRequest = webRequest;
}
public WebRequest WebRequest { get; private set; }
private ManualResetEventSlim _ResetEvent =
new ManualResetEventSlim();
public ManualResetEventSlim ResetEvent
{ get { return _ResetEvent; } }
public void Dispose()
{
ResetEvent.Dispose();
GC.SuppressFinalize(this);
}
}
注册回调可以实现延续传递风格(CPS),避免将
EndGetResponse()
和
Console.WriteLine()
代码顺序放在
BeginGetResponse()
之后。虽然仍需调用
EndGetResponse()
,但将其放在回调中可确保在异步调用完成时不会阻塞主线程。
在 APM 方法之间传递状态
除了
AsyncCallback
参数,状态参数用于在回调执行时传递额外数据。可以使用自定义类(如
WebRequestState
)或匿名方法(包括 Lambda 表达式)来传递状态。以下是使用匿名方法传递状态的示例代码:
using System;
using System.IO;
using System.Net;
using System.Linq;
using System.Threading;
public class Program
{
public static void Main(string[] args)
{
string url = "http://www.intelliTechture.com";
if (args.Length > 0)
{
url = args[0];
}
Console.Write(url);
WebRequest webRequest = WebRequest.Create(url);
ManualResetEventSlim resetEvent =
new ManualResetEventSlim();
IAsyncResult asyncResult =
webRequest.BeginGetResponse(
(completedAsyncResult) =>
{
HttpWebResponse response =
(HttpWebResponse)webRequest.EndGetResponse(
completedAsyncResult);
Stream stream =
response.GetResponseStream();
StreamReader reader =
new StreamReader(stream);
int length = reader.ReadToEnd().Length;
Console.WriteLine(FormatBytes(length));
resetEvent.Set();
resetEvent.Dispose();
}, null);
// Indicate busy using dots
while (
!asyncResult.AsyncWaitHandle.WaitOne(100))
{
Console.Write('.');
}
resetEvent.Wait();
}
static public string FormatBytes(long bytes)
{
string[] magnitudes =
new string[] { "GB", "MB", "KB", "Bytes" };
long max =
(long)Math.Pow(1024, magnitudes.Length);
return string.Format("{1:##.##} {0}",
magnitudes.FirstOrDefault(
magnitude =>
bytes > (max /= 1024) )?? "0 Bytes",
(decimal)bytes / (decimal)max).Trim();
}
}
需要注意的是,使用
ManualResetEvent
来信号
AsyncCallback
完成,因为
IAsyncResult
的
WaitHandle
会在异步方法完成但
AsyncCallback
执行之前设置。如果仅阻塞
IAsyncResult
的
WaitHandle
,可能会在
AsyncCallback
执行之前退出程序。
资源清理
APM 的另一个重要规则是,即使错误地未调用
EndX
方法,也不应发生资源泄漏。例如,
WebRequestState
类拥有
ManualResetEvent
,需要确保该资源得到正确清理。
综上所述,线程本地存储、定时器和异步编程模型是多线程编程中非常重要的概念和技术,合理运用它们可以提高程序的性能和可维护性。在实际开发中,需要根据具体需求和场景选择合适的方法和工具。
多线程编程中的同步、存储与异步模式解析
异步编程模型的进一步分析
APM 模式的流程图
下面是一个简单的 mermaid 流程图,展示了 APM 模式的基本流程:
graph LR
A[调用 BeginX 方法] --> B[异步执行 X 方法]
B --> C{X 方法完成?}
C -- 否 --> B
C -- 是 --> D[调用 EndX 方法]
D --> E[处理结果或异常]
这个流程图清晰地展示了 APM 模式从开始异步操作到最终处理结果的整个过程。当调用
BeginX
方法后,系统会异步执行
X
方法,不断检查是否完成,完成后调用
EndX
方法进行结果处理或异常捕获。
APM 模式的实际应用场景
APM 模式在许多需要处理长时间运行操作的场景中非常有用。例如,在网络编程中,像文件下载、网页请求等操作通常会花费较长时间,使用 APM 模式可以避免阻塞主线程,提高程序的响应性能。
在数据库操作方面,当执行复杂的查询或批量数据处理时,使用 APM 模式可以让程序在等待数据库响应的同时继续执行其他任务,从而提高整体效率。
线程本地存储的应用案例分析
数据库连接池中的应用
在多线程环境下使用数据库连接池时,线程本地存储可以发挥重要作用。每个线程可以拥有自己独立的数据库连接,避免多个线程同时竞争同一个连接,从而减少锁的使用,提高性能。
以下是一个简单的示例代码,展示了如何在线程本地存储中管理数据库连接:
using System;
using System.Data.SqlClient;
using System.Threading;
class DatabaseConnectionManager
{
private static ThreadLocal<SqlConnection> _connection = new ThreadLocal<SqlConnection>(() =>
{
string connectionString = "YourConnectionString";
return new SqlConnection(connectionString);
});
public static SqlConnection GetConnection()
{
return _connection.Value;
}
}
在上述代码中,每个线程都有自己独立的
SqlConnection
实例,通过
GetConnection
方法可以获取当前线程的数据库连接。
日志记录中的应用
在多线程应用程序中进行日志记录时,线程本地存储可以用来存储每个线程的日志上下文信息。例如,在一个 Web 应用程序中,每个请求可能由不同的线程处理,使用线程本地存储可以记录每个请求的相关信息,如请求的 URL、用户 ID 等。
using System;
using System.Threading;
class LogContext
{
private static ThreadLocal<string> _requestUrl = new ThreadLocal<string>();
private static ThreadLocal<int> _userId = new ThreadLocal<int>();
public static void SetRequestUrl(string url)
{
_requestUrl.Value = url;
}
public static void SetUserId(int id)
{
_userId.Value = id;
}
public static void LogMessage(string message)
{
Console.WriteLine($"[{_requestUrl.Value} - User: {_userId.Value}] {message}");
}
}
在上述代码中,
SetRequestUrl
和
SetUserId
方法用于设置当前线程的请求 URL 和用户 ID,
LogMessage
方法可以使用这些信息进行日志记录。
定时器的使用建议和注意事项
定时器的选择流程
以下是一个 mermaid 流程图,展示了如何根据不同的需求选择合适的定时器:
graph LR
A[是否用于用户界面编程?] -->|是| B[使用 System.Windows.Forms.Timer]
A -->|否| C[是否需要托管在 IContainer 中?]
C -->|是| D[使用 System.Timers.Timer]
C -->|否| E[使用 System.Threading.Timer]
这个流程图可以帮助开发者根据具体需求快速选择合适的定时器。
定时器的性能优化
在使用定时器时,需要注意性能优化。例如,避免在定时器的回调方法中执行长时间运行的操作,因为这可能会导致定时器的触发时间不准确,甚至影响整个应用程序的性能。
另外,如果定时器的间隔时间设置得非常短,可能会导致系统资源的过度消耗。因此,需要根据实际需求合理设置定时器的间隔时间。
多线程编程的最佳实践总结
同步与异步的选择
在多线程编程中,需要根据具体的场景选择合适的同步或异步方式。如果操作是短时间的,并且需要立即得到结果,同步方式可能更合适;如果操作是长时间运行的,如网络请求、文件读写等,异步方式可以避免阻塞主线程,提高程序的响应性能。
资源管理
在多线程编程中,资源管理非常重要。确保在使用完资源后及时释放,避免资源泄漏。例如,在使用数据库连接、文件句柄等资源时,要使用
using
语句或手动调用
Dispose
方法进行资源清理。
错误处理
多线程编程中,错误处理也需要特别注意。在异步操作中,异常可能不会立即抛出,而是在
EndX
方法调用时抛出。因此,需要在合适的位置捕获并处理异常,确保程序的稳定性。
总之,多线程编程涉及到同步、存储、定时器和异步编程模型等多个方面的知识。通过合理运用这些技术和遵循最佳实践,可以开发出高性能、可维护的多线程应用程序。在实际开发中,需要根据具体需求和场景进行选择和调整,以达到最佳的效果。
超级会员免费看
10万+

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



