使用Perl将并发附加到同一文件

使用Perl将并发附加到同一文件,perl,concurrency,locking,Perl,Concurrency,Locking,我需要升级Perl CGI脚本,其中用户必须完成3个步骤。完成每个步骤后,脚本将记录用户完成的步骤。记录这一点很重要,因此我们可以向用户证明,例如,他们只完成了第一步,没有完成所有三个步骤 现在,脚本正在为CGI脚本的每个实例创建一个日志文件。因此,如果UserA执行步骤1,那么UserB执行步骤1,然后执行步骤2,然后执行步骤3-然后UserA完成步骤2和步骤3,那么日志文件的顺序将是 LogFile.UserA.Step1 LogFile.UserB.Step1 LogFile.UserB.

我需要升级Perl CGI脚本,其中用户必须完成3个步骤。完成每个步骤后,脚本将记录用户完成的步骤。记录这一点很重要,因此我们可以向用户证明,例如,他们只完成了第一步,没有完成所有三个步骤

现在,脚本正在为CGI脚本的每个实例创建一个日志文件。因此,如果UserA执行步骤1,那么UserB执行步骤1,然后执行步骤2,然后执行步骤3-然后UserA完成步骤2和步骤3,那么日志文件的顺序将是

LogFile.UserA.Step1
LogFile.UserB.Step1
LogFile.UserB.Step2
LogFile.UserB.Step3
LogFile.UserA.Step2
LogFile.UserA.Step3
日志文件以当前时间戳、随机数和进程PID命名

这可以很好地防止同一个文件被多次写入,但目录很快就会得到数千个文件(每个文件中只包含几个字节)。有一个循环和压缩这些日志的过程,但我必须这样做,以便脚本每天只记录一个文件,以减少正在创建的日志文件的数量

基本上,日志文件的文件名中将包含当前日期,只要CGI脚本需要写入日志,它就会附加到当天的一个日志文件中,而不管用户或他们在执行什么步骤

不需要读取日志文件-唯一会发生在日志文件上的事情是CGI脚本的附加。日志循环将在7天或更早的日志文件上运行

我的问题是,处理此日志文件的并发附件的最佳方法是什么?附加之前是否需要锁定它?我在Perl Monks上发现,这似乎表明“当多个进程写入同一个文件,并且所有进程都打开该文件进行附加时,数据不会被覆盖。”

我明白了,仅仅因为它可以做到并不意味着我应该这样做,但在这种情况下,什么是最安全的,最好的做法是这样做的

总结:

  • 并发附加到同一文件
  • 每个附加到文件的内容只有一行,少于50个字符
  • 秩序无关紧要
谢谢

“当多个进程写入同一个文件,并且所有进程都打开了该文件以进行附加时,数据不应被覆盖”可能是真的,但这并不意味着您的数据不能被破坏(一个条目在另一个条目中)。对于少量的数据,这种情况不太可能发生,但可能会发生


flock
是解决该问题的可靠且相当简单的解决方案。我建议你简单地使用它。

你可以尝试使用文件锁定,但这会很快将你带到受伤的地方。更简单的方法是使用一个小的持久化进程或cron作业来扫描日志文件目录,并将事件一次一个地附加到日志文件中


为了更安全,您可以让日志脚本每隔一段时间(比如5分钟)创建一个新的日志文件,并让守护进程忽略小于5分钟的文件。

我想我应该运行一个单独的进程,例如使用Net::daemon或类似的程序,以集中的方式处理日志条目的写入。CGI脚本实例将通过套接字将日志字符串传递给此守护进程。

您有几个选项,按复杂性的增长顺序排列:

1) 每一行都有时间和邮戳。当需要检查合并的文件时,可以交错所有输入文件

2) 编写一个始终运行的脚本,使所有文件句柄保持打开状态,并使用select()查找包含新数据的文件,并按接收顺序将其转储到输出。这个方法可能会成为资源占用者,因为它会不断地调用select,然后查找新文件,然后打开新文件,然后再次调用select


