multithreading 线程最佳实践
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/660621/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Threading Best Practices
提问by patrick
Many projects I work on have poor threading implementations and I am the sucker who has to track these down. Is there an accepted best way to handle threading. My code is always waiting for an event that never fires.
我从事的许多项目的线程实现都很差,而我是必须追踪这些问题的笨蛋。是否有一种公认的最佳方式来处理线程。我的代码总是在等待一个永远不会触发的事件。
I'm kinda thinking like a design pattern or something.
我有点像设计模式之类的东西。
回答by Jon Skeet
(Assuming .NET; similar things would apply for other platforms.)
(假设 .NET;类似的事情也适用于其他平台。)
Well, there are lotsof things to consider. I'd advise:
嗯,有很多事情要考虑。我建议:
- Immutability is great for multi-threading. Functional programming works well concurrently partly due to the emphasis on immutability.
- Use locks when you access mutable shared data, both for reads and writes.
- Don't try to go lock-free unless you really have to. Locks are expensive, but rarely the bottleneck.
Monitor.Wait
should almost alwaysbe part of a condition loop, waiting for a condition to become true and waiting again if it's not.- Try to avoid holding locks for longer than you need to.
- If you ever need to acquire two locks at once, document the ordering thoroughly and make sure you always use the same order.
- Document the thread-safety of your types. Most types don'tneed to be thread-safe, they just need to not be thread hostile (i.e. "you can use them from multiple threads, but it's yourresponsibility to take out locks if you want to share them)
- Don't access the UI (except in documented thread-safe ways) from a non-UI thread. In Windows Forms, use Control.Invoke/BeginInvoke
- 不变性非常适合多线程。函数式编程同时运行良好,部分原因是强调不变性。
- 在访问可变共享数据时使用锁,用于读取和写入。
- 除非真的必须,否则不要尝试无锁。锁很昂贵,但很少成为瓶颈。
Monitor.Wait
应该几乎总是条件循环的一部分,等待条件变为真,如果不是,则再次等待。- 尽量避免持有锁的时间超过您需要的时间。
- 如果您需要一次获取两个锁,请彻底记录顺序并确保您始终使用相同的顺序。
- 记录您的类型的线程安全性。大多数类型并不需要是线程安全的,他们只需要不是线程敌对(即“你可以用它们从多个线程,但它是你的责任,采取了锁,如果你想分享)
- 不要从非 UI 线程访问 UI(除非以记录的线程安全方式)。在 Windows 窗体中,使用 Control.Invoke/BeginInvoke
That's off the top of my head - I probably think of more if this is useful to you, but I'll stop there in case it's not.
这超出了我的脑海 - 如果这对您有用,我可能会想到更多,但如果不是,我会停在那里。
回答by Daniel Earwicker
Learning to write multi-threaded programs correctly is extremely difficult and time consuming.
学习正确编写多线程程序是极其困难和耗时的。
So the first step is: replace the implementation with one that doesn't use multiple threads at all.
所以第一步是:用一个根本不使用多线程的实现替换。
Then carefully put threading back in if, and only if, you discover a genuine need for it, when you've figured out some very simple safe ways to do so. A non-threaded implementation that works reliably is far better than a broken threaded implementation.
然后,当且仅当您发现真正需要线程时,当您找到一些非常简单的安全方法时,小心地将线程放回原处。可靠工作的非线程实现远比损坏的线程实现要好得多。
When you're ready to start, favour designs that use thread-safe queues to transfer work items between threads and take care to ensure that those work items are accessed only by one thread at a time.
当您准备好开始时,倾向于使用线程安全队列在线程之间传输工作项的设计,并注意确保一次只能由一个线程访问这些工作项。
Try to avoid just spraying lock
blocks around your code in the hope that it will become thread-safe. It doesn't work. Eventually, two code paths will acquire the same locks in a different order, and everything will grind to a halt (once every two weeks, on a customer's server). This is especially likely if you combine threads with firing events, and you hold the lock while you fire the event - the handler may take out another lock, and now you have a pair of locks held in a particular order. What if they're taken out in the opposite order in some other situation?
尽量避免lock
在代码周围喷洒块,希望它成为线程安全的。它不起作用。最终,两个代码路径将以不同的顺序获取相同的锁,并且一切都会停止(每两周一次,在客户的服务器上)。如果您将线程与触发事件相结合,并且在触发事件时持有锁,则这种情况尤其可能发生 - 处理程序可能会取出另一个锁,现在您有一对按特定顺序持有的锁。如果在其他情况下以相反的顺序将它们取出怎么办?
In short, this is such a big and difficult subject that I think it is potentially misleading to give a few pointers in a short answer and say "Off you go!" - I'm sure that's not the intention of the many learned people giving answers here, but that is the impression many get from summarised advice.
简而言之,这是一个如此庞大而困难的主题,我认为在简短的回答中给出一些提示并说“走开!”可能会产生误导。- 我相信这不是许多有学识的人在这里给出答案的意图,但这是许多人从总结性建议中得到的印象。
Instead, buy this book.
相反,买这本书。
Here is a very nicely worded summary from this site:
这是来自该站点的措辞非常好的摘要:
Multithreading also comes with disadvantages. The biggest is that it can lead to vastly more complex programs. Having multiple threads does not in itself create complexity; it's the interaction between the threads that creates complexity. This applies whether or not the interaction is intentional, and can result long development cycles, as well as an ongoing susceptibility to intermittent and non-reproducable bugs. For this reason, it pays to keep such interaction in a multi-threaded design simple – or not use multithreading at all – unless you have a peculiar penchant for re-writing and debugging!
多线程也有缺点。最大的问题是它可以导致更复杂的程序。拥有多个线程本身并不会造成复杂性;是线程之间的交互造成了复杂性。无论交互是否有意,这都适用,并且可能导致较长的开发周期,以及对间歇性和不可重现的错误的持续敏感性。出于这个原因,在多线程设计中保持这种交互简单是值得的——或者根本不使用多线程——除非你有重写和调试的特殊癖好!
Perfect summary from Stroustrup:
The traditional way of dealing with concurrency by letting a bunch of threads loose in a single address space and then using locks to try to cope with the resulting data races and coordination problems is probably the worst possible in terms of correctness and comprehensibility.
处理并发的传统方法是让一堆线程在单个地址空间中松散,然后使用锁来尝试处理由此产生的数据竞争和协调问题,这在正确性和可理解性方面可能是最糟糕的。
回答by Matt Davis
(Like Jon Skeet, much of this assumes .NET)
(像 Jon Skeet 一样,其中大部分都假设 .NET)
At the risk of seeming argumentative, comments like these just bother me:
冒着似乎争论不休的风险,像这样的评论只会让我烦恼:
Learning to write multi-threaded programs correctly is extremely difficult and time consuming.
Threads should be avoided when possible...
学习正确编写多线程程序是极其困难和耗时的。
应尽可能避免使用线程...
It is practically impossible to write software that does anything significant without leveraging threads in some capacity. If you are on Windows, open your Task Manager, enable the Thread Count column, and you can probably count on one hand the number of processes that are using a single thread. Yes, one should not simply use threads for the sake of using threads nor should it be done cavalierly, but frankly, I believe these cliches are used too often.
在不利用线程的情况下,编写能够完成任何重要任务的软件几乎是不可能的。如果你在 Windows 上,打开你的任务管理器,启用线程计数列,你可能一方面可以计算使用单个线程的进程数。是的,我们不应该仅仅为了使用线程而使用线程,也不应该漫不经心地使用线程,但坦率地说,我相信这些陈词滥调被使用得太频繁了。
If I had to boil multithreaded programming down for the true novice, I would say this:
如果我必须为真正的新手来简化多线程编程,我会这样说:
- Before jumping into it, first understand that the the class boundary is not the same as a thread boundary. For example, if a callback method on your class is called by another thread (e.g., the AsyncCallback delegate to the TcpListener.BeginAcceptTcpClient() method), understand that the callback executes onthat other thread. So even though the callback occurs on the same object, you still have to synchronize access to the members of the object within the callback method. Threads and classes are orthogonal; it is important to understand this point.
- Identify what data needs to be shared between threads. Once you have defined the shared data, try to consolidate it into a single class if possible.
- Limit the places where the shared data can be written and read. If you can get this down to one place for writing and one place for reading, you will be doing yourself a tremendous favor. This is not always possible, but it is a nice goal to shoot for.
- Obviously make sure you synchronize access to the shared data using the Monitor class or the lock keyword.
- If possible, use a single object to synchronize your shared data regardless of how many different shared fields there are. This will simplify things. However, it may also overly constrain things too, in which case, you may need a synchronization object for each shared field. And at this point, using immutable classes becomes veryhandy.
- If you have one thread that needs to signal another thread(s), I would strongly recommend using the ManualResetEvent class to do this instead of using events/delegates.
- 在跳进去之前,首先要明白类边界和线程边界是不一样的。例如,如果您的类上的回调方法被另一个线程调用(例如,AsyncCallback 委托给 TcpListener.BeginAcceptTcpClient() 方法),请了解回调在该另一个线程上执行。因此,即使回调发生在同一个对象上,您仍然必须在回调方法中同步对对象成员的访问。线程和类是正交的;理解这一点很重要。
- 确定哪些数据需要在线程之间共享。定义共享数据后,如果可能,请尝试将其合并为一个类。
- 限制可以写入和读取共享数据的位置。如果您可以将其归结为一处写作和一处阅读,那么您将对自己大有裨益。这并不总是可能的,但这是一个很好的目标。
- 显然,请确保使用 Monitor 类或 lock 关键字同步对共享数据的访问。
- 如果可能,无论有多少不同的共享字段,都使用单个对象来同步您的共享数据。这将简化事情。但是,它也可能过度限制事物,在这种情况下,您可能需要为每个共享字段设置一个同步对象。在这一点上,使用不可变类变得非常方便。
- 如果您有一个线程需要向另一个线程发送信号,我强烈建议使用 ManualResetEvent 类来执行此操作,而不是使用事件/委托。
To sum up, I would say that threading is not difficult, but it can be tedious. Still, a properly threaded application will be more responsive, and your users will be most appreciative.
总而言之,我会说线程并不难,但它可能很乏味。尽管如此,正确线程化的应用程序将更具响应性,您的用户将非常感激。
EDIT: There is nothing "extremely difficult" about ThreadPool.QueueUserWorkItem(), asynchronous delegates, the various BeginXXX/EndXXX method pairs, etc. in C#. If anything, these techniques make it mucheasier to accomplish various tasks in a threaded fashion. If you have a GUI application that does any heavy database, socket, or I/O interaction, it is practically impossible to make the front-end responsive to the user without leveraging threads behind the scenes. The techniques I mentioned above make this possible and are a breeze to use. It is important to understand the pitfalls, to be sure. I simply believe we do programmers, especially younger ones, a disservice when we talk about how "extremely difficult" multithreaded programming is or how threads "should be avoided." Comments like these oversimplify the problem and exaggerate the myth when the truth is that threading has never been easier. There are legitimate reasons to use threads, and cliches like this just seem counterproductive to me.
编辑:C# 中的 ThreadPool.QueueUserWorkItem()、异步委托、各种 BeginXXX/EndXXX 方法对等没有什么“非常困难”。如果有的话,这些技术使它变得很重要以线程的方式更容易完成各种任务。如果您的 GUI 应用程序执行任何繁重的数据库、套接字或 I/O 交互,那么在不利用幕后线程的情况下使前端响应用户实际上是不可能的。我上面提到的技术使这成为可能,并且使用起来轻而易举。一定要了解陷阱,这一点很重要。我只是相信,当我们谈论多线程编程“极其困难”或“应该如何避免使用线程”时,我们对程序员,尤其是年轻的程序员是一种伤害。当事实是线程处理从未如此简单时,诸如此类的评论过度简化了问题并夸大了神话。使用线程是有正当理由的,而像这样的陈词滥调对我来说似乎适得其反。
回答by jalf
You may be interested in something like CSP, or one of the other theoretical algebras for dealing with concurrency. There are CSP libraries for most languages, but if the language wasn't designed for it, it requires a bit of discipline to use correctly. But ultimately, every kind of concurrency/threading boils down to some fairly simple basics: Avoid shared mutable data, and understand exactly when and why each thread may have to block while waiting for another thread. (In CSP, shared data simply doesn't exist. Each thread (or process in CSP terminology) is onlyallowed to communicate with others through blocking message-passing channels. Since there is no shared data, race conditions go away. Since message passing is blocking, it becomes easy to reason about synchronization, and literally prove that no deadlocks can occur.)
您可能对CSP或其他处理并发的理论代数之一感兴趣。大多数语言都有 CSP 库,但如果该语言不是为它设计的,则需要一些纪律才能正确使用。但最终,每种并发/线程都归结为一些相当简单的基础知识:避免共享可变数据,并准确了解每个线程在等待另一个线程时可能必须阻塞的时间和原因。(在 CSP 中,共享数据根本不存在。每个线程(或 CSP 术语中的进程)只是允许通过阻塞消息传递通道与他人通信。由于没有共享数据,竞争条件消失了。由于消息传递是阻塞的,因此很容易推断同步,并从字面上证明不会发生死锁。)
Another good practice, which is easier to retrofit into existing code is to assign a priority or level to every lock in your system, and make sure that the following rules are followed consistently:
另一个更容易改造现有代码的好做法是为系统中的每个锁分配优先级或级别,并确保始终遵循以下规则:
- While holding a lock at level N, you may only acquire new locks of lower levels
- Multiple locks at the same level must be acquired at the same time, as a single operation, which always tries to acquire all the requested locks in the same global order (Note that any consistent order will do, but any thread that tries to acquire one or more locks at level N, must do acquire them in the same order as any other thread would do anywhere else in the code.)
- 在持有 N 级锁时,您只能获取较低级别的新锁
- 同一级别的多个锁必须同时获取,作为单个操作,它总是尝试以相同的全局顺序获取所有请求的锁(注意任何一致的顺序都可以,但任何试图获取一个的线程或更多的 N 级锁,必须以与任何其他线程在代码中的任何其他地方所做的相同的顺序获取它们。)
Following these rules mean that it is simply impossible for a deadlock to occur. Then you just have to worry about mutable shared data.
遵循这些规则意味着根本不可能发生死锁。然后你只需要担心可变的共享数据。
回答by Aaron
BIG emphasis on the first point that Jon posted. The more immutable state that you have (ie: globals that are const, etc...), the easier your life is going to be (ie: the fewer locks you'll have to deal with, the less reasoning you'll have to do about interleaving order, etc...)
重点强调 Jon 发布的第一点。您拥有的不可变状态越多(即:const 的全局变量等),您的生活就会越轻松(即:您需要处理的锁越少,推理就越少做关于交错顺序等...)
Also, often times if you have small objects to which you need multiple threads to have access, you're sometimes better off copying it between threads rather than having a shared, mutable global that you have to hold a lock to read/mutate. It's a tradeoff between your sanity and memory efficiency.
此外,通常情况下,如果您有需要多个线程访问的小对象,有时最好在线程之间复制它,而不是拥有一个共享的、可变的全局变量,您必须持有一个锁才能读取/变异。这是您的理智和内存效率之间的权衡。
回答by Bartosz Klimek
Looking for a design pattern when dealing with threads is the really best approach to start with. It's too bad that many people don't try it, instead attempting to implement less or more complex multithreaded constructs on their own.
在处理线程时寻找设计模式是真正最好的开始方法。可惜很多人不去尝试,而是尝试自己实现更简单或更复杂的多线程结构。
I would probably agree with all opinions posted so far. In addition, I'd recommend to use some existing more coarse-grained frameworks, providing building blocks rather than simple facilities like locks, or wait/notify operations. For Java, it would be simply the built-in java.util.concurrent
package, which gives you ready-to-use classes you can easily combine to achieve a multithreaded app. The big advantage of this is that you avoid writing low-level operations, which results in hard-to-read and error-prone code, in favor of a much clearer solution.
我可能会同意到目前为止发布的所有意见。此外,我建议使用一些现有的更粗粒度的框架,提供构建块而不是像锁或等待/通知操作这样的简单工具。对于 Java,它只是内置java.util.concurrent
包,它为您提供了随时可用的类,您可以轻松地组合以实现多线程应用程序。这样做的最大优点是您可以避免编写低级操作,这会导致难以阅读且容易出错的代码,从而有利于更清晰的解决方案。
From my experience, it seems that most concurrency problems can be solved in Java by using this package. But, of course, you always should be careful with multithreading, it's challenging anyway.
根据我的经验,使用这个包似乎可以在 Java 中解决大多数并发问题。但是,当然,您应该始终小心使用多线程,无论如何它都是具有挑战性的。
回答by Julien Chastang
It's the mutable state, stupid
这是可变状态,愚蠢
That is a direct quote from Java Concurrency in Practiceby Brian Goetz. Even though the book is Java-centric, the "Summary of Part I" gives some other helpful hints that will apply in many threaded programming contexts. Here are a few more from that same summary:
这是Brian Goetz 的Java Concurrency in Practice的直接引用。尽管本书以 Java 为中心,“第一部分的总结”提供了一些其他有用的提示,这些提示将适用于许多线程编程上下文。以下是同一摘要中的更多内容:
- Immutable objects are automatically thread-safe.
- Guard each mutable variable with a lock.
- A program that accesses a mutable variable from multiple threads without synchronization is a broken program.
- 不可变对象自动是线程安全的。
- 用锁保护每个可变变量。
- 从多个线程访问可变变量而没有同步的程序是一个损坏的程序。
I would recommend getting a copy of the book for an in-depth treatment of this difficult topic.
我建议您购买本书的副本,以深入探讨这个难题。
(source: umd.edu)
(来源:umd.edu)
回答by Scott Wisniewski
I'd like to follow up with Jon Skeet's advice with a couple more tips:
我想跟进 Jon Skeet 的建议,并提供更多提示:
If you are writing a "server", and are likely to have a high amount of insert parallelism, don't use Microsoft's SQL Compact. Its lock manager is stupid. If you do use SQL Compact, DON'T use serializable transactions (which happens to be the default for the TransactionScope class). Things will fall apart on you rapidly. SQL Compact doesn't support temporary tables, and when you try to simulate them inside of serialized transactions it does rediculsouly stupid things like take x-locks on the index pages of the _sysobjects table. Also it get's really eager about lock promotion, even if you don't use temp tables. If you need serial access to multiple tables , your best bet is to use repeatable read transactions(to give atomicity and integrity) and then implement you own hierarchal lock manager based on domain-objects (accounts, customers, transactions, etc), rather than using the database's page-row-table based scheme.
When you do this, however, you need to be careful (like John Skeet said) to create a well defined lock hierarchy.
If you do create your own lock manager, use
<ThreadStatic>
fields to store information about the locks you take, and then add asserts every where inside the lock manager that enforce your lock hierarchy rules. This will help to root out potential issues up front.In any code that runs in a UI thread, add asserts on
!InvokeRequired
(for winforms), orDispatcher.CheckAccess()
(for WPF). You should similarly add the inverse assert to code that runs in background threads. That way, people looking at a method will know, just by looking at it, what it's threading requirements are. The asserts will also help to catch bugs.Assert like crazy, even in retail builds. (that means throwing, but you can make your throws look like asserts). A crash dump with an exception that says "you violated threading rules by doing this", along with stack traces, is much easier to debug then a report from a customer on the other side of the world that says "every now and then the app just freezes on me, or it spits out gobbly gook".
如果您正在编写“服务器”,并且可能具有大量插入并行性,请不要使用 Microsoft 的 SQL Compact。它的锁管理器是愚蠢的。如果您确实使用 SQL Compact,请勿使用可序列化事务(这恰好是 TransactionScope 类的默认值)。事情会很快在你身上分崩离析。SQL Compact 不支持临时表,并且当您尝试在序列化事务中模拟它们时,它会执行非常愚蠢的操作,例如在 _sysobjects 表的索引页上使用 x 锁。即使您不使用临时表,它也非常热衷于锁升级。如果您需要串行访问多个表,
但是,在执行此操作时,您需要小心(如 John Skeet 所说)以创建明确定义的锁层次结构。
如果您确实创建了自己的锁管理器,请使用
<ThreadStatic>
字段来存储有关您使用的锁的信息,然后在锁管理器内的每个位置添加断言,以强制执行您的锁层次结构规则。这将有助于预先根除潜在的问题。在 UI 线程中运行的任何代码中,添加断言
!InvokeRequired
(对于 winforms)或Dispatcher.CheckAccess()
(对于 WPF)。您应该类似地将反向断言添加到在后台线程中运行的代码。这样,查看方法的人只需查看它就知道它的线程要求是什么。断言也将有助于捕获错误。疯狂断言,即使在零售构建中也是如此。(这意味着投掷,但您可以使投掷看起来像断言)。带有异常的故障转储显示“您这样做违反了线程规则”以及堆栈跟踪,然后调试起来要容易得多,然后来自世界另一端的客户的报告说“时不时应用程序只是冻结在我身上,或者它会吐出狼吞虎咽的东西”。
回答by Dan Breslau
Adding to the points that other folks have already made here:
添加其他人已经在这里提出的观点:
Some developers seem to think that "almost enough" locking is good enough. It's been my experience that the opposite can be true -- "almost enough" locking can be worsethan enough locking.
一些开发人员似乎认为“几乎足够”的锁定就足够了。根据我的经验,反之亦然——“几乎足够”的锁定可能比足够多的锁定更糟糕。
Imagine thread A locking resource R, using it, and then unlocking it. A then uses resource R' without a lock.
想象线程 A 锁定资源 R,使用它,然后解锁它。然后 A 使用资源 R' 而不加锁。
Meanwhile, thread B tries to access R while A has it locked. Thread B is blocked until thread A unlocks R. Then the CPU context switches to thread B, which accesses R, and then updates R' during its time slice. That update renders R' inconsistent with R, causing a failure when A tries to access it.
同时,线程 B 尝试访问 R,而 A 已将其锁定。线程 B 被阻塞,直到线程 A 解锁 R。然后 CPU 上下文切换到线程 B,后者访问 R,然后在其时间片内更新 R'。该更新使 R' 与 R 不一致,从而在 A 尝试访问它时导致失败。
Test on as many different hardware and OS architectures as possible. Different CPU types, different numbers of cores and chips, Windows/Linux/Unix, etc.
在尽可能多的不同硬件和操作系统架构上进行测试。不同的 CPU 类型,不同数量的内核和芯片,Windows/Linux/Unix 等。
The first developer who worked with multi-threaded programs was a guy named Murphy.
第一个使用多线程程序的开发人员是一个叫 Murphy 的人。
回答by Tim Post
Well, everyone thus far has been Windows / .NET centric, so I'll chime in with some Linux / C.
嗯,到目前为止,每个人都以 Windows / .NET 为中心,所以我会加入一些 Linux / C。
Avoid futexes at all costs(PDF), unless you really, really need to recover some of the time spent with mutex locks. I am currently pulling my hair out with Linux futexes.
不惜一切代价避免使用 futex(PDF),除非你真的,真的需要恢复一些花费在互斥锁上的时间。我目前正在用 Linux futexes 拉我的头发。
I don't yet have the nerve to go with practical lock free solutions, but I'm rapidly approaching that point out of pure frustration. If I could find a good, well documented and portable implementation of the above that I could really study and grasp, I'd probably ditch threads completely.
我还没有勇气采用实用的无锁解决方案,但我正出于纯粹的挫败感而迅速接近这一点。如果我能找到一个好的、有据可查且可移植的实现,我可以真正研究和掌握上述内容,我可能会完全放弃线程。
I have come across so much code lately that uses threads which really should not, its obvious that someone just wanted to profess their undying love of POSIX threads when a single (yes, just one) fork would have done the job.
我最近遇到了很多使用线程的代码,而这些代码实际上不应该使用,很明显有人只是想表达他们对 POSIX 线程的永恒热爱,而一个(是的,只有一个)叉子就可以完成这项工作。
I wish that I could give you some code that 'just works', 'all the time'. I could, but it would be so silly to serve as a demonstration (servers and such that start threads for each connection). In more complex event driven applications, I have yet (after some years) to write anything that doesn't suffer from mysterious concurrency issues that are nearly impossible to reproduce. So I'm the first to admit, in that kind of application, threads are just a little too much rope for me. They are so tempting and I always end up hanging myself.
我希望我能给你一些“一直有效”、“一直有效”的代码。我可以,但是作为演示(服务器等为每个连接启动线程)会很愚蠢。在更复杂的事件驱动应用程序中,我还没有(几年后)编写任何不会遭受几乎不可能重现的神秘并发问题的东西。所以我是第一个承认,在那种应用中,线程对我来说有点太多了。他们太诱人了,我总是上吊自杀。