今天被GF问到一个问题,某个函数在多线程环境中,会不会有冲突。在帮她解答这个问题的过程中,发现很多人对线程冲突和线程安全的理解并不是很全面,所以萌发了写这么一篇文章的想法,也算是对GF的问题的一个完整的解答。
水平有限,如有错漏指出敬请指出。
一、什么是线程冲突
线程冲突其实就是指,两个或以上的线程同时对同一个共享资源进行操作而造成的问题。
一个比较经典的例子是,用一个全局变量做计数器,然后开N个线程去完成某个任务,每个线程完成一次任务就将计数器加一,直到完成100次任务。如果不考虑线程冲突问题,用类似下面的代码去做,则很可能会超额完成任务,线程越多,完成任务次数超出100次的可能性就越大。
伪代码如下:
int count = 0;//全局计数器
void ThreadMethod()//运行在每个线程的方法
{
while( true )
{
if ( count >= 100 )//如果达到任务指标
break;//中断线程执行
DoSomething();//完成某个任务
count++;
}
}
//省略线程的创建等代码。
具体的,为什么会超额完成任务的原因在这里我就不赘述了,这个例子在单线程环境中是绝对不会超额完成任务的。
当然,在这个例子中,将count++放到if语句中,也许能降低一些事故发生的概率,但那不是绝对的,换言之这样的程序不能杜绝超额完成任务的可能。
其实从线程冲突的定义中我们不难发现,要造成线程冲突有两个必要条件:多线程和共享资源。这两个条件中有一个不成立,就不可能发生线程冲突问题。
所以,在单线程环境中,是不存在线程冲突的问题的。不过很可惜的是,我们的软件早已进化到了多进程多线程的时代,单线程的程序几乎是不存在的,无论是WinForm还是WebForm,程序运行的环境都是多线程的,而不论你自己是不是明确的开启了一个线程。
既然多线程是不可避免的,那么要避免线程冲突就只能从共享资源来开刀了。
二、线程安全的资源
如果大家经常看MSDN或者VS帮助中的.NET类库参考的话,就不难发现几乎所有的类型都有这么一句话的描述:“此类型的任何公共 static(在 Visual Basic中为 Shared) 成员都是线程安全的。但不保证所有实例成员都是线程安全的。”那么线程安全到底是什么意思?
其实线程安全很简单,就是指一个函数(方法、属性、字段或者别的)在同一时间被不同线程使用,不会造成任何线程冲突的问题。就说这个东西是线程安全的。
接下来来谈谈什么样的资源是线程安全的。
之所以使用资源这个词,是因为线程冲突不仅仅会发生在共享的变量上,两个线程同时对同一个文件进行读写,两个程序同时用同一个端口与同一个地址进行通信,都会造成线程冲突。只不过是操作系统和帮我们协调了这些冲突而已。
一个线程安全的资源即是指,在不同线程中使用不会导致线程冲突问题的资源。
一个不能被改变的资源是线程安全的,比如说一个常量:
const decimal pai = 3.14159265;
//C++: const double pai = 3.14159265;
因为pai的值不可能被改变,所以在不同的线程中使用也不会造成冲突。换言之它在不同的线程中同时被使用和在一个线程中被使用是没有区别的,所以这个东西是线程安全的。
同样的,在.NET中,一个字符串的实例也是线程安全的,因为字符串的实例在.NET中也是不可以被改变的。一个字符串的实例一旦被创建,对其所有的属性、方法调用的结果都是唯一确定的,永远不会改变的。所以.NET类库参考中String类型才有:“此类型是线程安全的。”,与之类似的Type类型、Assembly类型,都是线程安全的。
但string的实例是线程安全的,却不代表string的变量是线程安全的,换言之,假设有一个静态变量:
public static string str = “123”;
str不是线程安全的,因为str这个变量的字符串实例可以被任何线程修改。
再考虑这样的例子:
public static readonly SqlConnection connection = new SqlConnection( “connectionString” );
虽然connection本身虽然是线程安全的,但connection的任何成员都不是线程安全的。
比如说,我在一个线程中对这个connection调用了Open方法,然后进行查询操作。但在同一时刻,另一个线程调用了Close方法,这时候,就出现错误了。
但,单纯的使用connection而不使用其任何成员,比如说if ( connection != null )这样的代码,是不存在线程冲突的。
线程安全的资源其实还有很多,在此不一一赘述。
对于.NET Framework的类型的成员来说,只读的字段是线程安全的。
那么对于属性和方法来说,怎么知道是不是线程安全的?
三、线程安全的函数
因为属性和方法都是函数组成的,所以我们探讨一下什么是线程安全的函数。
上面我们说到,线程冲突的必要条件是多线程和共享资源。那么如果一个函数里面没有使用任何可能共享的资源,那么就不可能出现线程冲突,也就是线程安全的。比如说这样的函数:
public static int Add( int a, int b )
{
return a + b;
}
这个函数中所使用的所有的资源都是自己的局部变量,而函数的局部变量是储存在堆栈上的,每个线程都有自己独立的堆栈,所以局部变量不可能跨线程共享。所以这样的函数显然是线程安全的。
但值得注意的是:下面的函数不是线程安全的:
public static void Swap( ref int a, ref int b )
//C++: void Swap( in& a, int& b )
{
int c = a;
a = b;
b = c;
}
因为ref的存在,使得函数的参数是按引用传递进来的,换言之a和b看起来是函数的局部变量,但实际上却是函数外面的东西,如果这两个东西是另一个函数的局部变量,倒也没有问题,如果这两个东西是全局变量(静态成员),就不能确保没有线程冲突了。而在上个例子中,a和b在传入函数之时,就做了一个拷贝的动作,所以传进来的a、b到底是全局变量还是静态成员都没有关系了。
同样,这样的函数也不是线程安全的:
public static int Add( INumber a, INumber b )
//C++: int Add( INumber* a, INumber* b );
{
return a.Number + b.Number;
//C++: return a->Number + b->Number;
}
原因在于a和b虽然是函数的内部变量没错,但a.Number和b.Number却不是,它们不存在于堆栈上,而是在托管堆上,可能被其他线程更改。
但只使用局部变量的函数在.NET类库中是很少的,但.NET类库中还是有那么多线程安全的函数,是为什么呢?
因为,即使一个函数使用了共享资源,如果其所使用的共享资源都是线程安全的,则这个函数也是线程安全的。
比如说这样的函数:
private const string connectionString = “…”;
public string GetConnectionString()
{
return connectionString;
}
虽然这个函数使用了一个共享资源connectionString,但因为这个资源是线程安全的,所以这个函数还是线程安全的。
同样的,我们可以得出,如果一个函数只调用线程安全的函数,只使用线程安全的共享资源,那么这个函数也是线程安全的。
这里有一个容易被忽略的问题,运算符。并不是所有的运算符(尤其是重载后的运算符)都是线程安全的。
四、互斥锁
有时候我们不得不面对线程不安全的问题,比如说在一开始提出来的那个例子,多线程完成100次任务,我们怎样才能解决这个问题,一个简单的办法就是给共享资源加上互斥锁。在C#中这很简单。比如一开始的那个例子:
public static class Environment
{
public static int count = 0;//全局计数器
}
//…
void ThreadMethod()//运行在每个线程的方法
{
while( true )
{
lock ( typeof( Environment ) )
{
if ( count >= 100 )//如果达到任务指标
break;//中断线程执行
DoSomething();//完成某个任务
count++;
}
}
}
通过互斥锁,使得一个线程在使用count字段的时候,其他所有的线程都无法使用,而被阻塞等待。达到了避免线程冲突的效果。
当然,这样的锁会使得这个多线程程序退化成同时只有一个线程在跑,所以我们可以把count++提前,使得lock的范围缩小,如这样:
void ThreadMethod()//运行在每个线程的方法
{
while( true )
{
lock ( typeof( Environment ) )
{
if ( count++ >= 100 )//如果达到任务指标
break;//中断线程执行
}
DoSomething();//完成某个任务
}
}
最后来聊聊SyncRoot的问题。
用.NET的一定会有很多朋友困惑,为什么对一个容器加锁,需要这样写:
lock( Container.SyncRoot )
而不是直接lock( Container )
因为锁定一个容器并不能保证不会对这个容器进行修改,考虑这样一个容器:
public class Collection
{
private ArrayList _list;
public Add( object item )
{
_list.Add( item );
}
public object this[ int index ]
{
get { return _list[index]; }
set { _list[index] = value;}
}
}
看起来,将其lock起来后,就万事大吉了,没有人能修改这个容器,但实际上这个容器不过是用一个ArrayList实例来实现的,如果某段代码绕过这个容器而直接操作_list的话,则对这个容器对象lock也不可能保证容器不被修改了。
分享到:
相关推荐
在"JAVA多线程编程详解-详细操作例子.doc"和"Java多线程编程详解.doc"文档中,你应该能找到关于以上知识点的具体示例和深入解释,包括如何创建线程、线程间的通信(如wait/notify机制、Semaphore、CountDownLatch)...
### Java多线程编程详解 #### 一、线程同步机制 在Java多线程编程中,线程同步是一项至关重要的技术,它主要用于解决多线程环境中的资源共享问题。由于同一进程中的多个线程共享同一片内存空间,这就意味着它们...
1. **线程同步是为了解决共享资源的并发访问问题,防止数据不一致和冲突。** 2. **同步意味着线程排队,依次访问共享资源,而不是同时访问。** 3. **只有共享变量(可变状态)才需要同步,常量或不可变对象可以并发...
【多线程编程详解】 在计算机编程中,多线程是一种技术,允许程序在同一时间执行多个不同的任务。这种技术在VC++(Visual C++)编程环境中尤为重要,因为它可以提高程序的响应性和效率,特别是在处理耗时操作时。...
在多线程环境中,为了避免竞态条件和数据冲突,Delphi提供了多种同步机制。这些机制可以帮助开发者确保线程间的协调访问。 - **TCriticalSection**:最常用的同步对象之一,用于保护对共享资源的访问。使用示例: ...
.doc 格式 详细解析多线程技术 基础篇 • 怎样创建一个线程 • 受托管的线程与 Windows线程 • 前台线程与后台线程 • 名为BeginXXX和EndXXX的方法是做什么...• 计算一下冲突的可能性 • 请多使用lock,少用Mutex
解决办法是使用Invoke或BeginInvoke方法,这两个方法会在UI线程中安全地执行传入的委托,避免线程冲突。 ### 线程池 线程池是一组预创建的线程,用于执行任务,降低了创建和销毁线程的开销。每个进程有自己的线程池...
线程安全详解及相关实用技巧 引言 在并发编程中,多线程是实现高性能和高效资源利用的关键技术。然而,编写线程安全的代码并非易事,它涉及到多个线程之间的协调和同步,以避免数据竞争、死锁等问题。本文旨在深入...
2. 互斥量(Mutex):一次只允许一个线程访问资源,防止并发冲突。 3. 信号量(Semaphore):控制对有限资源的访问数量。 4. 条件变量(Condition):线程等待特定条件满足后才能继续执行。 四、线程优先级 Delphi...
.NET框架中的多线程是构建高性能、响应迅速的应用程序的关键技术。...了解程序运行方式、业务逻辑对事务和线程安全的要求,以及评估冲突可能性,有助于优化多线程设计。在实践中,平衡性能和线程安全是至关重要的。
在多线程环境下,同步机制是非常关键的,用于解决共享资源的访问冲突。Java提供了多种同步机制,如`synchronized`关键字、`wait()`, `notify()`, `notifyAll()`方法以及`Lock`接口及其实现类,如`ReentrantLock`。 ...
4. ThreadLocal:每个线程都有自己独立的一份变量副本,避免了线程间的冲突。 5. Condition:配合 Lock 使用,可以实现更复杂的线程间通信和同步。 六、线程中断与停止 Java 不推荐直接停止线程(如使用 `stop()` ...
### C#多线程详解 #### 一、基础篇 **1. 怎样创建一个线程** 在C#中创建线程的方式有很多种,这里主要介绍三种常用的创建方式:使用`Thread`类、使用`Delegate.BeginInvoke`以及使用`ThreadPool....
这份PDF文档,"java多线程设计模式详解",提供了一种深入理解如何在Java环境中高效利用多线程并保证程序稳定性的途径。下面,我们将详细探讨多线程设计模式的相关知识点。 1. **生产者消费者模式**:这种模式通过...
### POWER6线程与UltraSPARC T2 Plus线程详解 #### IBM的POWER6与SUN的UltraSPARC T2 Plus:多线程系统对比分析 近期,两大科技巨头IBM和SUN公司分别展示了旗下先进的多线程系统,即IBM的64线程Power 595和SUN的...
### 线程 - 实例详解(从初级到高级系列讲解) #### 一、绪论与基本概念 在计算机科学领域,线程是程序执行流的最小单位,它被包含在一个进程内,由操作系统调度执行。一个进程可以拥有多个线程,这些线程共享进程内...
在书店的例子中,通过`lock`关键字可以确保在任何时候只有一个售货员能够销售那本独一无二的书,避免了并发冲突。 在实际编程中,理解并合理使用进程和线程对于提高程序的性能和正确性至关重要。同时,线程同步和...
这意味着,如果多个线程同时访问一个对象,数据不会发生冲突。可以使用锁(如TCriticalSection)来控制对共享资源的访问。 4. **事件处理**: 多线程COM服务器中的事件处理需要特别注意,因为事件通常在调用线程中...