class Lock {}

Lock 是一个低级别的并发控制构造。它提供互斥,这意味着一次只能有一个线程持有锁。一旦锁被解锁,另一个线程就可以锁定它。

Lock 通常用于保护对一个或多个状态部分的访问。例如,在此程序中

my $x = 0;
my $l = Lock.new;
await (^10).map: {
    start {
        $l.protect({ $x++ });
    }
}
say $x;         # OUTPUT: «10␤»

Lock 用于保护对 $x 的操作。增量不是原子操作;如果没有锁,两个线程都可能读取数字 5,然后都将数字 6 存储回,从而丢失更新。使用 Lock 时,一次只能有一个线程运行增量。

Lock 是可重入的,这意味着持有锁的线程可以在不阻塞的情况下再次锁定它。该线程必须解锁相同次数,然后另一个线程才能获取锁(它的工作原理是保持递归计数)。

重要的是要理解,Lock 与任何特定数据部分之间没有直接联系;由程序员确保在涉及相关数据的操作期间始终持有 LockOO::Monitors 模块虽然不是此问题的完整解决方案,但确实提供了一种避免显式处理锁并鼓励采用更结构化方法的方法。

Lock 类由操作系统提供的构造支持,因此,从操作系统的角度来看,等待获取锁的线程被阻塞。

使用高级 Raku 并发构造的代码应避免使用 Lock。等待获取 Lock 会阻塞一个真正的 Thread,这意味着线程池(由多个高级 Raku 并发机制使用)在此期间无法将该线程用于其他任何用途。

持有 Lock 时执行的任何 await 都将以阻塞方式运行;await 的标准非阻塞行为依赖于在不同的 Thread 上恢复 `await` 之后的代码,这与 Lock 必须由锁定它的同一线程解锁的要求不兼容。有关没有此缺点的替代机制,请参阅 Lock::Async。除此之外,主要区别在于 Lock 主要映射到操作系统机制,而 Lock::Async 使用 Raku 原语来实现类似的效果。如果您正在执行低级别操作(本机绑定)和/或实际上想要阻塞真正的操作系统线程,请使用 Lock。但是,如果您想要一个非阻塞互斥,并且不需要递归并且正在 Raku 线程池上运行代码,请使用 Lock::Async。

Lock 本质上不可组合,并且如果出现对锁的循环依赖,就有可能导致挂起。更喜欢对并发程序进行结构化,以便它们传达结果而不是修改共享数据结构,使用诸如 PromiseChannelSupply 之类的机制。

方法§

方法 protect§

multi method protect(Lock:D: &code)

获取锁,运行 &code,然后释放锁。即使代码因异常而退出,也要确保释放锁。

请注意,Lock 本身需要在代码中被线程化和需要保护的部分之外创建。在下面的第一个示例中,Lock 首先被创建并分配给 $lock,然后在 Promise内部使用它来保护敏感代码。在第二个示例中,出现了一个错误:Lock 直接在 Promise 内部创建,因此代码最终会产生一堆独立的锁,在许多线程中创建,因此它们实际上并不能保护我们想要保护的代码。

# Right: $lock is instantiated outside the portion of the 
# code that will get threaded and be in need of protection 
my $lock = Lock.new;
await ^20 .map: {
    start {
        $lock.protect: {
            print "Foo";
            sleep rand;
            say "Bar";
        }
    }
}
 
# !!! WRONG !!! Lock is created inside threaded area! 
await ^20 .map: {
    start {
        Lock.new.protect: {
            print "Foo"sleep randsay "Bar";
        }
    }
}

方法 lock§

method lock(Lock:D:)

获取锁。如果当前不可用,则等待。

my $l = Lock.new;
$l.lock;

由于 Lock 是使用操作系统提供的设施实现的,因此等待锁的线程在锁可供其使用之前不会被调度。由于 Lock 是可重入的,因此如果当前线程已经持有锁,则调用 lock 只会增加递归计数。

虽然使用 lock 方法很容易,但正确使用 unlock 却比较困难。相反,最好使用 protect 方法,它负责确保 lock/unlock 调用始终同时发生。

方法 unlock§

method unlock(Lock:D:)

释放锁。

my $l = Lock.new;
$l.lock;
$l.unlock;

确保始终释放 Lock 非常重要,即使抛出异常也是如此。确保这一点的最安全方法是使用 protect 方法,而不是显式调用 lockunlock。如果没有,请使用 LEAVE 相位器。

my $l = Lock.new;
{
    $l.lock;
    LEAVE $l.unlock;
}

方法 condition§

method condition(Lock:D: )

将条件变量作为 Lock::ConditionVariable 对象返回。查看 这篇文章维基百科,了解有关条件变量及其与锁和互斥体的关系的背景信息。

my $l = Lock.new;
$l.condition;

当您希望与锁的交互比简单获取或释放锁更复杂时,您应该在锁上使用条件。

constant ITEMS = 100;
my $lock = Lock.new;
my $cond = $lock.condition;
my $todo = 0;
my $done = 0;
my @in = 1..ITEMS;
my @out = 0 xx ITEMS;
 
loop ( my $i = 0$i < @in$i++ ) {
    my $in := @in[$i];
    my $out := @out[$i];
    Thread.start{
      my $partial = $in² +1;
      if $partial.is-prime {
          $out = $partial but "Prime";
      } else {
          $out = $partial;
      }
      $lock.protect{
         $done++;
         $cond.signal if $done == $todo;
      } );
    } );
    $todo++;
}
$lock.protect{
    $cond.wait({  $done == $todo } );
});
 
say @out.map: { $_.^roles > 2 ?? $_.Num ~ "*" !! $_ };
# OUTPUT: «2* 5* 10 17* 26 37* 50 65 82 101* … » 

在本例中,我们使用条件变量 $cond 等待,直到所有数字都生成并检查,并且还 .signal 到另一个线程,以便在特定线程完成后唤醒它。

类型图§

Lock 的类型关系
raku-type-graph Lock Lock Any Any Lock->Any Mu Mu Any->Mu

展开上面的图表