CLR 基元线程同步构造web
《CLR via C#》到了最后一部分,这一章重点在于线程同步,多个线程同时访问共享数据时,线程同步能防止数据虽坏。之因此要强调同时,是由于线程同步问题其实就是计时问题。为构建可伸缩的、响应灵敏的应用程序,关键在于不要阻塞你拥有的线程,使它们能用于(和重用于)执行其余任务。编程
不须要线程同步是最理想的状况,由于线程同步存在许多问题:缓存
1 第一个问题是,它比较繁琐,很容易出错。安全
2 第二个问题是,它们会损坏性能。获取和释放锁是须要时间的,由于要调用一些额外的方法,并且不一样的CPU 必须进行协调,以决定哪一个线程先取得锁。让机器中的CPU 以这种方式互相通讯,会对性能形成影响。服务器
添加锁后速度会慢下来,具体慢多少要取决于所选的锁的种类。即使是最快的锁,也会形成 方法 数倍地慢于没有任何锁的版本。多线程
3 第三个问题在于,它们一次只容许一个线程访问资源。这是锁的所有意义之所在,但也是问题之所在,由于阻塞一个线程会形成更多的线程被建立。并发
线程同步如此的很差,应该如何在设计本身的应用时,尽可能避免线程同步呢?app
具体就是避免使用像静态字段这样的共享数据。可试着使用值类型,由于它们老是被复制,每一个线程操做的都是它本身的副本。异步
多个线程同时共享数据进行只读访问是没有任何问题的。async
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);
}