我最近读过很多关于如何编写多线程应用程序是一个巨大的痛苦,并且已经充分了解了该主题,至少在某种程度上,为什么会这样理解。
我已经读过使用函数式编程技术可以帮助缓解一些痛苦,但我从未见过一个简单的并发功能代码示例。那么,使用线程有哪些替代方案呢?至少,有哪些方法可以将它们抽象出去,这样你就不必考虑锁定以及特定库的对象是否是线程安全的。
我知道Google的MapReduce应该可以解决这个问题,但我还没有看到它的简洁解释。
虽然我在下面给出一个具体的例子,但我对通用技术比对解决这个特定问题更感兴趣(使用示例来帮助说明其他技术会有所帮助)。
当我编写一个简单的网络爬虫作为学习练习时,我提出了这个问题。它工作得很好,但速度很慢。大部分瓶颈来自下载页面。它目前是单线程的,因此一次只下载一个页面。因此,如果可以同时下载页面,即使爬虫在单个处理器计算机上运行,也会大大加快速度。我考虑使用线程来解决问题,但他们吓到了我。关于如何在不释放可怕的线程噩梦的情况下为这类问题添加并发性的任何建议?
答案 0 :(得分:22)
函数式编程有助于并发的原因并不是因为它避免了使用线程。
相反,函数式编程提倡不变性,并且没有副作用。
这意味着可以将操作扩展到N个线程或进程,而不必担心混乱共享状态。
答案 1 :(得分:9)
实际上,在你需要同步它们之前,线程很容易处理。通常,您使用线程池添加任务并等待它们完成。
当线程需要通信并访问共享数据结构时,多线程变得非常复杂。一旦你有两个锁,你就会遇到死锁,这就是多线程变得非常困难的地方。有时,只需几条指令就可能导致锁定代码错误。在这种情况下,您只能看到生产中的错误,在多核计算机上(如果您在单核上开发,发生在我身上),或者它们可能由其他一些硬件或软件触发。单元测试在这里没有多大帮助,测试发现了错误,但你可能永远不会像“普通”应用程序那样确定。
答案 2 :(得分:8)
我将添加一个示例,说明如何使用功能代码安全地使代码并发。
以下是您可能希望并行执行的一些代码,因此您无需等待一个文件完成以开始下载下一个文件:
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提供了这样的功能。因此,通过使用函数式编程,您可以更改一行代码,并突然有一些并行运行的东西,用于顺序运行。这非常强大。
答案 3 :(得分:4)
有一些关于异步模型的简短提及,但没有人真正解释过它,所以我认为我会讨论。我见过的最常用的方法是多线程的替代方案是异步架构。所有这些意味着,不是在单个线程中按顺序执行代码,而是使用轮询方法启动某些功能,然后返回并定期检查,直到有可用的数据。
这实际上只适用于像前面提到的爬虫这样的模型,其中真正的瓶颈是I / O而不是CPU。从广义上讲,异步方法会在几个套接字上启动下载,并且轮询循环会定期检查它们是否已完成下载,完成后,我们可以继续下一步。这允许您通过同一线程内的上下文切换来运行在网络上等待的多个下载。
除了使用单独的线程而不是轮询循环检查同一线程中的多个套接字之外,多线程模型的工作方式大致相同。在I / O绑定应用程序中,异步轮询几乎与许多用例的线程一样,因为真正的问题只是等待I / O完成而不是等待CPU处理数据。 / p>
另一个现实世界的例子是需要执行许多其他可执行文件并等待结果的系统。这可以在线程中完成,但它也相当简单,并且几乎同样有效地将几个外部应用程序作为Process对象触发,然后定期检查,直到它们全部执行完毕。这会将CPU密集型部分(运行代码放在外部可执行文件中)放在自己的进程中,但数据处理都是异步处理的。
我工作的Python ftp服务器库,pyftpdlib使用Python asyncore库来处理只有一个线程的FTP客户端,以及用于文件传输和命令/响应的异步套接字通信。
有关进一步阅读Asynchronous Programming上的Python Twisted库页面的信息,请参阅 - 虽然有些特定于使用Twisted,但它也从初学者的角度介绍了异步编程。
答案 4 :(得分:1)
并发性是计算机科学中一个非常复杂的主题,它要求对硬件体系结构以及操作系统行为有很好的理解。
多线程有许多基于您的硬件和托管操作系统的实现,并且尽管很难实现,但是陷阱很多。应该注意的是,为了实现“真正的”并发,线程是唯一的方法。基本上,线程是您作为程序员在软件的不同部分之间共享资源同时允许它们并行运行的唯一方式。通过 parallel ,你应该考虑标准CPU(双核/多核)只能一次做一件事。像上下文切换这样的概念现在发挥作用,它们有自己的一套规则和限制。
在你开始在程序中实现并发之前,我认为你应该像你所说的那样寻求更多关于这个主题的通用背景。
我想最好的起点是wikipedia article on concurrency,然后从那里开始。
答案 5 :(得分:1)
通常使多线程编程成为一场噩梦的是线程共享资源和/或需要相互通信的时候。在下载网页的情况下,您的线程将独立工作,因此您可能没有太多麻烦。
您可能想要考虑的一件事是产生多个进程而不是多个线程。在你提到的情况下 - 同时下载网页 - 你可以将工作负载分成多个块,并将每个块交给一个单独的工具实例(如cURL)来完成工作。
答案 6 :(得分:1)
如果您的目标是实现并发,那么很难摆脱使用多个线程或进程。诀窍不是避免它,而是以可靠且不容易出错的方式管理它。死锁和竞争条件尤其是并发编程的两个方面容易出错。管理这一点的一种通用方法是使用生产者/消费者队列......线程将工作项写入队列,工作人员从中拉出项目。您必须确保正确同步对队列的访问权限并进行设置。
此外,根据您的问题,您可能还可以创建一个特定于域的语言来消除并发问题,至少从使用您的语言的人的角度来看...当然是处理该语言的引擎仍然需要处理并发,但如果这将在许多用户中得到利用,那么它可能是有价值的。
答案 7 :(得分:1)
那里有一些好的图书馆。
java.util.concurrent.ExecutorCompletionService将收集Futures(即返回值的任务)的集合,在后台线程中处理它们,然后在队列中将它们打包,以便您在完成时进一步处理。当然,这是Java 5及更高版本,所以无处可用。
换句话说,您的所有代码都是单线程的 - 但是您可以识别并行运行的东西,您可以将其移植到合适的库中。
重点是,如果你可以独立完成任务,那么通过一点思考就不可能实现线程安全 - 尽管强烈建议你将复杂的位(如实现ExecutorCompletionService)留给专家......
答案 8 :(得分:0)
在简单场景中避免线程化的一种简单方法是从不同进程下载。主进程将使用将文件下载到本地目录的参数调用其他进程,然后主进程可以完成实际工作。
我认为这些问题没有任何简单的解决办法。它不是一个线程问题。它是制造人类思维的并发性。
答案 9 :(得分:0)
您可以使用F#语言观看MSDN视频:PDC 2008: An introduction to F#
这包括您正在寻找的两件事。 (功能+异步)
答案 10 :(得分:0)
对于python,这看起来像一个有趣的方法:http://members.verizon.net/olsongt/stackless/why_stackless.html#introduction
答案 11 :(得分:0)
不应避免线程,也不要“难”。功能编程也不一定是答案。 .NET框架使线程相当简单。稍加思考就可以制作合理的多线程程序。
以下是您的webcrawler示例(在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
答案 12 :(得分:0)
使用扭曲。 “Twisted是一个用Python编写的事件驱动的网络引擎”http://twistedmatrix.com/trac/。有了它,我可以在不使用线程的情况下一次制作100个异步http请求。
答案 13 :(得分:0)
您的具体示例很少通过多线程解决。正如许多人所说,这类问题是IO限制的,这意味着处理器几乎没有工作要做,并且大部分时间花在等待一些数据通过线路到达并处理它,同样它必须等待磁盘缓冲区要刷新,以便它可以将更多最近下载的数据放在磁盘上。
性能的方法是通过select()工具或等效的系统调用。基本过程是打开许多套接字(用于Web爬虫下载)和文件句柄(用于将它们存储到磁盘)。接下来,将所有不同的套接字和fh设置为非阻塞模式,这意味着不是让程序等到发出请求后数据可供读取,而是立即返回一个特殊代码(通常为EAGAIN)来指示没有数据准备就绪。如果以这种方式循环遍历所有套接字,那么您将进行轮询,这很有效,但仍然浪费cpu资源,因为您的读取和写入几乎总是会返回EAGAIN。
为了解决这个问题,所有套接字和fp将被收集到'fd_set'中,然后传递给select系统调用,然后你的程序将阻塞,等待任何套接字,并唤醒你的程序当要处理的任何流上有一些数据时。
另一个常见的情况,即计算绑定工作,毫无疑问是通过某种真正的并行性(与上面提到的异步并发相关)来访问多个cpu的资源。如果您的cpu绑定任务在单线程结构上运行,那么肯定会避免任何并发,因为开销实际上会降低您的任务速度。