【问题标题】:How can threads be avoided?如何避免线程?
【发布时间】:2008-12-19 22:54:54
【问题描述】:

我最近阅读了很多关于编写多线程应用程序如何令人头疼的文章,并且已经对该主题了解得足够多,至少在某种程度上可以理解为什么会这样。

我读过使用函数式编程技术可以帮助减轻这种痛苦,但我从未见过一个简单的并发函数式代码示例。那么,使用线程有哪些替代方法?至少,有什么方法可以将它们抽象出来,这样您就不必考虑诸如锁定之类的事情以及特定库的对象是否是线程安全的。

我知道 Google 的 MapReduce 应该可以帮助解决这个问题,但我还没有看到一个简洁的解释。

虽然我在下面给出了一个具体的例子,但我对一般技术比解决这个具体问题更感兴趣(虽然使用这个例子来帮助说明其他技术会很有帮助)。

我在编写一个简单的网络爬虫作为学习练习时提出了这个问题。它工作得很好,但速度很慢。大多数瓶颈来自下载页面。它目前是单线程的,因此一次只能下载一个页面。因此,如果可以同时下载页面,即使爬虫在单处理器机器上运行,它也会显着加快速度。我研究过使用线程来解决问题,但它们吓到了我。有关如何在不引发可怕的线程噩梦的情况下为此类问题添加并发性的任何建议?

