多线程编程中的同步、存储与异步模式解析
线程本地存储
在多线程编程中,同步锁的使用有时会带来性能和可扩展性方面的问题。例如在某些情况下,为特定数据元素提供同步可能过于复杂,尤其是在原始代码编写完成后再添加同步逻辑时。此时,隔离是一种替代同步的解决方案,而线程本地存储就是实现隔离的一种方法。
线程本地存储使得每个线程都拥有自己独立的变量实例。这样一来,就无需进行同步操作,因为在单个线程的上下文中同步数据是没有意义的。常见的线程本地存储实现方式有
ThreadLocal<T>
和
ThreadStaticAttribute
。
ThreadLocal<T>
在 .NET Framework 4 中使用线程本地存储,需要声明一个
ThreadLocal<T>
类型的字段(或变量)。下面是一个使用
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);
}
}
运行该代码,输出结果如下:
Decrement Count = -32767.01134
Main Count = 32767.01134
从输出可以看出,执行
Main()
方法的线程的
Count
值不会被执行
Decrement()
方法的线程所改变。这是因为
Count
基于
ThreadLocal<T>
类型的静态字段,两个线程在
_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);
}
}
运行结果如下:
Decrement Count = -32767
Main Count = 32767.01134
可以发现,执行
Decrement()
方法的线程的
Count
值没有被初始化为 0.01134。这是因为只有运行静态构造函数的线程关联的线程静态实例会被初始化。因此,在每个线程最初调用的方法中初始化线程本地存储字段是一个好的做法。
是否使用线程本地存储需要进行一定的成本效益分析。例如,对于数据库连接,如果为每个线程创建一个连接可能成本较高;而对连接进行加锁同步又会限制可扩展性。此外,线程本地存储还可以用于在不通过参数显式传递数据的情况下,使常用的上下文信息可供其他方法使用。
定时器
在使用定时器类时,可能会意外出现与用户界面相关的线程问题。当定时器通知回调触发时,执行回调的线程可能不是用户界面线程,因此无法安全地访问用户界面控件和窗体。
常见的定时器类有
System.Windows.Forms.Timer
、
System.Timers.Timer
和
System.Threading.Timer
。
-
System.Windows.Forms.Timer
:专门为富客户端用户界面设计。程序员可以将其拖放到窗体上作为非可视化控件,并通过属性窗口控制其行为。它总是能从可以与用户界面交互的线程安全地触发事件。
-
System.Timers.Timer
和
System.Threading.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();
}
}
}
这两个示例代码的输出结果相同,如下所示:
12:19:36 AM:- 1
12:19:37 AM:- 2
12:19:38 AM:- 3
12:19:39 AM:- 4
12:19:40 AM:- 5
12:19:41 AM:- 6
12:19:42 AM:- 7
12:19:43 AM:- 8
12:19:44 AM:- 9
(Alarm Thread Id) 4 != 1 (Main Thread Id)
Final Count = 9
异步编程模型
多线程编程存在以下复杂性:
1.
监控异步操作状态以确定完成情况
:需要确定异步操作何时完成,最好不要通过轮询线程状态或阻塞等待的方式。
2.
线程池的使用
:避免启动和销毁线程的高昂成本,同时避免创建过多线程导致系统花费过多时间进行线程切换。
3.
避免死锁
:在保护数据不被不同线程同时访问时,防止死锁的发生。
4.
提供操作的原子性和同步数据访问
:对一组操作添加同步机制,确保操作作为一个整体执行,并避免被其他线程不当中断。
当一个方法需要长时间运行时,通常需要使用多线程编程,即异步调用该方法。随着开发者编写更多的多线程代码,会出现一些常见的场景和处理这些场景的编程模式。其中一个重要的模式是异步编程模型(APM)。
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();
}
}
运行结果如下:
http://www.intelliTechture.com. ........29.36 KB
APM 的关键在于
BeginX
和
EndX
方法对,它们有明确的签名。
BeginX
返回一个
System.IAsyncResult
对象,用于访问异步调用的状态,以便等待或轮询完成。
EndX
方法将这个返回值作为输入参数,确保
BeginX
和
EndX
方法的正确配对。每个
BeginX
调用必须对应一个
EndX
调用。
EndX
方法有四个作用:
1.
阻塞执行直到工作完成
:调用
EndX
会阻塞后续执行,直到请求的工作成功完成(或因异常出错)。
2.
获取返回数据
:如果方法
X
返回数据,可以从
EndX
方法调用中获取。
3.
重新抛出异常
:如果在执行请求的工作时发生异常,该异常会在调用
EndX
时重新抛出,就像在同步调用中发生异常一样。
4.
资源清理
:如果因
X
的调用需要清理任何资源,
EndX
将负责清理这些资源。
APM 签名
BeginX
和
EndX
APM 方法的组合签名应与同步方法的签名匹配。例如,对于一个虚构的同步方法
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
方法还有两个在同步方法中没有的额外参数:
callback
参数,一个
System.AsyncCallback
委托,用于在方法完成时调用;
state
参数,类型为
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()
之后。这样可以确保在异步调用完成时执行相应代码,同时不会阻塞主线程。
在 APM 方法之间传递状态
除了
AsyncCallback
参数,
state
参数用于在回调执行时传递额外数据。可以使用自定义类(如
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();
}
}
需要注意的是,虽然
IAsyncResult
包含一个
WaitHandle
,但它在异步方法完成但回调执行之前就会被设置。因此,为了确保在回调执行完成后再退出程序,我们使用了一个单独的
ManualResetEvent
。
资源清理
APM 的另一个重要规则是,即使错误地未调用
EndX
方法,也不应发生资源泄漏。例如,
WebRequestState
类拥有
ManualResetEvent
,它需要确保在适当的时候清理该资源。
通过以上介绍,我们了解了多线程编程中的线程本地存储、定时器以及异步编程模型等重要概念和技术,这些知识对于编写高效、稳定的多线程程序非常有帮助。
多线程编程中的同步、存储与异步模式解析
多线程模式的总结与应用建议
在多线程编程中,线程本地存储、定时器和异步编程模型(APM)各有其特点和适用场景。以下是对这些技术的总结和应用建议:
线程本地存储
线程本地存储提供了一种避免同步开销的方法,通过为每个线程提供独立的变量实例。
ThreadLocal<T>
和
ThreadStaticAttribute
是两种实现方式。
-
ThreadLocal<T>
:适用于 .NET Framework 4 及以上版本,提供了更灵活的初始化方式,即使是静态字段,每个线程也有独立的实例。
-
ThreadStaticAttribute
:在 .NET Framework 4 之前就可用,但需要注意字段初始化的问题,只有运行静态构造函数的线程关联的实例会被初始化。
应用建议:在需要为每个线程维护独立状态,且同步开销较大时,可以考虑使用线程本地存储。但在使用数据库连接等昂贵资源时,需要进行成本效益分析。
定时器
不同的定时器类适用于不同的场景:
-
System.Windows.Forms.Timer
:专门为富客户端用户界面设计,能确保在用户界面线程上安全触发事件,但长时间运行的操作可能会延迟定时器到期。
-
System.Timers.Timer
:是
System.Threading.Timer
的包装器,提供了更高级的功能,支持托管在
IContainer
中。
-
System.Threading.Timer
:实现更轻量级,允许传递状态参数,但不能作为组件放在组件容器中。
应用建议:对于用户界面编程,优先选择
System.Windows.Forms.Timer
;在服务器环境中,如果需要托管在
IContainer
中,选择
System.Timers.Timer
;否则,默认选择
System.Threading.Timer
。
异步编程模型(APM)
APM 模式通过
BeginX()
和
EndX()
方法实现异步操作,解决了多线程编程中的一些复杂性问题。
-
调用 APM
:使用
BeginX()
方法启动异步操作,
EndX()
方法结束操作,确保每个
BeginX
调用对应一个
EndX
调用。
-
APM 签名
:
BeginX
和
EndX
方法的签名与同步方法匹配,确保参数的正确映射。
-
延续传递风格(CPS)
:通过注册回调函数,实现异步操作完成后的延续执行,避免阻塞主线程。
-
传递状态
:使用
state
参数在 APM 方法之间传递额外数据,可以使用自定义类或匿名方法的闭包。
-
资源清理
:确保
EndX
方法负责清理因异步操作产生的资源,避免资源泄漏。
应用建议:在需要处理长时间运行的操作时,使用 APM 模式可以提高程序的响应性和性能。但需要注意正确使用
BeginX
和
EndX
方法,以及资源的清理。
多线程编程的最佳实践
为了编写高效、稳定的多线程程序,以下是一些最佳实践:
避免不必要的同步
同步操作会带来一定的开销,因此应尽量避免不必要的同步。可以使用线程本地存储等技术,为每个线程提供独立的变量实例,减少同步的需求。
合理使用线程池
线程池可以避免启动和销毁线程的高昂成本,同时避免创建过多线程导致系统性能下降。在使用线程池时,应根据实际情况调整线程池的大小,避免线程饥饿。
避免死锁
死锁是多线程编程中常见的问题,应采取措施避免死锁的发生。例如,确保线程按照相同的顺序获取锁,避免嵌套锁等。
资源清理
在多线程编程中,资源清理尤为重要。确保在使用完资源后及时释放,避免资源泄漏。可以使用
using
语句或实现
IDisposable
接口来管理资源。
错误处理
多线程程序中,错误处理更加复杂。应确保在每个线程中都有适当的错误处理机制,避免一个线程的异常影响其他线程的正常运行。
示例代码的流程图分析
以下是使用 mermaid 格式绘制的
System.Net.WebRequest
类调用 APM 下载网页的流程图:
graph TD;
A[开始] --> B[创建 WebRequest 对象];
B --> C[调用 BeginGetResponse 方法启动异步操作];
C --> D{异步操作完成?};
D -- 否 --> E[显示忙碌状态];
E --> D;
D -- 是 --> F[调用 EndGetResponse 方法获取响应];
F --> G[读取响应内容];
G --> H[格式化并输出内容长度];
H --> I[结束];
这个流程图展示了使用 APM 模式下载网页的主要步骤,包括异步操作的启动、等待完成、获取响应和处理结果。
总结
多线程编程是一个复杂但强大的技术领域,线程本地存储、定时器和异步编程模型(APM)是其中的重要组成部分。通过合理使用这些技术,可以提高程序的性能、响应性和可扩展性。在实际应用中,应根据具体的需求和场景选择合适的技术,并遵循最佳实践,确保程序的稳定性和可靠性。
希望本文的介绍能帮助你更好地理解和应用多线程编程技术,编写出高效、稳定的多线程程序。
| 技术 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 线程本地存储 | 避免同步开销,为每个线程提供独立状态 | 可能增加资源消耗 | 需要为每个线程维护独立状态,且同步开销较大的场景 |
| 定时器 | 实现定时任务 | 可能存在线程安全问题,不同定时器类有不同的适用范围 | 需要定时执行任务的场景,根据用户界面或服务器环境选择合适的定时器 |
| 异步编程模型(APM) | 提高程序响应性和性能 | 代码复杂度较高,需要正确处理资源清理和错误处理 | 处理长时间运行的操作,避免阻塞主线程 |
通过以上的表格,我们可以更清晰地对比不同技术的优缺点和适用场景,在实际开发中做出更合适的选择。
超级会员免费看
10万+

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



