基元线程同步构造

  多个线程同时访问共享数据时,线程同步能防止数据损坏。不需要线程同步是最理想的情况,因为线程同步存在许多问题。

第一个问题就是它比较繁琐,而且很容易写错。

第二个问题是,他们会损害性能。获取和释放锁是需要时间的。

第三个问题是,他们一次只允许一个线程访问资源,就可能导致其他线程被阻塞,使用多线程是为了提高效率,而阻塞无疑降低了你的效率。

综上所述,线程同步是一件不好的事情,所以在设计自己的应用程序时,应尽可能避免进行线程同步。具体就是避免使用像静态字段这样的共享数据。线程用new操作符构造对象时,new操作符会返回新对象的引用,如果能避免将这个引用传给可能同时使用对象的另一个线程,就不必同步对该对象进行访问。可试着使用值类型,因为他们总是被赋值,每个我线程操作的都是它自己的副本。最后,多个线程同时对共享数据进行制度访问是没有任何问题的。

基元用户模式和内核模式构造

基元(primitive)是指可以在代码中使用的最简单的构造。有两种基元构造:用户模式(user mode)和内核模式(kernel mode)。

  用户模式构造

  应尽量使用基元用户模式构造,他们的速度要显著快于内核模式的构造。这是因为他们使用了特殊cpu指令来协调线程。这意味着协调实在硬件中发生的(所以才这么块)。但这也意味着windows操作系统永远检测不到一个线程在纪元用户模式的构造上阻塞了。由于在用户模式的基元构造上阻塞的线程池线程永远不认为已阻塞,所以线程池不会创建新的线程来替换这种临时阻塞的线程。此外,这些cpu指令只阻塞相当短的时间。

这也是我认为较好的构造方式,CLR Via C# 的作者Jeffrey Richter也建议尽量使用用户模式。但用户模式也有一个缺点:只有windows操作系统内核才能停止一个线程的运行(防止它浪费cpu时间)。在用户模式中运行的线程可能被系统抢占(preempted),想要取得资源但暂时无法取到的线程会一直在用户模式中“自旋”。这回浪费大量cpu时间。

  内核模式构造

内核模式的构造是是由windows操作系统自身提供的。所以,他们要求在应用程序的线程中调用由操作系统内核实现的函数。将线程从用户模式切换到内核模式(或相反)会导致巨大的性能损失,这正式为什么要避免使用内核模式构造的原因。但它们有一个重要的有点:线程通过内核模式的构造获取其他线程拥有的资源时,windows会阻塞线程以避免它浪费cpu时间。当资源变得可用时,windows会恢复线程,允许它访问资源。

对于一个等待的线程,如果不释放它,它就一直阻塞。如果是用户模式,线程将一直在cpu上运行,我们称为“活锁”。如果是内核模式,线程将一直阻塞,我们称为“死锁”。两种情况都不好,但在两者之间,死锁总是优于活锁,因为活锁既浪费cpu时间,又浪费内存(线程栈等),而死锁只浪费内存。

  理想中的模式

构造应该兼具上面两种模式的长处。也就是说,在没有竞争的情况下,应该快而不会阻塞(用户模式)。但如果存在竞争,我希望它被操作系统内核阻塞。这种构造,我们称为混合构造(hybrid construct)。应用程序使用混合构造是很常见的现象。

用户模式构造

  易变字段 VOLATILE

静态system.threading.volatile类提供了两个静态方法

这个方法比较特殊,他们事实上会禁止c#编译器、jit编译器和cpu平常执行的一些优化。下面描述了这些方法是如何工作的。

1       Volatile.Write方法强迫location中的值在调用时写入。此外,按照编码顺序,之前的加载和存储操作必须在调用Volatile.Write之前发生

2       Volatile.Read方法强迫location中的值在调用时读取。此外,按照编码顺序,之后的加载和存储操作必须必须在调用Volatile.Read之后发生。

这样会避免编译器对你的代码进行了过度的优化,提前赋值数据。当然,你也可以使用volatile关键字,不过我并不喜欢这么做,因为大多时候,你的读取或写入顺序都可以按照正常方式进行,这样效率更高。你可以在用必要的时候显示调用Volatile类的方法,这样程序的性能更好。

  互锁构造 interlocked

  volatile的read方法执行一次原子性的读取操作,write方法执行一次原子性的写入操作。本节我们讨论静态system.threading.interlocked类提供的方法。interlocked类中的每个方法都执行一次院子读取以及写入操作。此外,interlocked的所有方法都建立了完整的内存栅栏(memory fence)。换言之,调用某个interlocked方法之前的任何变量写入都在这个interlocked方法调用之前执行;而这个调用之后的任何变量读取都在这个调用之后读取。

