当你需要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. Java的一些良好习惯及细节------持续更新中...

    1.在做条件判断时,不要将变量放在判断符的左边,这样做可以防止出现空指针异常,以字符串比较为例: String name = "Tom"; //这种方式不推荐,如果变量name为空 ...

  2. mssql学习

    1.创建表和数据插入SQL 我们在开始创建数据表和向表中插入演示数据之前,我想给大家解释一下实时数据表的设计理念,这样也许能帮助大家能更好的理解SQL查询. 在数据库设计中,有一条非常重要的规则就是要 ...

  3. Mysql字符串截取总结:left()、right()、substring()、substring_index()

    同步首发:http://www.yuanrengu.com/index.php/20171226.html 在实际的项目开发中有时会有对数据库某字段截取部分的需求,这种场景有时直接通过数据库操作来实现 ...

  4. crack the coding interview

    crack the coding interview answer c++ 1.1 #ifndef __Question_1_1_h__  #define __Question_1_1_h__  #i ...

  5. python使用

    1. ipython 打印所有的输出变量 from IPython.core.interactiveshell import InteractiveShell InteractiveShell.ast ...

  6. Ubuntu忘记root密码怎么办?

    http://www.linuxidc.com/Linux/2016-05/131256.htm

  7. 2道acm简单题(2010):1.猜数字游戏;2.字符串提取数字并求和;

    //第一题是猜数字的游戏.//题目:随即产生一个3位的正整数,让你进行猜数字,//如果猜小了,输出:"猜小了,请继续".//如果猜大了,输出:"猜大了,请继续" ...

  8. dubbox系列【三】——简单的dubbox提供者+消费者示例

    1.dubbox-provider示例 在eclipse中建立maven project,名为provider-parent,包含两个maven medule:provider-api 和 provi ...

  9. hihoCoder1319 岛屿周长 (bfs)

    思路:从给定坐标开始bfs,将所有联通点标记,然后把每个联通点的四个方向都判断一下,如果这个方向相邻的是一个非联通点说明需要把这条边实在最外围,即周长的一部分. AC代码 #include <s ...

  10. 求第k小的数 O(n)复杂度

    思路:利用快速排序的思想,把数组递归划分成两部分.设划分为x,数组左边是小于等于x,右边大于x.关键在于寻找一个最优的划分,经过 Blum . Floyd . Pratt . Rivest . Tar ...