当你需要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. Flask-Moment----探索

    前言:  Flask-Moment在所有的flask扩展中算是相对简单的一个了,但是还是有很多需要理解的地方.那么今天就跟着笔者一起,来学习一下flask-moment在flask项目中的应用. 首先 ...

  2. MySQL备份常用命令总结

    MySQL备份常用命令总结 1.数据库和数据全部备份 mysqldump -uroot -pPassword -hlocalhost databasename > test.sqlmysqldu ...

  3. css实现隐藏多余溢出文字并显示省略号

    <meta charset="utf-8" /> <style> .txt{ width:200px; border:1px solid #ddd; ove ...

  4. 洛谷P2756飞行员配对方案问题 P2055假期的宿舍【二分图匹配】题解+代码

    洛谷 P2756飞行员配对方案问题 P2055假期的宿舍[二分图匹配] 飞行员配对方案问题 题目背景 第二次世界大战时期.. 题目描述 英国皇家空军从沦陷国征募了大量外籍飞行员.由皇家空军派出的每一架 ...

  5. Python自动化--语言基础7--操作日志、加密、发送邮件、线程、生产者消费者

    1.操作日志 logging.basicConfig:日志的统一处理器,对日志的输出格式和方式做配置日志级别等级CRITICAL > ERROR > WARNING > INFO & ...

  6. applicationContext.xml最基本配置文件

    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...

  7. js创建数组的三个方式

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. python资源推荐

    一.文档教程 1. 廖雪峰python教程 廖老师的教程我相信不用说了吧,每个学习python的人或多或少都听说过他,对我的帮助很大. 2.python中文学习大本营 名字叫做python中文学习大本 ...

  9. hihoCoder 1033 : 交错和 数位dp

    思路:数位dp,dp(i, j, k)表示考虑i位数,每位数可以任意取[0~9],并且这i位数的交错和为j,k=1表示前缀全是0(如000456),k=0表示前缀不为0.注意,前缀是否为0是这道题的一 ...

  10. 在SpringBoot中使用FluentValidator验证插件

    前言 在我们编写项目的时候,在controller中往往离不开对一些数据的校验.这里并不是说对于这些数据业务上面的校验,而是对这些数据进行空校验或者是长度校验等. 有些时候校验可以省略,根据业务的需要 ...