interlocked方法的运行速度相当快,而且能做不少事情。下面我们有一个简单的例子,使用interlocked方法异步查询几个web服务器,并同时处理返回数据。代码很短,而且不阻塞任何线程,而且使用线程池来实现自动伸缩。

internal enum CoordinationStatus
{
AllDone,
Timeout,
Cancel
}
internal sealed class MultiWebRequests
{
//这个辅助类用于协调所有异步操作
private AsyncCoordinator m_ac = new AsyncCoordinator(); //这是想要查询的web服务器及其响应(异常或int32)的集合
//注意:多个线程访问该字典不需要以同步方式进行
//因为构造后键就是只读的
private Dictionary<String, Object> m_servers = new Dictionary<string, object>
{
{"https://www.baidu.com/" ,null},
{"https://www.microsoft.com/zh-cn/",null},
{"https://www.taobao.com/",null}
};
public MultiWebRequests(Int32 timeout=Timeout.Infinite)
{
var httpClient = new HttpClient();
foreach (var server in m_servers.Keys)
{
m_ac.AboutToBegin();
httpClient.GetByteArrayAsync(server).ContinueWith(task => ComputeResult(server, task));//task是Task<byte[]>类型
} //告诉AsyncCoordinator所有操作都已发起,并在所有操作完成
//调用cancel或者发生超时的时候调用AllDone
m_ac.AllBegun(AllDone, timeout);
}
//将结果保存到集合中,然后将完成状态进行通知
private void ComputeResult(string server, Task<Byte[]> task)
{
object result;
if (task.Exception!=null)
{
result = task.Exception.InnerException;
}
else
{
//线程池线程处理I/O完成
//在此添加自己的计算密集型算法。。。。
result = task.Result.Length;
}
//保存结果(exception/sum),指出一个操作完成
m_servers[server] = result;
m_ac.JustEnded();
} //调用这个方法指出结果已无关紧要
public void Cancel()
{
m_ac.Cancel();
} //所有web服务器都响应、调用了cancel或者发生超时,就调用该方法,显示执行结果
private void AllDone(CoordinationStatus status)
{
switch (status)
{
case CoordinationStatus.Cancel:
Console.WriteLine("operation canceled");
break;
case CoordinationStatus.Timeout:
Console.WriteLine("operation time-out");
break;
case CoordinationStatus.AllDone:
Console.WriteLine("operation completed;results below;");
foreach (var server in m_servers)
{
Console.WriteLine("{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} bytes",result);
}
}
break;
}
}
}

可以看出,上述代码并没有直接使用interlocked的任何方法,因为我将所有协调代码都放到可重用的AsyncCoordinator类中。如下

internal sealed class AsyncCoordinator
{
//AllBegun内部调用justended来递减它
private Int32 m_opCount = ;
//0 = false 1= true
private Int32 m_statusReported = ;
private Action<CoordinationStatus> m_callback;
private Timer m_timer;
//该方法必须在发起一个操作之前调用
public void AboutToBegin(Int32 opsToAdd=)
{
//返回的是计算之后的m_opCount的值
Interlocked.Add(ref m_opCount, opsToAdd);
} //该方法必须在处理好一个操作的结果之后调用
public void JustEnded()
{
if (Interlocked.Decrement(ref m_opCount)==) //返回的是计算之后的m_opCount的值
{
ReportStatus(CoordinationStatus.AllDone);
}
} //该方法必须在发起所有操作之后调用
public void AllBegun(Action<CoordinationStatus> callback,Int32 timeout=Timeout.Infinite)
{
m_callback = callback;
if (timeout!=Timeout.Infinite)
{
m_timer = new Timer(TimeExpired, null, timeout, Timeout.Infinite);
}
//相当于多减了一次,对冲初始化把m_opCount设置为1的多出来的1
JustEnded();
} private void TimeExpired(object o)
{
ReportStatus(CoordinationStatus.Timeout);
} public void Cancel()
{
ReportStatus(CoordinationStatus.Cancel);
} private void ReportStatus(CoordinationStatus status)
{
//这个用来判断状态是否是从未报告过;只有第一次调用这个方法的状态才会被记录
if (Interlocked.Exchange(ref m_statusReported,)==)//这个将m_statusReported的值变为1,并返回m_statusReported原有的值
{
m_callback(status);
}
}
}

