当你需要2个线程读写同一个数据时,就需要数据同步。线程同步的办法有:(1)原子操作;(2)锁。原子操作能够保证该操作在CPU内核中不会被“拆分”,锁能够保证只有一个线程访问该数据,其他线程在尝试获得有锁的数据时,会被拒绝,直到当前获得数据的线程将锁释放,其他线程才能够获得数据。

  • 为什么要线程同步?

  我们先看一个需要数据同步的例子,

static void Main(string[] args){
bool flag = false;
var t1 = new Thread(() => { if (flag) Console.WriteLine("Flag"); });
var t2 = new Thread(() => { flag = true; });
  t1.Start();
  t2.Start();
Console.ReadLine();
}

  上述例子中,t2线程将flag置为true,有可能发生:当t2打算执行flag = true时,t1执行了if(flag)语句,这造成了不可知的情况。此时就需要在t2执行时,若t1想要获取flag的值,要等到flag=true执行完成后,再执行,这就是所谓的“线程同步”,一个线程要等待另一个线程执行到某段代码后,再执行。线程同步能保证程序的执行符合“预想”--若t2没有执行,则flag为false,t2若已执行,则flag=true。线程同步是为了防止t2正在执行flag=true的时候,t1开始执行,此时flag应该是true,因为t2已经开始执行了,但是实际上flag=false,因为t2的flag=true没有执行完。解决的办法就是当t2执行flag=true时,将任何尝试读取flag的线程都阻塞,直到flag=true执行结束后,其他线程再执行。类似下面的代码。

var m_lock = GetSomeLock();
pulick void Go(){
  var t1 = new Thread(()=>Go1());
  var t2 = new Thread(()=>Go2());
  t1.Star();
  t2.Start();
}
public void Go1(){
m_lock.lock();
if (flag)
//dosomething;  
Console.WriteLine(flag);
   m_lock.Unlock();
}
public void Go2(){
m_lock.lock();
flag = true;
m_lock.Unlock();
}

在flag=true和if(flag)外面添加m_lock.lock()和m_lock.Unlock()就是为了保证线程同步。但是这样的同步带来的问题就是性能的下降,还有可能造成死锁。摘要中说过,线程同步有2个手段,上面介绍了锁,还有原子操作我没有介绍。在介绍原子操作之前,我介绍下关键字volatile。

  • 关键字volatile

  该关键字能够作用在变量前,其意义是对该变量的读写操作都是原子操作,这种特性被称作“易变性”。

  编译器在编译过程中,会根据代码的具体情况进行适当“优化”,例如:

public void Go(){
int value = * - * ;
for (int i = ; i < value; i++)
Console.WriteLine(i);
}

编译器在看到有地方调用该方法,会跳过其中的语句,因为这段语句毫无意义,这当然是好的,编译器弥补了我们的错误。但是有的时候这种优化会造成我们不想要的效果。

private static bool s_stopWorker = false;
static void Main(string[] args){
Console.WriteLine("Main:letting worker run for 5s");
var t = new Thread(Worker);
t.Start();
Thread.Sleep();
s_stopWorker = true;
Console.WriteLine("Main: waiting for worker to stop.");
t.Join();
}
private static void Worker(object o){
int x = ;
while (s_stopWorker) x++;
Console.WriteLine("Worker: stopped when x = {0}", x);
}

该段代码中,主线程阻塞5秒,然后s_stopWorker=true,本意是要中断t线程,让其显示数到的数后返回。但实际上编译器在看到while(s_stopWorker)时,又看到s_stopWorker在Worker方法中没有任何改变,因此该方法中对s_stopWorker的判断只会在最开始判断一次,若s_stopWorker=true,则进入死循环,若是false,则显示Worker stopped when x = 0之后该线程就返回了。若想实际看到运行效果,需要将改短代码放在.cs文件中,利用命令行编译该段代码。利用命令行编译代码要添加环境变量,变量的路径是C:\Windows\Microsoft.NET\Framework\v4.0.30319。然后就可以在命令行中编译该文件,注意要打开/platform:x86,其意义在《CLR via C#》29章中有解释,x86编译器比x64编译器更成熟,优化也更大胆。在命令行中输入 csc /platform:x86  你的cs文件的路径,之后在输入Program.exe(假设你的文件名字叫Program.cs),之后你会看到程序一直卡死在Main: waiting for worker to stop.之后一直没有出现数到的数字。

  下面来讨论如何解决这个问题。在System.Threading.Volatile中提供了2个静态方法,

