C#多线程编程(5)--线程安全1
当你需要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的更多相关文章
- .NET面试题解析(07)-多线程编程与线程同步
系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实是很多的,比如多线程编程.线程上下文.异步编程.线程同步构造.GUI的跨线程访问等等, ...
- .NET面试题解析(07)-多线程编程与线程同步 (转)
http://www.cnblogs.com/anding/p/5301754.html 系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实 ...
- vc 基于对话框多线程编程实例——线程之间的通信
vc基于对话框多线程编程实例——线程之间的通信 实例:
- Python中的多线程编程,线程安全与锁(二)
在我的上篇博文Python中的多线程编程,线程安全与锁(一)中,我们熟悉了多线程编程与线程安全相关重要概念, Threading.Lock实现互斥锁的简单示例,两种死锁(迭代死锁和互相等待死锁)情况及 ...
- Python中的多线程编程,线程安全与锁(一)
1. 多线程编程与线程安全相关重要概念 在我的上篇博文 聊聊Python中的GIL 中,我们熟悉了几个特别重要的概念:GIL,线程,进程, 线程安全,原子操作. 以下是简单回顾,详细介绍请直接看聊聊P ...
- C#多线程编程实例 线程与窗体交互
C#多线程编程实例 线程与窗体交互 代码: public partial class Form1 : Form { //声明线程数组 Thread[] workThreads = ]; public ...
- Win32多线程编程(2) — 线程控制
Win32线程控制只有是围绕线程这一内核对象的创建.挂起.恢复.终结以及通信等操作,这些操作都依赖于Win32操作系统提供的一组API和具体编译器的C运行时库函数.本篇围绕这些操作接口介绍在Windo ...
- Win32多线程编程(3) — 线程同步与通信
一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线 ...
- Delphi 实现多线程编程的线程类 TThread
http://blog.csdn.net/henreash/article/details/3183119 Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉 ...
- Java多线程编程(5)--线程间通信
一.等待与通知 某些情况下,程序要执行的操作需要满足一定的条件(下文统一将其称之为保护条件)才能执行.在单线程编程中,我们可以使用轮询的方式来实现,即频繁地判断是否满足保护条件,若不满足则继续判断 ...
随机推荐
- Selenium_Java版本安装及初试
[环境] ①JDK版本:jdk1.8.0_73 ②Eclipse:jee-mars-4.5.2 ③Selenium:selenium-java-3.5.3 ④GoogleChrome:60 ⑤chro ...
- Java常用日志框架介绍
Java常用日志框架介绍 java日志概述 对于一个应用程序来说日志记录是必不可少的一部分.线上问题追踪,基于日志的业务逻辑统计分析等都离不日志.java领域存在多种日志框架,目前常用的日志框架包括L ...
- mysql 获取上个月,这个月的第一天或最后一天
/*上个月今天的当前时间*/select date_sub(now(),interval 1 month) /*上个月今天的当前时间(时间戳)*/select UNIX_TIMESTAMP(date_ ...
- .NET常用第三方库(包)总结
文章会不定期更新,以下内容均为个人总结,欢迎各位拍砖指正 序列化与反序列化 JSON.NET应该是.NET平台上使用最为广泛的序列化/反序列化包了,ASP.NET和ASP.NET Core中默认序列化 ...
- php环境下所有的配置文件以及作用
以下主要是针对linux下的目录(windows也是一样,文件名都一样) Apache:etc/httpd.conf PHP:etc/php.ini (Apache 正在运行的 PHP 版本) M ...
- Java经典编程题50道之九
一个数如果恰好等于它的因子之和,这个数就称为"完数".例如6=1+2+3.编程找出1000以内的所有完数. public class Example09 { public s ...
- C语言_简单了解一下typedef
作为一名PHPer,了解一下C还是有必要的,只是做一个简单的了解,因为并没有做开发C的想法. 关于typedef的详细说明,网上搜过了很多帖子,这篇算是最详细的了:http://blog.csdn.n ...
- Shell脚本的颜色样式及属性控制
首先看一下格式 echo -e "\033[字背景颜色:文字颜色m字符串\033[0m" 举例 echo -e "\033[41;36m 字体 \033[0m" ...
- Python实现二分查找
老生常谈的算法了. #!/usr/bin/python # -*- coding:utf-8 -*- # Filename: demo.py # 用python实现二分查找 def binarySea ...
- codeforce-748A
简单判断一下就行. AC代码: #include<cstdio> int main(){ int n,m,k; while(scanf("%d%d%d",&n, ...