【问题讨论】:

    标签: multithreading language-agnostic concurrency


    【解决方案1】:

    函数式编程有助于并发的原因并不是因为它避免使用线程。

    相反,函数式编程宣扬不变性和没有副作用。

    这意味着一个操作可以扩展到 N 个线程或进程,而不必担心会弄乱共享状态。

    【讨论】:

      【解决方案2】:

      实际上,线程很容易处理,除非您需要同步它们。通常,您使用线程池添加任务并等待它们完成。

      当线程需要通信和访问共享数据结构时,多线程变得非常复杂。只要你有两个锁,就会出现死锁,这就是多线程变得非常困难的地方。有时,只需几条指令,您的锁定代码就可能是错误的。在这种情况下,您只能在生产中看到错误,在多核机器上(如果您在单核上开发,发生在我身上)或者它们可能由其他一些硬件或软件触发。单元测试在这里没有多大帮助,测试会发现错误,但你永远无法像在“普通”应用程序中那样确定。

      【讨论】:

        【解决方案3】:

        我将添加一个示例,说明如何使用函数式代码安全地使代码并发。

        以下是您可能希望并行执行的一些代码,因此您无需等待一个文件完成即可开始下载下一个文件:

        void DownloadHTMLFiles(List<string> urls)
        {
            foreach(string url in urls)
            {
                 DownlaodOneFile(url);  //download html and save it to a file with a name based on the url - perhaps used for caching.
            }
        }
        

        如果您有许多文件,用户可能会花一分钟或更长时间等待它们。我们可以像这样在功能上重写这段代码,它基本上做了同样的事情:

        urls.ForEach(DownloadOneFile);
        

        请注意,这仍然是按顺序运行的。然而,它不仅更短,而且我们在这里获得了一个重要的优势。由于对 DownloadOneFile 函数的每次调用都与其他函数完全隔离(就我们的目的而言,可用带宽不是问题),您可以很容易地将 ForEach 函数换成另一个非常相似的函数:一个启动对 DownlaodOneFile 的每次调用的函数来自线程池的单独线程。

        事实证明.Net 使用Parallel Extensions 提供了这样一个功能。因此,通过使用函数式编程,您可以更改一行代码,突然间让一些原本按顺序运行的并行运行。太强大了。

        【讨论】:

          【解决方案4】:

          有一些关于异步模型的简短提及,但没有人真正解释过它,所以我想我会插话。我见过的用于替代多线程的最常用方法是异步架构。真正的意思是,不是在单个线程中按顺序执行代码,而是使用轮询方法来启动一些函数,然后返回并定期检查,直到有可用数据。

          这真的只适用于像你前面提到的爬虫这样的模型,真正的瓶颈是 I/O 而不是 CPU。概括地说,异步方法将在多个套接字上启动下载,并且轮询循环定期检查它们是否完成下载,完成后,我们可以继续下一步。这允许您通过在同一个线程中进行上下文切换来运行在网络上等待的多个下载。

          多线程模型的工作原理大致相同,只是使用单独的线程而不是轮询循环检查同一线程中的多个套接字。在 I/O 绑定应用程序中,对于许多用例来说,异步轮询的工作方式几乎与线程处理一样好,因为真正的问题只是等待 I/O 完成,而不是等待 CPU 处理数据。

          另一个现实世界的例子是一个需要执行许多其他可执行文件并等待结果的系统。这可以在线程中完成,但它也相当简单,并且几乎与简单地启动几个外部应用程序作为 Process 对象一样有效,然后定期检查直到它们全部执行完毕。这会将 CPU 密集型部分(外部可执行文件中的运行代码)放在它们自己的进程中,但数据处理都是异步处理的。

          我使用的 Python ftp 服务器库,pyftpdlib 使用 Python asyncore 库来处理仅通过单个线程为 FTP 客户端提供服务,以及用于文件传输和命令/响应的异步套接字通信。

          请参阅Asynchronous Programming 上的 Python Twisted 库页面以进一步阅读 - 虽然有些特定于使用 Twisted,但它还从初学者的角度介绍了异步编程。

          【讨论】:

            【解决方案5】:

            并发是计算机科学中相当复杂的学科,需要对硬件架构和操作系统行为有很好的理解。

            多线程有许多基于您的硬件和托管操作系统的实现,尽管它已经很困难,但陷阱很多。需要注意的是,要想实现“真正的”并发,线程是唯一的出路。基本上,线程是您作为程序员在软件的不同部分之间共享资源同时允许它们并行运行的唯一方法。通过并行,您应该考虑到标准 CPU(除了双核/多核之外)只能一次做一件事。上下文切换等概念现在开始发挥作用,它们有自己的一套规则和限制。

            我认为在你开始在你的程序中实现并发之前,你应该寻找更多关于这个主题的通用背景,就像你所说的那样。

            我想最好的起点是wikipedia article on concurrency,然后从那里继续。

            【讨论】:

            • 多线程并不是实现并发的唯一方式,您还可以使用多处理。毕竟,线程是轻量级进程。
            • 多处理显然是一个技术上可用的选项,但如果我将其划掉作为仅应在极少数情况下使用的选项,请原谅我。
            • 所以您认为 *nix 世界主要由“罕见情况”组成?
            【解决方案6】:

            当线程共享资源和/或需要相互通信时,通常会使多线程编程成为一场噩梦。在下载网页的情况下,您的线程将独立工作,因此您可能没有太多麻烦。

            您可能需要考虑的一件事是生成多个进程而不是多个线程。在你提到的情况下——同时下载网页——你可以将工作负载分成多个块,并将每个块交给一个单独的工具实例(如cURL)来完成工作。

            【讨论】:

              【解决方案7】:

              如果您的目标是实现并发,则很难摆脱使用多个线程或进程。诀窍不是避免它,而是以一种可靠且不易出错的方式管理它。尤其是死锁和竞争条件是并发编程中容易出错的两个方面。管理此问题的一种通用方法是使用生产者/消费者队列......线程将工作项写入队列,工作人员从中提取项目。您必须确保正确同步对队列的访问并做好准备。

              此外,根据您的问题,您还可以创建一种域特定语言来消除并发问题,至少从使用您的语言的人的角度来看……当然是处理该语言的引擎仍然需要处理并发性,但如果这将被许多用户利用,它可能是有价值的。

              【讨论】:

                【解决方案8】:

                那里有一些不错的库。

                java.util.concurrent.ExecutorCompletionService 将获取 Futures 的集合(即返回值的任务),在后台线程中处理它们,然后将它们放入队列中,以便您在它们完成时进一步处理。当然,这是 Java 5 及更高版本,因此并非随处可用。

                换句话说,您的所有代码都是单线程的 - 但是您可以确定并行运行的安全内容,您可以将其移植到合适的库中。

                要点是,如果您可以使任务独立,那么只要稍微考虑一下,线程安全就不是不可能实现的——尽管强烈建议您将复杂的部分(如实现 ExecutorCompletionService)留给专家...

                【讨论】:

                  【解决方案9】:

                  在您的简单场景中避免线程化的一种简单方法是从不同的进程下载。主进程会调用其他进程带参数将文件下载到本地目录,然后主进程就可以完成真正的工作了。

                  我认为这些问题没有任何简单的解决方案。它不是线程问题。它的并发性使人的思维崩溃。

                  【讨论】:

                    【解决方案10】:

                    您可以观看有关 F# 语言的 MSDN 视频:PDC 2008: An introduction to F#

                    这包括您正在寻找的两件事。 (函数式+异步)

                    【讨论】:

                      【解决方案11】:

                      对于 python,这看起来是一个有趣的方法:http://members.verizon.net/olsongt/stackless/why_stackless.html#introduction

                      【讨论】:

                      • 不是。它不能帮助你避免束缚自己。
                      • 从我对 Stackless 的快速浏览来看,它似乎是一种协作式(即非抢占式)软线程解决方案,根据我的经验,这只会增加编写并行代码的难度。跨度>
                      【解决方案12】:

                      使用扭曲。 “Twisted 是一个用 Python 编写的事件驱动的网络引擎”http://twistedmatrix.com/trac/。有了它,我可以在不使用线程的情况下一次发出 100 个异步 http 请求。

                      【讨论】:

                      • 不要把例子中的问题丢了。
                      【解决方案13】:

                      您的具体示例很少使用多线程解决。正如许多人所说,这类问题是 IO 密集型的,这意味着处理器要做的工作很少,并且大部分时间都在等待一些数据通过线路到达并进行处理,同样它必须等待用于刷新磁盘缓冲区,以便它可以将更多最近下载的数据放在磁盘上。

                      性能的方法是通过 select() 工具或等效的系统调用。基本过程是打开一些套接字(用于网络爬虫下载)和文件句柄(用于将它们存储到磁盘)。接下来,您将所有不同的套接字和 fh 设置为非阻塞模式,这意味着在发出请求后,它不会让您的程序等待直到数据可以读取,而是立即返回一个特殊代码(通常是 EAGAIN)来指示没有数据准备好。如果您以这种方式循环通过所有套接字,您将进行轮询,这很好,但仍然浪费 cpu 资源,因为您的读取和写入几乎总是会返回 EAGAIN。

                      为了解决这个问题,所有的套接字和 fp 将被收集到一个“fd_set”中,它被传递给 select 系统调用,然后你的程序将阻塞,等待任何一个套接字,并唤醒你的程序当任何流上有一些数据要处理时。


                      另一个常见的情况,计算绑定工作,毫无疑问,最好通过某种真正的并行性(与上面介绍的异步并发相反)来访问多个 cpu 的资源。如果您的 cpu 绑定任务在单线程架构上运行,请务必避免任何并发,因为开销实际上会减慢您的任务。

                      【讨论】:

                        【解决方案14】:

                        线程是不可回避的,它们也不是“困难的”。函数式编程也不一定是答案。 .NET 框架使线程相当简单。稍加思考,您就可以制作出合理的多线程程序。

                        这是您的网络爬虫示例(在 VB.NET 中)

                        
                        Imports System.Threading
                        Imports System.Net
                        
                        Module modCrawler
                        
                            Class URLtoDest
                                Public strURL As String
                                Public strDest As String
                                Public Sub New(ByVal _strURL As String, ByVal _strDest As String)
                                    strURL = _strURL
                                    strDest = _strDest
                                End Sub
                            End Class
                        
                            Class URLDownloader
                        
                                Public id As Integer
                                Public url As URLtoDest
                        
                                Public Sub New(ByVal _url As URLtoDest)
                                    url = _url
                                End Sub
                        
                                Public Sub Download()
                        
                                    Using wc As New WebClient()
                                        wc.DownloadFile(url.strURL, url.strDest)
                                        Console.WriteLine("Thread Finished - " & id)
                                    End Using
                        
                                End Sub
                        
                            End Class
                        
                            Public Sub Download(ByVal ud As URLtoDest)
                                Dim dldr As New URLDownloader(ud)
                                Dim thrd As New Thread(AddressOf dldr.Download)
                                dldr.id = thrd.ManagedThreadId
                                thrd.SetApartmentState(ApartmentState.STA)
                                thrd.IsBackground = False
                                Console.WriteLine("Starting Thread - " & thrd.ManagedThreadId)
                                thrd.Start()        
                            End Sub
                        
                            Sub Main()
                        
                                Dim lstUD As New List(Of URLtoDest)
                        
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file0.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file1.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file2.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file3.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file4.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file5.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file6.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file7.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file8.txt"))
                                lstUD.Add(New URLtoDest("http://stackoverflow.com/questions/382478/how-can-threads-be-avoided", "c:\file9.txt"))
                        
                                For Each ud As URLtoDest In lstUD
                                    Download(ud)
                                Next
                        
                                ' you will see this message in the middle of the text
                                ' pressing a key before all files are done downloading aborts the threads that aren't finished
                                Console.WriteLine("Press any key to exit...")
                                Console.ReadKey()
                        
                            End Sub
                        
                        End Module
                        
                        

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 2011-03-21
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2014-04-12
                          • 2012-11-19
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多