public static class Volatile{
public static bool Read(ref bool location);
public static bool Write(ref bool location, bool value);
}

这两个方法能够阻止编译器对读和写进行优化,修改后的代码如下:

private static bool s_stopWorker = false;
static void Main(string[] args){
Console.WriteLine("Main:letting worker run for 5s");
var t = new Thread(Worker);
t.Start();
Thread.Sleep();
//防止优化
Volatile.Write(ref s_stopWorker, true);
Console.WriteLine("Main: waiting for worker to stop.");
t.Join();
Console.Read();
}
private static void Worker(object o){
int x = ;
//防止优化
while (Volatile.Read(ref s_stopWorker)) x++;
Console.WriteLine("Worker: stopped when x = {0}", x);
}

在s_stopWorker的读写处,都改用了Volatile类中的Read和Write方法。再次利用命令行编译该代码,会发现运行正常。很多时候我们搞不清到底该什么时候调用Volatile中的读写,什么时候该正常读写,于是C#提供了volatile关键字,该关键字能够保证对该变量的读写都是原子的,并且能够阻止对该方法进行优化。由于为了提高CPU的运行效率,现在的程序都是乱序执行,但是volatile能够保证该关键字之前的代码会在该关键字的变量读写时已经执行完成,该关键字修饰的变量以后的代码一定会在之后执行,而不会因乱序优化而在之前执行。我们去掉Volatile.Write和Read,然后将s_stopWorker前加上volatile关键字,运行上述代码,会发现结果正确。

  volatile关键字能够保证变量的线程安全,但是其缺点也是很明显的,将变量的每次读写都变成易变的读写,是对性能的浪费,因为这种情况极少发生。

volatile int m = ;
m=m+m;//volatile会阻止优化

通常,将一个变量增大一倍,只需要将该变量左移一位,就可以,但是volatile会阻止该优化。CPU会将m读入一个寄存器,然后读入另一个寄存器,然后在执行add,再将结果写入m。如果m不是int类型,而是更大的类型,则造成更大的浪费,如果在循环中,那真是杯具。

另外C#不支持将有volatile修饰的变量以引用的形式传入方法,如Int32.TryParse("123", m);会得到一个警告,对volatile字段的引用将不被视为volatile。

  • 变量捕获(闭包)

  第一段代码中,flag变量被lamda表达式包含。程序并没有在主线程中执行,而是在t1和t2中执行,该变量已经脱离了它的作用域,为了保证flag变量能够生效,编译器负责延长flag的生命周期,以保证在t1和t2线程执行时,该变量能够被访问,这就是变量捕获,也叫“闭包”,可以利用IL反编译器查看上述代码的IL指令来验证。

  上图可以看到为了保证flag的生命周期编译器将2个lamda表达式(b_0和b_1)和flag用一个类包了起来,这样这3个的生命周期就一致了。这很好,因为不需要我们去关心在t1和t2获取flag值时,flag是否有效,编译器已经帮我们全做了。

  本文讲了线程安全的必要性以及线程安全的手段之一:volatile(易变性),还简单介绍了变量捕获。线程安全的内容还没讲完,预计分3-4篇博客来讲线程安全。欢迎小伙伴在评论区与我交流。

