概述
使用多线程,线程安全是我们必须考虑的一个问题,而且我们都知道线程安全的关键点有三个
原子性:一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行
可见性:多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
有序性:即程序的执行顺序按照代码的先后顺序来执行。
那么,什么样的线程叫做不安全的呢?先从代码层面看一个例子
//定义一个公共变量
public int count = 0;
public int getCount(int count)
{
count++;
return count;
}
一个简单的把公共变量加1的例子,在单线程来说,这段代码没有任何问题,但是在多线程中可能会出现这种情况:
线程A调用了getCount方法,还没来得及给count+1返回,线程B也调用了此方法,此时获取的count的值和线程A获取的值是一样的,然后线程A中+1返回的和线程B+1返回的值相同,那么我们称这段代码是不安全的。
在并发编程中,这种由于不恰当的执行顺序而导致不正确的结果我们必须要考虑情况,它有一个正式的名字,叫“竞态条件”。竞态条件就是 当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件,最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观察结果来决定下一步的动作。比如首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这个期间创建了文件X),从而导致各种问题(未预期的异常、数据覆盖、文件被破坏等)。
至于如何保证线程安全,我们首先想到的可能是锁。
在java中,提供了一种内置的锁机制来支持原子性,即同步代码块(synchronized block),同步代码快包括两部分,一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。每个Java对象都可以用作一个实现同步的锁,这些所被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,这里要说,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。值得一提的是,内置锁相当于一种互斥锁,意味着最多只有一个线程能持有这种锁。要注意了,任何一个执行同步代码块的线程,都不能看到有其他线程正在执行由同一锁保护的同步代码块(这里涉及到可见性的问题)。synchronized直接加在方法上,虽然能很方便的解决线程安全的问题,但是也会带来性能低下的问题,所以synchronized怎么使用,也是值得深入学习和研究的。
然而在.NET中,也提供了一种保证原子性操作的变量 Interlocked,它提供了几个方法:
//替换usingResource为1,返回原始值
Interlocked.Exchange(ref usingResource, 1);
//usingResource增加4
Interlocked.Add(ref usingResource, 4);
//比较替换。如果值usingResource为4 则替换为10
Interlocked.CompareExchange(ref usingResource, 10, 4);
//usingResource--
Interlocked.Decrement(ref usingResource);
//usingResource++
Interlocked.Increment(ref usingResource);
我们来举个例子看它是如何实现原子性的
//一种否认重入的简单方法。多线程争用时只有一个线程执行进来。
static bool UseResource()
{
//返回0则进入方法执行
if (0 == Interlocked.Exchange(ref usingResource, 1))
{
Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
//访问非线程安全资源的代码将在这里。
//模拟一些工作
//Thread.Sleep(500);
Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
//释放锁,重新设置为0
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else
{
Console.WriteLine("
{0} was denied the lock", Thread.CurrentThread.Name);
return false;
}
}
除了锁机制外,高版本的C#中加入了async和await方法来保证线程安全,如下所示:
public static class AsynAndAwait
2
{
3
//step 1
4
private static int count = 0;
5
//用async和await保证多线程下静态变量count安全
6
public async static void M1()
7
{
8
//async and await将多个线程进行串行处理
9
//等到await之后的语句执行完成后
10
//才执行本线程的其他语句
11
//step 2
12
await Task.Run(new Action(M2));
13
Console.WriteLine("Current Thread ID is {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
14
//step 6
15
count++;
16
//step 7
17
Console.WriteLine("M1 Step is {0}", count);
18
}
19
20
public static void M2()
21
{
22
Console.WriteLine("Current Thread ID is {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
23
//step 3
24
System.Threading.Thread.Sleep(3000);
25
//step 4
26
count++;
27
//step 5
28
Console.WriteLine("M2 Step is {0}", count);
29
}
30 }
至于可见性,可以用 volatile 关键字修饰,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。举例来说就是
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,则编译器会逐一地进行编译并产生相应的机器代码(产生四条代码)。
您只能在有限的一些情形下使用 volatile变量替代锁。要使 volatile变量提供理想的线程安全,必须同时满足下面两个条件:
● 对变量的写操作不依赖于当前值。
● 该变量没有包含在具有其他变量的不变式中。
后续此文章还会继续完善,另外如有错误希望各位能够帮忙指正~~~
最后
以上就是顺利烧鹅为你收集整理的多线程-线程安全的全部内容,希望文章能够帮你解决多线程-线程安全所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复