执行结果

  构造一个MultiWebRequests时,会先初始化一个AsyncCoordinator和包含了一组服务器uri的字典。然后,它以异步方式一个接一个地发出所有web请求。为此,他首先调用AsyncCoordinator的AboutToBegin方法,想他传递要发出的请求数量(这里也可以一次把所有要执行请求的数量发给AboutToBegin)。然后,他调用httpClient.GetByteArrayAsync(server)初始化请求,这回返回一个task,ContinueWith执行computeResult方法,它可以并发处理结果。所有请求都发出后,将调用AsyncCoordinator的AllBegun方法,向他传递要在所有操作完成后执行的方法(AllDone)以及一个超时值。每收到一个响应,线程池都会调用computeResult进行后续处理任务,computeResult保存请求结果之后最后会调用JustEnded,使AsyncCoordinator知道一个对象已经执行完成。

JustEnded方法判断出所有任务都已经执行完成后,会调用回调(AllDone)处理来自所有web服务器的结果。执行AllDone方法的线程就是获取最后一个web服务器响应的那个线程池线程。但如果发生超时或者调用cancel方法的那个线程,调用AllDone的线程就是向asyncCoordinator通知超时的那个线程池线程,或者调用cancel方法的那个线程。

注意,这里存在竞态条件,因为以下事情可能恰好同时发生:所有web服务器请求完成、调用Allbegun、发生超时以及调用cancel。这时,AsyncCoordinator会选择1个赢家和3个输家,确保alldone不被多次调用。赢家是通过传给AllDone的status实参来识别的。

AsyncCoordinator类封装了所有线程协调逻辑。他用interlocked提供的方法来操作一切,确保代码以极快速度运行,同时并没有线程会被阻塞。

AsyncCoordinator类最重要的字段就是m_opCount,用于跟踪仍在进行的一步操作的数量。每个异步操作开始前都会调用AboutToBegin。该方法调用interlocked.Add,以院子方式将传给它的数字加到m_opCount字段上,m_opCount上的运算必须以原子方式进行。处理好web服务器的响应之后会调用justEnded,该方法调用interlocked.Decerment,以院子方式从m_opCount上减1.当opCount等于0时,由这个线程调用ReportStatus。

注意:m_opCount字段初始化为1(而非0),这一点很重要。执行构造器方法的线程在发出web服务器请求期间,由于m_opCount字段位1,所以能保证AllDone不会被调用。构造器调用AllBegun之前,m_opCount永远不可能变为0。构造器调用allBegun时,会执行一次justEnded方法来递减m_opCount,所以事实上撤掉了把它初始化为1的效果。

  实现简单的自旋锁

Interlocked的方法很好用,但是主要用于操作Int值。如果需要原子性地操作类对象中的一组字段,又该怎么办呢?在这种情况,需要采取一个办法阻止所有线程,只允许其中一个进入对字段进行操作的。可以使用Interlocked的方法构造一个线程同步块。

internal struct SimpleSpinLock
{
private Int32 m_ResourceInUse;// 0=false(默认) 1 =true
public void Enter()
{
while (true)
{
//总是将资源设为“正在使用”(1)
//只有从“未使用”编程“正在使用”才会返回
if (Interlocked.Exchange(ref m_ResourceInUse,)==)
{
return;
}
//在这里添加“黑科技”
}
}
public void Leave()
{
//将资源标记为“未使用”
Volatile.Write(ref m_ResourceInUse, );
}
}

下面这个类展示了如何使用SimpleSpinLock

public sealed class SomeResource
{
private SimpleSpinLock m_sl = new SimpleSpinLock();
public void AccessResource()
{
m_sl.Enter();
//一次只有一个线程才能进入这里访问资源
m_sl.Leave();
}
}

这个锁很简单,他的最大问题是会造成线程“自旋”,自旋会浪费cpu时间。

  SpinLock是.net已经实现的自旋锁,他和我们前面举例的SimpleSpinLock类似,只是使用了spinwait结构来增强性能(SpinWait在自旋中加入sleep方法,使他在一段时间内不占用cpu时间),还增加了超时支持。

这篇我们暂时介绍以上概念,下篇文字我们一起了解内核模式。

