CLR 线程同步
Posted mingjie-c
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CLR 线程同步相关的知识,希望对你有一定的参考价值。
CLR 基元线程同步构造
《CLR via C#》到了最后一部分,这一章重点在于线程同步,多个线程同时访问共享数据时,线程同步能防止数据虽坏。之所以要强调同时,是因为线程同步问题其实就是计时问题。为构建可伸缩的、响应灵敏的应用程序,关键在于不要阻塞你拥有的线程,使它们能用于(和重用于)执行其他任务。
不需要线程同步是最理想的情况,因为线程同步存在许多问题:
-
1 第一个问题是,它比较繁琐,很容易出错。
-
2 第二个问题是,它们会损坏性能。获取和释放锁是需要时间的,因为要调用一些额外的方法,而且不同的CPU 必须进行协调,以决定哪个线程先取得锁。让机器中的CPU 以这种方式互相通信,会对性能造成影响。
添加锁后速度会慢下来,具体慢多少要取决于所选的锁的种类。即便是最快的锁,也会造成 方法 数倍地慢于没有任何锁的版本。
-
3 第三个问题在于,它们一次只允许一个线程访问资源。这是锁的全部意义之所在,但也是问题之所在,因为阻塞一个线程会造成更多的线程被创建。
线程同步如此的不好,应该如何在设计自己的应用时,尽量避免线程同步呢?
-
具体就是避免使用像静态字段这样的共享数据。可试着使用值类型,因为它们总是被复制,每个线程操作的都是它自己的副本。
-
多个线程同时共享数据进行只读访问是没有任何问题的。
1 类库和线程安全
Microsoft 的 Framework Class Library (FCL)保证所有静态方法都是线程安全的。另一方面,FCL 不保证实列方法是线程安全的。Jeffery Richter 建议你自己的类库也遵循这个模式。这个模式有一点要注意:如果实例方法的目的是协调线程,则实例方法应该是线程安全的。
注意:使一个方法线程安全,并不是说它一定要在内部获取一个线程同步锁。线程安全的方法意味着在两个线程试图同时访问数据时,数据不会被破坏。例如:System.Math 类的一个静态方法 Max。
2 基元用户模式和 内核模式构造
基元(primitive)是指可以在代码中使用的最简单的构造。有两种基元构造:用户模式(user-mode)和 内核模式(kernel-mode)。尽量使用基元用户模式构造,它们的速度要显著快于内核模式构造。因为它们使用了特殊 CPU 指令来协调线程。这意味着协调是在硬件中发生的(所以才这么快)。
但这意味着 Windows 系统永远检测不到一个线程在基元用户模式的构造上阻塞了。由于在用户模式的基元构造上阻塞的线程池不认为已阻塞,所以线程池不会创建新的线程来替换这种临时阻塞的线程。此外,这些CPU 指令只阻塞线程相当短的时间。
3 用户模式构造
CLR 保证对以下数据类型的变量读写是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用类型。
举个列子:
internal static class SomeTyoe{
public static Int32 x = 0;
}
如果一个线程执行这一行代码:
SomeType.x = 0x01234567;
x 变量会一次性(原子性)地从0x00000000 变成0x01234567。另一个线程不可能看到处于中间状态的值。假定上述SomeType 类中的x 字段是一个Int64 ,那么当一个线程执行以下代码时:
SomeType.x = 0x0123456789abcdef
另一个线程可能查询x ,并得到0x0123456700000000 或 0x0000000089abcdef 值,因为读取和写入操作不是原子性的。
虽然变量的原子访问可保证读取或写入操作一次性完成,但由于编译器和CPU 的优化,不保证操作什么时候发生。本节讨论的基元用户模式构造,用于规划好这些原子性读取/写入操作的时间。 此外,这些构造还可强制对(U)Int64 和 Double 类型的变量进行原子性的、规划好了时间的访问。
有两种基于用户模式线程同步构造。
-
1 易变构造:在特定的时间,它在包含一个简单数据类型的变量上 执行 原子性的读 或 写操作。
-
2 互锁构造:在特定的时间,它在包含一个简单数据类型的变量上 执行 原子性的读 和 写操作。
所有易变 和 互锁构造都要求传递对包含简单数据类型的一个变量的引用(内存地址)。
3.1 易变构造 Volatile.Read 和 Volatile.Write
C# 对易变字段的支持
C# 编译器提供了 volatile 关键字,它可应用于以下任何类型的静态 或 实例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single和 Char。还可将 volatile 关键字应用于引用类型的字段,以及基础类型为 (S)Byte,(U)Int16,(U)Int32 的任何枚举字段。
JIT 编译器确保对易变字段的所有访问都是易变读取或写入的方式执行,不必显示调用 Volatile 的静态 Read 或 Write 方法。另外,volatile 关键字告诉C# 和 JIT 编译器不将字段缓存到CPU 的寄存器中,确保字段的所有读写操作都在 RAM 中进行。
下面是Volatile.Write 方法和 Volatile.Read 方法的使用。
internal sealed class ThreadsSharingData {
private Int32 m_flag = 0;
private Int32 m_value = 0;
// This method is executed by one thread
public void Thread1() {
// Note: 5 must be written to m_value before 1 is written to m_flag
m_value = 5;
Volatile.Write(ref m_flag, 1);
}
// This method is executed by another thread
public void Thread2() {
// Note: m_value must be read after m_flag is read
if (Volatile.Read(ref m_flag) == 1)
Console.WriteLine(m_value);
}
}
-
Volatile.Write 方法强迫location 中的值在调用时写入。此外,按照编码顺序,之前的加载和存储操作必须在调用 Volatile.Write 之前 发生。
-
Volatile.Read 方法强迫location 中的值在调用时读取。此外,按照编码顺序,之后的加载和存储操作必须在调用 Volatile.Read 之后 发生。
C# 对易变字段的支持
为了简化编程,C# 编译器提供了 Volatile 关键字,它可应用于以下任何类型的静态或实例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single 和 Char。还可以将 Volatile 关键字应用于引用类型的字段,以及基础类型为(S)Byte,(U)Int16 或 (U)Int32 的任何枚举字段。
volatile 关键字告诉 C# 和 JIT 编译器不将字段缓存到 CPU 的寄存器中,确保字段的所有读写操作都在 RAM 中进行。
用 volatile 引起的不好事情:
-
如:m_amount = m_amount + m_amount;
//假定m_amount 是类中定义的一个volatile 字段。编译器必须生成代码将m_amount 读入一个寄存器,再把它读入另一个寄存器,将两个寄存器加到一起,再将结果写回 m_amount 字段。但最简单的方式是将它的所有位都左移1 位。
-
另外,C# 不支持以引用的方式将 volatile 字段传给方法。
3.2 互锁构造
本节将讨论静态System.Threading.Interlocked 类提供的方法。InterLocked 类中的每个方法都执行一次原子读取 以及 写入操作。此外,Interlocked 的所有方法都建立了完整的内存栅栏(memory fence)。也就是说,调用某个 Interlocked 方法之前的任何变量写入都在这个InterLocked 方法调用之前执行。而这个调用之后的任何变量读取都在这个调用之后读取。
作者很喜欢用 Interlocked 的方法,它们相当快,不阻塞任何线程。
AsyncCoordinator 可协调异步操作。作者给了个例子。
internal sealed class MultiWebRequests {
// This helper class coordinates all the asynchronous operations
private AsyncCoordinator m_ac = new AsyncCoordinator();
// Set of web servers we want to query & their responses (Exception or Int32)
// NOTE: Even though multiple could access this dictionary simultaneously,
// there is no need to synchronize access to it because the keys are
// read•only after construction
private Dictionary<String, Object> m_servers = new Dictionary<String, Object> {
{ "http://Wintellect.com/", null },
{ "http://Microsoft.com/", null },
{ "http://1.1.1.1/", null }
};
public MultiWebRequests(Int32 timeout = Timeout.Infinite) {
// Asynchronously initiate all the requests all at once
var httpClient = new HttpClient();
foreach (var server in m_servers.Keys) {
m_ac.AboutToBegin(1);
httpClient.GetByteArrayAsync(server).
ContinueWith(task => ComputeResult(server, task));
}
// Tell AsyncCoordinator that all operations have been initiated and to call
// AllDone when all operations complete, Cancel is called, or the timeout occurs
m_ac.AllBegun(AllDone, timeout);
}
private void ComputeResult(String server, Task<Byte[]> task) {
Object result;
if (task.Exception != null) {
result = task.Exception.InnerException;
} else {
// Process I/O completion here on thread pool thread(s)
// Put your own compute•intensive algorithm here...
result = task.Result.Length; // This example just returns the length
}
// Save result (exception/sum) and indicate that 1 operation completed
m_servers[server] = result;
m_ac.JustEnded();
}
// Calling this method indicates that the results don‘t matter anymore
public void Cancel() { m_ac.Cancel(); }
// This method is called after all web servers respond,
// Cancel is called, or the timeout occurs
private void AllDone(CoordinationStatus status) {
switch (status) {
case CoordinationStatus.Cancel:
Console.WriteLine("Operation canceled.");
break;
case CoordinationStatus.Timeout:
Console.WriteLine("Operation timed•out.");
break;
case CoordinationStatus.AllDone:
Console.WriteLine("Operation completed; results below:");
foreach (var server in m_servers) {
Console.Write("{0} ", server.Key);
Object result = server.Value;
if (result is Exception) {
Console.WriteLine("failed due to {0}.", result.GetType().Name);
} else {
Console.WriteLine("returned {0:N0} bytes.", result);