最近我尝试从线程(UI线程除外)访问文本框,并抛出异常。它说的是“代码不是线程安全的”,所以我最终编写了一个代表(来自MSDN的示例帮助)并调用它。
但即便如此,我也不太明白为什么所有额外的代码都是必要的。
更新: 如果我检查
,我会遇到任何严重的问题Controls.CheckForIllegalCrossThread..blah =true
答案 0 :(得分:106)
Eric Lippert有一篇很好的博文,题为What is this thing you call "thread safe"?关于维基百科的线程安全定义。
从链接中提取的3个重要事项:
“如果一段代码在期间正常运行,则它是线程安全的 多线程同时执行。“
“特别是,它必须满足多线程的需要 访问相同的共享数据,......“
“......只需一个人就可以访问共享数据 在任何给定的时间线程。“
绝对值得一读!
答案 1 :(得分:88)
用最简单的术语来说,线程安全意味着从多个线程访问是安全的。当您在程序中使用多个线程并且每个线程都试图访问内存中的公共数据结构或位置时,可能会发生一些不好的事情。所以,你添加一些额外的代码来防止这些坏事。例如,如果两个人同时写同一文档,则第二个保存的人将覆盖第一个人的工作。为了使线程安全,那么在允许人2编辑文档之前,必须强制人2等待人1完成他们的任务。
答案 2 :(得分:16)
Wikipedia有一篇关于线程安全的文章。
这个definitions page(你必须跳过广告 - 抱歉)这样定义:
在计算机编程中,线程安全描述了可以从多个编程线程调用的程序部分或例程,而线程之间没有不必要的交互。
线程是程序的执行路径。单线程程序只有一个线程,所以不会出现这个问题。实际上,所有GUI程序都有多个执行路径,因此有多个线程 - 至少有两个,一个用于处理GUI的显示和处理用户输入,另外一个用于实际执行程序的操作。
这样做是为了在程序工作时通过将任何长时间运行的进程卸载到任何非UI线程来使UI仍然响应。这些线程可以创建一次并在程序的生命周期中存在,或者只在需要时创建并在完成后销毁。
由于这些线程通常需要执行常见操作 - 磁盘i / o,将结果输出到屏幕等 - 代码的这些部分需要以这样的方式编写,即它们可以处理从多个线程调用,经常在同一时间。这将涉及以下内容:
答案 3 :(得分:5)
简单地说,线程安全意味着多个线程可以同时使用方法或类实例而不会出现任何问题。
考虑以下方法:
private int myInt = 0;
public int AddOne()
{
int tmp = myInt;
tmp = tmp + 1;
myInt = tmp;
return tmp;
}
现在,线程A和线程B都想执行AddOne()。但是A首先启动并将myInt(0)的值读入tmp。由于某种原因,调度程序决定暂停线程A并将执行延迟到线程B.线程B现在还将myInt(仍为0)的值读入其自己的变量tmp。线程B完成整个方法,所以最后myInt = 1.返回1。现在轮到A了。线程A继续。并且向tmp添加1(对于线程A,tmp为0)。然后将此值保存在myInt中。 myInt再次为1.
所以在这种情况下,方法AddOne被调用了两次,但由于该方法没有以线程安全的方式实现,myInt的值不是2,正如预期的那样,但是因为第二个线程在读取变量myInt之前第一个线程完成了更新。
在非平凡的情况下,创建线程安全方法非常困难。而且有很多技巧。在Java中,您可以将方法标记为已同步,这意味着在给定时间只有一个线程可以执行该方法。其他线程排队等候。这使方法线程安全,但如果在方法中要做很多工作,那么这会浪费很多空间。另一种技术是通过创建一个锁或信号量来锁定这个小部分(通常称为临界区),'仅将方法的一小部分标记为同步'。甚至有一些方法被实现为无锁线程安全,这意味着它们以这样的方式构建,即多个线程可以同时通过它们而不会引起问题,这可能是仅在方法时的情况执行一个原子调用。原子调用是无法中断的调用,一次只能由一个线程完成。
答案 4 :(得分:4)
如果模块保证在面向多线程和并发使用时可以保持其不变量,则该模块是线程安全的。
这里,模块可以是数据结构,类,对象,方法/过程或函数。基本上是一段代码和相关数据。
保证可能仅限于某些特定CPU架构等环境,但必须适用于这些环境。如果没有明确的环境划分,那么通常意味着它适用于所有可以编译和执行代码的环境。
线程不安全的模块可能在多线程和并发使用下正常运行,但这通常比运气和巧合更重要,而不是精心设计。即使某些模块没有为您打破,移动到其他环境时也可能会中断。
多线程错误通常很难调试。其中一些只是偶尔发生,而另一些则是积极地表现出来 - 这也可能是环境特定的。它们可以表现为巧妙的错误结果或死锁。它们可能以不可预测的方式搞乱数据结构,并导致其他看似不可能的错误出现在代码的其他远程部分。它可能非常特定于应用程序,因此很难给出一般描述。
答案 5 :(得分:4)
您可以从书中获得更多解释" Java Concurrency in Practice":
如果一个类在从多个线程访问时行为正确,则无论运行时环境是否调度或交错执行这些线程,并且调用时没有其他同步或其他协调,该类都是线程安全的代码。
答案 6 :(得分:4)
在外行的实际例子中
假设您拥有互联网和手机银行的银行帐户,而您的帐户只需10美元。 您使用手机银行将转帐余额转移到另一个帐户,同时,您使用同一个银行帐户进行了在线购物。 如果这个银行账户不是线程安全的,那么银行允许您同时执行两笔交易,然后银行将破产。
线程安全意味着如果多个线程同时尝试访问该对象,则对象的状态不会发生变化。
答案 7 :(得分:3)
线程安全性 :线程安全程序保护其数据免受内存一致性错误的影响。在高度多线程的程序中,线程安全程序不会对同一对象上的多个线程进行多次读/写操作产生任何副作用。不同的线程可以共享和修改对象数据而没有一致性错误
您可以使用高级并发API来实现线程安全。本文档page提供了良好的编程结构,以实现线程安全。
Lock Objects支持锁定简化许多并发应用程序的习惯用法。
Executors定义了用于启动和管理线程的高级API。 java.util.concurrent提供的执行程序实现提供了适用于大规模应用程序的线程池管理。
Concurrent Collections可以更轻松地管理大量数据,并且可以大大减少同步需求。
Atomic Variables具有最小化同步并有助于避免内存一致性错误的功能。
ThreadLocalRandom (在JDK 7中)可以从多个线程中高效生成伪随机数。
对于其他编程结构,也请参阅java.util.concurrent和java.util.concurrent.atomic包。
答案 8 :(得分:1)
您显然在WinForms环境中工作。 WinForms控件显示线程关联,这意味着创建它们的线程是唯一可用于访问和更新它们的线程。这就是为什么你会在MSDN和其他地方找到示例如何将回调编组回主线程的原因。
正常的WinForms实践是拥有一个专用于所有UI工作的线程。
答案 9 :(得分:1)
我发现http://en.wikipedia.org/wiki/Reentrancy_%28computing%29的概念是我通常认为的不安全线程,当一个方法具有并依赖于诸如全局变量之类的副作用时。
例如,我见过将浮点数格式化为字符串的代码,如果其中两个在不同的线程中运行,则decimalSeparator的全局值可以永久更改为“。”
//built in global set to locale specific value (here a comma)
decimalSeparator = ','
function FormatDot(value : real):
//save the current decimal character
temp = decimalSeparator
//set the global value to be
decimalSeparator = '.'
//format() uses decimalSeparator behind the scenes
result = format(value)
//Put the original value back
decimalSeparator = temp
答案 10 :(得分:-1)
要了解线程安全性,请阅读以下sections:
<强> 4.3.1。示例:使用委派的车辆跟踪器
作为一个更具体的授权示例,让我们构建一个委托给线程安全类的车辆跟踪器版本。我们将位置存储在Map中,因此我们从线程安全的Map实现
ConcurrentHashMap
开始。我们还使用不可变的Point类而不是MutablePoint
来存储位置,如清单4.6所示。清单4.6。 DelegatingVehicleTracker使用的不可变Point类。
class Point{ public final int x, y; public Point() { this.x=0; this.y=0; } public Point(int x, int y) { this.x = x; this.y = y; } }
清单4.7中的
Point
是线程安全的,因为它是不可变的。不可变值可以自由共享和发布,因此我们不再需要在返回时复制位置。
DelegatingVehicleTracker
不使用任何显式同步;所有对状态的访问都由ConcurrentHashMap
管理,并且Map的所有键和值都是不可变的。清单4.7。将线程安全委派给ConcurrentHashMap。
public class DelegatingVehicleTracker { private final ConcurrentMap<String, Point> locations; private final Map<String, Point> unmodifiableMap; public DelegatingVehicleTracker(Map<String, Point> points) { this.locations = new ConcurrentHashMap<String, Point>(points); this.unmodifiableMap = Collections.unmodifiableMap(locations); } public Map<String, Point> getLocations(){ return this.unmodifiableMap; // User cannot update point(x,y) as Point is immutable } public Point getLocation(String id) { return locations.get(id); } public void setLocation(String id, int x, int y) { if(locations.replace(id, new Point(x, y)) == null) { throw new IllegalArgumentException("invalid vehicle name: " + id); } }
}
如果我们使用原始
MutablePoint
类而不是Point,我们将通过让getLocations
发布对非线程安全的可变状态的引用来打破封装。请注意,我们稍微改变了车辆跟踪器类的行为;当监视器版本返回位置的快照时,委派版本返回车辆位置的不可修改但“实时”视图。这意味着如果线程A调用getLocations
并且线程B稍后修改某些点的位置,则这些更改将反映在返回到线程A的Map中。<强> 4.3.2。独立状态变量
我们还可以将线程安全性委托给多个基础状态变量,只要这些基础状态变量是独立的,这意味着复合类不会强制涉及多个状态变量的任何不变量。
清单4.9中的
VisualComponent
是一个图形组件,允许客户端为鼠标和击键事件注册监听器。它维护每种类型的已注册侦听器列表,以便在发生事件时可以调用相应的侦听器。但是鼠标监听器和关键监听器之间没有任何关系;这两者是独立的,因此VisualComponent
可以将其线程安全义务委托给两个基础线程安全列表。清单4.9。将线程安全性委托给多个基础状态变量。
public class VisualComponent { private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>(); private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>(); public void addKeyListener(KeyListener listener) { keyListeners.add(listener); } public void addMouseListener(MouseListener listener) { mouseListeners.add(listener); } public void removeKeyListener(KeyListener listener) { keyListeners.remove(listener); } public void removeMouseListener(MouseListener listener) { mouseListeners.remove(listener); } }
VisualComponent
使用CopyOnWriteArrayList
存储每个侦听器列表;这是一个线程安全的List实现,特别适合管理监听器列表(参见第5.2.3节)。每个List都是线程安全的,并且由于没有约束将一个状态与另一个状态耦合,VisualComponent
可以将其线程安全职责委托给基础mouseListeners
和keyListeners
对象。<强> 4.3.3。当代表团失败时
大多数复合类并不像
VisualComponent
那么简单:它们具有与组件状态变量相关的不变量。代码清单4.10中的NumberRange
使用两个AtomicIntegers
来管理其状态,但是施加了一个额外的约束 - 第一个数字小于或等于第二个。清单4.10。数字范围类不能充分保护其不变量。不要这样做。
public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { //Warning - unsafe check-then-act if(i > upper.get()) { throw new IllegalArgumentException( "Can't set lower to " + i + " > upper "); } lower.set(i); } public void setUpper(int i) { //Warning - unsafe check-then-act if(i < lower.get()) { throw new IllegalArgumentException( "Can't set upper to " + i + " < lower "); } upper.set(i); } public boolean isInRange(int i){ return (i >= lower.get() && i <= upper.get()); } }
NumberRange
不是线程安全的;它不保留限制下部和上部的不变量。setLower
和setUpper
方法试图尊重这种不变量,但做得很差。setLower
和setUpper
都是check-then-act序列,但它们没有使用足够的锁定来使它们成为原子序列。如果数字范围成立(0,10),并且一个线程调用setLower(5)
而另一个线程调用setUpper(4)
,则一些不幸的时间都会通过设置器中的检查,并且将应用这两个修改。结果是范围现在成立(5,4) - 无效状态。所以虽然底层的AtomicIntegers是线程安全的,但复合类不是。由于基础状态变量lower
和upper
不是独立的,NumberRange
不能简单地将线程安全委托给其线程安全状态变量。
NumberRange
可以通过使用锁定来保持其不变量,例如使用公共锁保护下部和上部,从而使线程安全。它还必须避免发布lower和upper以防止客户端破坏其不变量。如果一个类具有复合动作,如
NumberRange
所做的那样,单独的委托也不适合于线程安全。在这些情况下,类必须提供自己的锁定以确保复合操作是原子的,除非整个复合操作也可以委托给基础状态变量。如果一个类由多个独立的线程安全状态变量组成,并且没有任何操作具有任何无效的状态转换,那么它可以将线程安全性委托给基础状态变量。