C#多线程编程(5)--线程安全1的更多相关文章

  1. .NET面试题解析(07)-多线程编程与线程同步

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实是很多的,比如多线程编程.线程上下文.异步编程.线程同步构造.GUI的跨线程访问等等, ...

  2. .NET面试题解析(07)-多线程编程与线程同步 (转)

    http://www.cnblogs.com/anding/p/5301754.html 系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实 ...

  3. vc 基于对话框多线程编程实例——线程之间的通信

     vc基于对话框多线程编程实例——线程之间的通信 实例:

  4. Python中的多线程编程,线程安全与锁(二)

    在我的上篇博文Python中的多线程编程,线程安全与锁(一)中,我们熟悉了多线程编程与线程安全相关重要概念, Threading.Lock实现互斥锁的简单示例,两种死锁(迭代死锁和互相等待死锁)情况及 ...

  5. Python中的多线程编程,线程安全与锁(一)

    1. 多线程编程与线程安全相关重要概念 在我的上篇博文 聊聊Python中的GIL 中,我们熟悉了几个特别重要的概念:GIL,线程,进程, 线程安全,原子操作. 以下是简单回顾,详细介绍请直接看聊聊P ...

  6. C#多线程编程实例 线程与窗体交互

    C#多线程编程实例 线程与窗体交互 代码: public partial class Form1 : Form { //声明线程数组 Thread[] workThreads = ]; public ...

  7. Win32多线程编程(2) — 线程控制

    Win32线程控制只有是围绕线程这一内核对象的创建.挂起.恢复.终结以及通信等操作,这些操作都依赖于Win32操作系统提供的一组API和具体编译器的C运行时库函数.本篇围绕这些操作接口介绍在Windo ...

  8. Win32多线程编程(3) — 线程同步与通信

      一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线 ...

  9. Delphi 实现多线程编程的线程类 TThread

    http://blog.csdn.net/henreash/article/details/3183119 Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉 ...

  10. Java多线程编程(5)--线程间通信

    一.等待与通知   某些情况下,程序要执行的操作需要满足一定的条件(下文统一将其称之为保护条件)才能执行.在单线程编程中,我们可以使用轮询的方式来实现,即频繁地判断是否满足保护条件,若不满足则继续判断 ...

随机推荐

  1. Trie树/字典树题目(2017今日头条笔试题:异或)

    /* 本程序说明: [编程题] 异或 时间限制:1秒 空间限制:32768K 给定整数m以及n个数字A1,A2,..An,将数列A中所有元素两两异或,共能得到n(n-1)/2个结果,请求出这些结果中大 ...

  2. Atom Mac安装 有快捷方式

    https://jeffjade.com/2016/03/03/2016-03-02-how-to-use-atom/ 如何在 PyCharm 中使用 MacDown 作为外部编辑器 新编码神器Ato ...

  3. .NET 设计模式的六大原则理论知识

    1. 单一职责原则(SRP)(Single Responsibility Principle)2. 里氏替换原则(LSP)(Liskov Substitution Principle)3. 依赖倒置原 ...

  4. Hibernate 一对一中的一些问题

    1.对于想查询一对一种一方为空的时候使用 例如一个用户对应一个人,则要从人查找没有用户的人员的话, 使用hql语句是查询不到的 我今天也碰到了这个问题,研究了下,可以用以下语句查出来:from Per ...

  5. java6 - 面向对象编程思想

    一.学习大纲: 1. 类的理解:对现实事物的抽象表示 2. 行为与特征的理解:在类抽象过程中,通常把行为抽象成方法,把特征抽象成属性 3. 对象的理解:类的一个实例即是对象 4. Object 根类 ...

  6. Centos下Sphinx的下载与编译安装

    官方下载地址   http://sphinxsearch.com/downloads/release/ 百度云下载地址  https://pan.baidu.com/s/1gfmPbd5 wget  ...

  7. POJ - 3268 单源最短路

    题意:给定一些有向边,以及一个目的地,从某个点到达目的地,再从目的地回到那个点.共有n个点,问这n个点花费最大是多少? 思路:从目的地回去直接把目的地作为源点即可.那么从某个点到达目的地应该如何得到最 ...

  8. Fantasia (Tarjan+树形DP)

    Time Limit: 1000 ms   Memory Limit: 256 MB Description 给定一张N个点.M条边的无向图 $G$ .每个点有个权值Wi. 我们定义 $G_i$ 为图 ...

  9. Windows 窗体中的事件顺序(WinForm)

    引用MSDN,以便以后查看 引用:https://msdn.microsoft.com/zh-cn/library/86faxx0d.aspx 应用程序启动和关闭事件  Form 和 Control  ...

  10. CSS盒模型的深度思考及BFC

    本文最初发表于博客园,并在GitHub上持续更新前端的系列文章.欢迎在GitHub上关注我,一起入门和进阶前端. 以下是正文. 题目:谈一谈你对CSS盒模型的认识 专业的面试,一定会问 CSS 盒模型 ...