3) 编写一个接受TCP连接的脚本。如果您最终遇到这样一种情况,即记录器每次打开的日志文件比操作系统中的进程所能支持的要多,那么您就回到了解决方案1。老实说,选择1号。

是的,使用
flock

下面是一个示例程序,从典型的前端开始:

#! /usr/bin/perl

use warnings;
use strict;

use Fcntl qw/ :flock /;
然后,我们指定日志路径和将运行的客户端数:

my $log = "/tmp/my.log";
my $clients = 10;
要记录消息,请在追加模式下打开文件,以便所有写入操作都自动结束。然后调用
flock
等待轮到我们以独占方式访问日志。我们起床后,写下消息并关闭把手,它会自动释放锁

sub log_step {
  my($msg) = @_;

  open my $fh, ">>", $log or die  "$0 [$$]: open: $!";
  flock $fh, LOCK_EX      or die  "$0 [$$]: flock: $!";
  print $fh "$msg\n"      or die  "$0 [$$]: write: $!";
  close $fh               or warn "$0 [$$]: close: $!";
}
现在
fork
off
$clients
子进程以随机间隔完成以下三个步骤:

my %kids;
my $id = "A";
for (1 .. $clients) {
  my $pid = fork;
  die "$0: fork: $!" unless defined $pid;

  if ($pid) {
    ++$kids{$pid};
    print "$0: forked $pid\n";
  }
  else {
    my $user = "User" . $id;
    log_step "$user: Step 1";
    sleep rand 3;
    log_step "$user: Step 2";
    sleep rand 3;
    log_step "$user: Step 3";
    exit 0;
  }

  ++$id;
}
不要忘记等待所有孩子退出:

print "$0: reaping children...\n";
while (keys %kids) {
  my $pid = waitpid -1, 0;
  last if $pid == -1;

  warn "$0: unexpected kid $pid" unless $kids{$pid};
  delete $kids{$pid};
}

warn "$0: still running: ", join(", " => keys %kids), "\n"
  if keys %kids;

print "$0: done!\n", `cat $log`;
样本输出:

[...] ./prog.pl: reaping children... ./prog.pl: done! UserA: Step 1 UserB: Step 1 UserC: Step 1 UserC: Step 2 UserC: Step 3 UserD: Step 1 UserE: Step 1 UserF: Step 1 UserG: Step 1 UserH: Step 1 UserI: Step 1 UserJ: Step 1 UserD: Step 2 UserD: Step 3 UserF: Step 2 UserG: Step 2 UserH: Step 2 UserI: Step 2 UserI: Step 3 UserB: Step 2 UserA: Step 2 UserA: Step 3 UserE: Step 2 UserF: Step 3 UserG: Step 3 UserJ: Step 2 UserJ: Step 3 UserE: Step 3 UserH: Step 3 UserB: Step 3 [...] /prog.pl:收获孩子。。。 /prog.pl:完成! UserA:步骤1 用户B:步骤1 UserC:步骤1 用户C:步骤2 用户C:步骤3 UserD:步骤1 用户E:步骤1 UserF:步骤1 UserG:步骤1 UserH:步骤1 UserI:步骤1 UserJ:步骤1 用户D:步骤2 用户D:步骤3 UserF:步骤2 UserG:步骤2 UserH:步骤2 UserI:步骤2 UserI:步骤3 用户B:步骤2 UserA:第2步 UserA:步骤3 用户E:步骤2 UserF:步骤3 UserG:步骤3 UserJ:步骤2 UserJ:步骤3 用户E:步骤3 UserH:步骤3 用户B:步骤3
请记住,每次运行的顺序都不同。

我敦促Log::Log4Perl

gbacon做到这一点是正确的,但在修改代码时要记住一点:您不解锁(
锁定_UN
)文件,而是关闭它。这将确保数据被刷新,然后解锁。谢谢。秩序并不重要,所以这不是问题。我不完全确定我是否需要插手我的案子。由于这是一个CGI脚本(不是快速CGI-它不会保持活动状态),用户在脚本生命周期内只能执行一个步骤-一旦完成一个步骤,脚本将退出。然后,在web上,他正在处理第2步,点击提交,第2步将被记录,脚本将退出。@BrianH