【问题标题】:Is it bad practice to just kick off new threads for blocking operations (Perl)为阻塞操作启动新线程是不好的做法吗(Perl)
【发布时间】:2013-06-24 13:29:45
【问题描述】:

如果执行 CPU 密集型任务,我认为每个内核有一个线程是最佳选择。如果您有一个 4 核 CPU,您可以运行 4 个 CPU 密集型子例程实例而不会受到任何惩罚。例如,我曾经在一个四核 CPU 上实验性地运行了四个 CPU 密集型算法实例。每个过程最多四倍的时间没有减少。在第五个实例中,所有实例都花费了更长的时间。

阻塞操作的情况是什么?假设我有一个包含 1,000 个 URL 的列表。我一直在做以下事情:

(请不要介意任何语法错误,我只是模拟了这个)

my @threads;
foreach my $url (@urlList) {    
     push @threads, async {
         my $response = $ua->get($url);
         return $response->content;   
     }
}

foreach my $thread (@threads) {
    my $response = $thread->join;
    do_stuff($response); 
}

我基本上启动了与 URL 列表中的 URL 一样多的线程。如果有一百万个 URL,那么就会启动一百万个线程。这是最优的,如果不是最优线程数是多少?对于任何可以等待的阻塞 I/O 操作(读取文件、数据库查询等),使用线程是一种好习惯吗?

相关奖金问题

出于好奇,Perl 线程的工作方式与 Python 和 GIL 相同吗?使用 python 来获得多线程的好处并利用所有内核执行 CPU 密集型任务,您必须使用多处理。

【问题讨论】:

  • perl 没有像GIL 这样的东西,所以你可以放松一下 :) 另一方面,perl 线程很消耗内存,但如果你有足够的资源就去吧。

标签: multithreading perl thread-safety blocking


【解决方案1】:

出于好奇,Perl 线程的工作方式与 Python 和 GIL 相同吗?使用 python 来获得多线程的好处并利用所有内核执行 CPU 密集型任务,您必须使用多处理。

不,但结论是一样的。 Perl 没有有一个大锁来保护跨线程的解释器;相反,它对每个不同的线程都有一个重复的解释器。由于一个变量属于一个解释器(并且只有一个解释器),因此默认情况下线程之间不会共享任何数据。当变量被显式共享时,它们被放置在一个共享解释器中,该解释器代表其他线程序列化对共享变量的所有访问。除了这里其他人提到的内存问题外,Perl 中的线程也存在一些严重的性能问题,以及对可以共享的数据类型以及您可以用它做什么的限制(更多信息请参阅perlthrtut信息)。

结果是,如果您需要并行化大量 IO 并且可以使其成为非阻塞的,那么您将从事件循环模型中获得比线程更高的性能。如果你需要并行化那些不能变成非阻塞的东西,你可能会比使用 perl 线程更幸运(一旦你熟悉了那种代码,也更容易调试)。

也可以将这两种模型结合起来(例如,一个主要是单进程事件的应用程序,它使用 POE::Wheel::RunAnyEvent::Run 将某些昂贵的工作传递给子进程,或者一个多进程应用程序具有事件父管理非事件子,或节点集群类型设置,其中您有许多预分叉事件网络服务器,父仅accepts 并将 FD 传递给其子)。

不过,没有灵丹妙药,至少现在还没有。

【讨论】:

  • 或者也可以是IO::Async::Routine或者::Function,也是很相似的思路。
  • @LeoNerd 谢谢,我不是有意将你排除在外,但我不知道 IO::Async 案例中的正确模块是什么 :)
【解决方案2】:

从这里: http://perldoc.perl.org/threads.html

内存消耗

在大多数系统上,频繁且持续地创建和销毁线程会导致 Perl 解释器的内存占用不断增加。虽然启动线程然后 ->join() 或 ->detach() 很简单,但对于长期存在的应用程序,最好维护一个线程池,并将它们重用于所需的工作,使用队列来通知线程待处理的工作。该模块的 CPAN 分发版包含一个简单示例 (examples/pool_reuse.pl),说明了可重用线程池的创建、使用和监控。

【讨论】:

    【解决方案3】:

    让我们看看你的代码。我发现它存在三个问题:

    1. 简单的第一:你使用->content而不是->decoded_content(charset => 'none')

      ->content 返回原始 HTML 响应主体,如果没有标头中的信息来解码它(例如,它可能被压缩),它是无用的。它有时会起作用。

      ->decoded_content(charset => 'none') 为您提供实际响应。它始终有效。

    2. 您处理订单请求中的响应。这意味着您可能会在响应等待服务时被阻止。

      最简单的解决方案是将响应放在Thread::Queue::Any 对象中。

      use Thread::Queue::Any qw( );
      
      my $q = Thread::Queue::Any->new();
      
      my $requests = 0;
      for my $url (@urls) {
         ++$requests;
         async {
            ...
            $q->enqueue($response);
         };
      }
      
      while ($requests && my $response = $q->dequeue()) {
         --$requests;
         $_->join for threads->list(threads::joinable);
         ...
      }
      
      $_->join for threads->list();
      
    3. 您创建了许多只使用一次的线程。

      这种方法会产生大量开销。一个常见的多线程实践是创建一个持久工作线程池。这些工人执行任何需要完成的工作,然后继续下一个工作而不是退出。作业到池而不是特定线程,以便作业可以尽快启动。除了消除线程创建开销之外,这还允许控制一次运行的线程数。这对于 CPU 密集型任务非常有用。

      但是,您的需求有所不同,因为您使用线程来执行异步 IO。线程创建的 CPU 开销不会对您产生太大影响(尽管它可能会导致启动滞后)。内存相当便宜,但您使用的内存仍然远远超过您的需要。线程确实不适合这项任务。

      执行异步 IO 的系统要好得多,但它们不一定可以从 Perl 轻松获得。但是,在您的具体情况下,最好避免使用线程并使用Net::Curl::Multi按照概要中的示例,您将获得一个能够制作并行网络的非常快速的引擎开销很小的请求。

      我们在我的前任雇主已经切换到 Net::Curl::Multi 没有问题的高负载任务关键型网站,我们喜欢它。

      如果您想限制对周围代码的更改,可以很容易地创建一个创建 HTTP::Response 对象的包装器。 (我们就是这种情况。)请注意,有一个reference 到底层库(libcurl)是有帮助的,因为 Perl 代码是底层库的一个薄层,因为文档非常好,而且因为它记录您可以提供的所有选项。

    【讨论】:

      【解决方案4】:

      您可能只想考虑使用非阻塞用户代理。我喜欢Mojo::UserAgent,它是Mojolicious 套件的一部分。您可能想看一个我为another question 模拟的非阻塞爬虫示例。

      【讨论】:

      • 谢谢。用户代理就是一个例子,但我也在询问“长时间运行 I/O”的一般情况。它可能是 URL 获取、数据库查询、文件读取或其他我还没有想到的东西。我想知道线程是否是处理这个问题的方法以及如何优化问题。 (还是)感谢你的建议。 Mojo 不会做类似于“在引擎盖下”穿线的事情吗?
      • Mojolicious 有自己的事件循环,如果可用,它将使用 EV(以及 AnyEvent)。它自己的循环和 AE 都有文件监视程序等。我从未在多核/线程应用程序中使用过这些,但我确信有人已经完成了这项工作(可能?)。我可能会先看看 AnyEvent 和 Coro。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-21
      相关资源
      最近更新 更多