C#异步编程(二)用户模式线程同步的更多相关文章

  1. C#异步编程(三)内核模式线程同步

    其实,在开发过程中,无论是用户模式的同步构造还是内核模式,都应该尽量避免.因为线程同步都会造成阻塞,这就影响了我们的并发量,也影响整个应用的效率.不过有些情况,我们不得不进行线程同步. 内核模式 wi ...

  2. C#多线程编程实战(二):线程同步

    2.1 简介 竞争条件:多个线程同时使用共享对象.需要同步这些线程使得共享对象的操作能够以正确的顺序执行 线程同步问题:多线程的执行并没有正确的同步,当一个线程执行递增和递减操作时,其他线程需要依次等 ...

  3. C#异步编程(一)线程及异步编程基础

    最近试着做了几个.NET CORE的demo,看了些源码,感觉异步编程在Core里面已经成为主流,而对这块我还没有一个系统的总结,所以就出现了这篇文字,接下来几篇文章,我会总结下异步编程的思路,主要参 ...

  4. C# 异步编程1 APM模式异步程序开发

    C#已有10多年历史,单从微软2年一版的更新进度来看活力异常旺盛,C#中的异步编程也经历了多个版本的演化,从今天起着手写一个系列博文,记录一下C#中的异步编程的发展历程.广告一下:喜欢我文章的朋友,请 ...

  5. 【憩园】C#并发编程之异步编程(二)

    写在前面 前面一篇文章介绍了异步编程的基本内容,同时也简要说明了async和await的一些用法.本篇文章将对async和await这两个关键字进行深入探讨,研究其中的运行机制,实现编码效率与运行效率 ...

  6. C#异步编程(五)异步的同步构造

    异步的同步构造 任何使用了内核模式的线程同步构造,我都不是特别喜欢.因为所有这些基元都会阻塞一个线程的运行.创建线程的代价很大.创建了不用,这于情于理说不通. 创建了reader-writer锁的情况 ...

  7. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  8. 异步编程模型(APM)模式

    什么是APM .net 1.0时期就提出的一种异步模式,并且基于IAsyncResult接口实现BeginXXX和EndXXX类似的方法. .net中有很多类实现了该模式(比如HttpWebReque ...

  9. JavaScript异步编程的Promise模式(转)

    异步模式在web编程中变得越来越重要,对于web主流语言Javascript来说,这种模式实现起来不是很利索,为此,许多Javascript库(比如 jQuery和Dojo)添加了一种称为promis ...

随机推荐

  1. PsySH——PHP交互式控制台

    PsySH PsySH is a runtime developer console, interactive debugger and REPL for PHP. PsySH是一个PHP的运行时开发 ...

  2. Android相机实时自动对焦的完美实现

    https://zhidao.baidu.com/question/873328177698804372.html Android相机实时自动对焦的完美实现 http://blog.csdn.net/ ...

  3. JavaScript笔记04——事件与回调

    1.在浏览器中,大多数代码都是由事件驱动的(event-driven). 这和生物中的神经反射有点类似. 比如说,谷歌页面上的一个按钮, 当我们“按下”这个按钮的时候,将跳出如下界面. 那么你有没想过 ...

  4. 初识python---简介,简单的for,while&if

    一编程语言:编程语言是程序员与计算机沟通的介质: 编程语言的分类:  1机器语言:是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合.           优点:灵活,直接执行和速度快   ...

  5. get_called_class--后期静态绑定("Late Static Binding")类的名称

    get_called_class--后期静态绑定("Late Static Binding")类的名称 string get_called_class ( void ) 获取静态方 ...

  6. etcd 安装部署

    etcd 是coreos团队开发的分布式服务发现键值存储仓库. github地址: https://github.com/coreos/etcd 安装: 1.下载etcd最新版本 https://gi ...

  7. linux crontab使用

    1.查看.编辑和删除 cron把命令行保存在crontab(cron table)文件里,这个文件通常在 /etc 目录下. 每个系统用户都可以有自己的crontab(在 /var/spool/cro ...

  8. mybaties mapping中if

    mapping中 if的简单使用 <insert id="addPassenger" resultMap="EmpResultMap" parameter ...

  9. SpringMVC的AJAX请求报406错误

    SpringMVC的AJAX请求报406错误原因有两种:1.jackson包没有引入 2.如果已经引入jackson包了还报406的错误,那么就有可能是请求的url路径是.html结尾,但是返回的数据 ...

  10. 【bzoj4401】块的计数(水dfs)

    题目传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=4401 假设把树划分为x个节点作一块,那么显然只有当x|n的时候才可能存在划分方案,并且这 ...