C#异步编程(二)用户模式线程同步

时间:2022-11-09 23:44:12

基元线程同步构造

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

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

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

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

综上所述,线程同步是一件不好的事情,所以在设计自己的应用程序时,应尽可能避免进行线程同步。具体就是避免使用像静态字段这样的共享数据。线程用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#异步编程(二)用户模式线程同步

这个方法比较特殊,他们事实上会禁止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方法调用之前执行;而这个调用之后的任何变量读取都在这个调用之后读取。

C#异步编程(二)用户模式线程同步

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);
}
}
}

执行结果

C#异步编程(二)用户模式线程同步

  构造一个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提供的方法来操作一切,确保代码以极快速度运行,同时并没有线程会被阻塞。

C#异步编程(二)用户模式线程同步

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&num;异步编程(三)内核模式线程同步

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

  2. C&num;多线程编程实战(二):线程同步

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

  3. C&num;异步编程(一)线程及异步编程基础

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

  4. C&num; 异步编程1 APM模式异步程序开发

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

  5. 【憩园】C&num;并发编程之异步编程&lpar;二&rpar;

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

  6. C&num;异步编程(五)异步的同步构造

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

  7. &lbrack; 高并发&rsqb;Java高并发编程系列第二篇--线程同步

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

  8. 异步编程模型&lpar;APM&rpar;模式

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

  9. JavaScript异步编程的Promise模式&lpar;转&rpar;

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

随机推荐

  1. linux 系统内核空间与用户空间通信的实现与分析&lt&semi;转&gt&semi;

    linux 系统内核空间与用户空间通信的实现与分析 2 评论: 陈鑫 (chen.shin@hotmail.com), *软件爱好者, 南京邮电学院电子工程系 2004 年 7 月 01 日 内容 ...

  2. 足球游戏论坛数据分析--简单粗暴的K均值聚类

    在<<足球游戏论坛数据分析--简单粗暴的贝叶斯>>中尝试了贴标签后,一直觉得结果无法接受, 慢慢回想, 其实选择的算法是错误的,原因有 论坛帖子分类并非就是PC/PS/XBOX ...

  3. SQL Server代理&lpar;4&sol;12&rpar;:配置数据库邮件

    SQL Server代理是所有实时数据库的核心.代理有很多不明显的用法,因此系统的知识,对于开发人员还是DBA都是有用的.这系列文章会通俗介绍它的很多用法. 在以前的文章里我们看到,SQL Serve ...

  4. iOS&colon; 实现苹果的内购

    一.介绍: 在个人开发的app上架到AppStore后,苹果官方允许我们将自己的app在appstore上进行付费使用,也就是所谓的内购.其中,支付方式规定的必须是苹果的支付方式:应用内支付. 二.流 ...

  5. struts2不兼容servlet、COS

    如果你在一个项目中使用了struts2,也就是说,你在web.xml中配置了如下代码: <filter> <filter-name>struts2</filter-nam ...

  6. &lbrack;转&rsqb; splice系列系统调用

    关注splice系列系统调用(包括splice,tee和vmsplice)已经有一段时间了,开始的时候并未能领会splice的意义所在,致使得出了“splice系列系统调用不怎么实用”的错误结论.随着 ...

  7. C&plus;&plus;类构造函数

    一.概述 类是一种用户自定义的类型,声明一个类对象时,编译程序要为对象分配存储空间,进行必要的初始化.在C++中,这项工作是由构造函数来完成的. 大部分对象在使用之前没有正确的初始化是C++出错的主要 ...

  8. java中public与private还有protect的区别

    java中public与private还有protect的区别 总是忘记.

  9. android 向webview传值

    android中可以使用WebView加载网页,同时Android端的java代码可以与网页上的javascript代码之间相互调用. 效果图: (一)Android部分: 布局代码: <spa ...

  10. O365 Manager Plus帮助台委派功能一览表

    O365 Manager Plus帮助台委派介绍 虽然Office 365允许您在全球任何地方工作,但它提供的管理功能十分不足.当一个组织分布在多个国家/地区时,一个管理员很难单独管理所有用户和邮箱. ...