草庐IT

(原创)多线程并发:AQS源码分析(2)——共享锁的实现原理

一只烤鸭朝北走 2023-03-28 原文

  在上一篇文章多线程并发(一)中我们通过acquire()详细地分析了AQS中的独占锁的获取流程,提到独占锁,自然少不了共享锁,所以我们这边文章就以AQS中的acquireShared()方法为例,来分析下并发编程中共享锁的获取与释放吧,获取共享锁的大体流程和获取独占锁一样,但是因为共享锁可以被多个线程同时持有,所以共享锁比起独占锁来可能更复杂,文章有点长,静下心来,慢慢读,读完之后可能会使你收获颇多。

   通过上篇文章的分析,我们发现AQS中主要做三件事:1、同步状态的state的获取和释放,即同步状态的管理;2、同步队列的维护;3、线程的阻塞和唤醒,即线程间的协作;AQS中定义了大量的同步状态管理的模板方法,比如acquireShared()就是一个线程获取共享锁的入口方法,我们就从这个方法开始我们的共享锁之旅吧!

  1、acquireShared(int arg)方法:

1 public final void acquireShared(int arg) {
2         //获取共享资源成功直接返回
3         if (tryAcquireShared(arg) < 0)
4             //获取资源不成功执行此方法阻塞
5             doAcquireShared(arg);
6     }

  这个方法是AQS中定义的一个模版方法,也是获取共享锁的入口,调用tryAcquireShared()尝试获取共享锁,如果获取共享锁成功,则此方法直接返回;获取共享锁不成功,则执行doAcquireShared()方法,将当前节点包装成Node节点,加入到同步队列中,进行阻塞,直到被其它线程唤醒了,成功获取到了共享锁再返回。是不是和获取独占锁的流程很类似呢?是的,大致流程基本一致,但是两者最大的区别共享锁在同一时刻只能被一个线程持有,而共享锁在同一时刻可能会被多个线程同时持有,所以共享锁比独占锁更复杂。其中这个tryAcquireShared()方法我们这个模版类中只提供了定义,并没有提供实现,具体实现还需要自定义的同步器去实现。我们接着往下看doAcquireShared()方法:

  2、doAcquireShared方法:

 1     private void doAcquireShared(int arg) {
 2         //将当前线程加入到同步队列中,并标记为共享模式
 3         final Node node = addWaiter(Node.SHARED);
 4         //线程阻塞等待获取共享资源的过程中是否发生了异常
 5         boolean failed = true;
 6         try {
 7             //现在在阻塞等待获取资源的过程中,其它线程对此线程是否发生了中断请求
 8             boolean interrupted = false;
 9            
10             /*自旋,找到合适点的点将当前线程挂起,再寻找合适点的过程中也不断尝试重新获取共享锁,因为可能再这个尝试的过程中,其它线程释放了共享锁*/
11             for (;;) {
12                 //找到当前节点的前继节点
13                 final Node p = node.predecessor();
14                 
15                 //当前节点的前继节点是同步队列的头节点
16                 if (p == head) {
17                     //尝试获取指定量的共享资源
18                     int r = tryAcquireShared(arg);
19                     //当前Node节点的线程成功获取到了共享资源
20                     if (r >= 0) {
21                         //将当前线程Node节点设置为head头节点,并尝试唤醒后面的阻塞节点
22                         setHeadAndPropagate(node, r);
23                         p.next = null; // help GC
24                         //等待获取资源的过程中发生了线程中断的请求是不响应线程中断的,所以这里要将线程中断补上。
25                         if (interrupted)
26                             //获取独占锁是放在acquire()方法中处理的,不过作用都一样。
27                             selfInterrupt();
28                         //表示获取阻塞获取资源的过程中没有发生异常,就不用执行finally中的取消方法了。
29                         failed = false;
30                         return;
31                     }
32                 }
33                 
34                 //找到当前被阻塞线程节点的前继有效节点,将它的状态设置为Node.SIGNAL
35                 if (shouldParkAfterFailedAcquire(p, node) &&
36                         /**
37                          * 找到了有效前继节点并它的状态设置为Node.SIGNAL,那么我们就可以将当前节点park(),
38                          * 等待前继节点释放资源后唤醒它,唤醒之后在进行一次线程中断检测,进入下次"自旋"。
39                          */
40                     parkAndCheckInterrupt())
41                     interrupted = true;
42             }
43         } finally {
44             //阻塞等待获取共享资源的时候发生了异常,需要将当前Node节点出队,上一篇文章中讲过,这里就不再赘述了。
45             if (failed)
46                 cancelAcquire(node);
47         }
48     }

  上面已经说过这个方法主要干两件事情:1、将阻塞的线程包装成Node节点,加入到同步队列中;2、通过一定次数的“自旋”操作,当前线程找到合适的点,将自己挂起,等待其它线程唤醒。

  在寻找这个“合适点”(这个合适点的选择,上篇文章多线程并发(一):以AQS中acquire()方法为例来分析多线程间的同步与协作中提到过,有不清楚的可以在这里找到答案)的过程中,有可能其它线程释放了共享锁,那么当前线程应该检查下有没有资格获取,有资格获取,并且获取成功,那么就将它自己设置为头节点,然后唤醒后继节点之后再返回,至此获取锁的整个流程就完了。

  细心的读者可能发现,这个“自旋”中,将尝试获取锁放在前面,将阻塞判定放在后面执行,现在想想这是不是一个类型do{}while()模型呢,要是第一次直接获取锁成功了,是不是线程就少了一次阻塞----》唤醒的状态转化呢?

  上面方法中也提到过,“如果当前线程节点的前继节点是队列head节点时,我们就可以尝试获取一次共享资源”。为什么当前节点的前继节点不是head节点的时候,就不能尝试获取共享锁呢?这是因为当前这个LCH同步队列是严格按照FIFO出队的,当前节点前继节点不是head, 说明在当前节点之前还有线程被阻塞等待获取共享锁,所以当前线程节点就应该老老实实地等待,等待它的前继节点获取成功共享锁或者释放了共享锁之后,再唤醒它去尝试获取共享锁吧。

  在独占锁模式中,因为锁只能被一个线程持有,所以当同步队列中的一个线程获取了独占锁之后,只需要将它自身设置为头节点,让原来的头节点“出队”就可以了。但是,在共享锁模式下,因为共享锁可以被多个线程同时持有,当前线程获取共享锁成功,并将自身设置为头节点之后,还需判断同步队列中是否有满足唤醒条件的后继节点,如果有则继续唤醒后继节点去竞争共享锁,这个是通过 setHeadAndPropagate()来实现的。

  3、setHeadAndPropagate()方法分析:

private void setHeadAndPropagate(Node node, int propagate) {
        //后继节点成功获取了共享锁,队列的"旧head"还没有改变,将其保存下来,锁定到方法的局部变量做后序的判断使用;
        Node h = head; // Record old head for check below
        /**
         * 将这个获取共享锁成功的后继节点设置为同步队列的“新head”,此时同步队列的head发生变化, 此线程还未唤起任何线程。
         */
        setHead(node);
        /**
         * 1、h == null这个条件什么时候成立呢?仔细翻了下AQS中的源码发现:
         * 这个setHeadAndPropagate()方法只在共享锁模式下,同步队列head的后继节点成功获取了共享锁才会调用。
         * 获取到共享锁的当前线程是同步队列的头结点的后继节点,"旧head"有后继节点,说明同步队列不为空,那么"旧head"也必定不为空,
         * 此方法中第一行通过h == head,在执行setHead(node)方法之前将"旧head"保存了下来,所以h == null必定不会成立,
         * 至于为什么这么写呢? 查阅了下资料网上说"发现这个是防止空指针异常发生的标准写法(既如果要取一个对象的某个属性进行判断的时候,首先对这个对象进行null判断)。"
         * 这说的过去吧?
         * 
         * 2、(h = head) == null这个条件什么时候成立呢?
         * 这个条件也是不可能成立的,下面这种情况应该是最常见的:
         *  (1)、例如有个Semaphore实例s初始化了2个许可,线程A首先调用s.acquire(2)申请了两个许可,成功申请到了许可;
         *  (2)、线程B调用了s.acquire()方法申请一个许可,申请失败,加入到同步队列;
         *  (3)、线程C调用了s.acquire()方法申请一个许可,申请失败,加入到同步队列;
         *  (4)、线程A调用了s.releaseShared(2)方法释放了两个许可,再调用doReleaseShared()方法,进行同步队列唤醒;
         *  (6)、首先唤醒了同步队列中的线程B,B线程获取到共享锁:
         *      a)、如果此时线程B还未setHead(Node)方法,还未改变同步队列的head头结点,那么线程A的唤醒工作就结束,也仅仅只是唤醒了同步队列中的线程B,
         *              则必定有(h = head) == Node(C) != null成立,线程C的唤醒工作仍然需要线程B去执行;
         *      b)、如果此时线程B执行了SetHead(Node)方法,改变了同步队列的head头结点,那么线程A同时也会唤醒线程C,相当于线程A同时唤醒了线程B和线程C:
         *         1)、如果线程C中的setHeadAndPropagate()在线程B前调用完毕(即线程C执行了setHead()方法改变了同步队列的head),那么 (h = head) == Node(C);
         *         2)、如果线程C中的setHeadAndPropagate()在线程B之后才调用(即线程C此时还未执行setHead()方法,未改变同步队列的head),那么 (h = head) == Node(B)
         *  所以综上所述,只要执行过addWaiter()方法,向同步队列中添加过线程,那么(h = head)== null必定不成立。只能理解为“防止空指针的标准写法”。 
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            /**
             * s == null这种情况是可能存在的,如果当前唤醒的这个node节点是同步队列的尾节点就可能出现node.next == null;
             * s.isShared()指定是共享锁模式,当前线程获取共享锁之后,是需要尝试唤醒同步队列中的其它线程的。
             */
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

  上面提到的这个方法其实就做了两件事情:1、将当前获取共享锁的线程设置为同步队列的头节点;2、根据同步队列头节点head的状态,来决定是否需要唤醒后续节点,符合条件就调用doReleaseShared()方法执行唤醒后续节点的操作。存在的各种情况,也在上面的代码中上面代码中分析过了,下面我们接着往下走来分析doReleaseShared()方法吧! 

  4、doReleaseShared()方法:

 1  private void doReleaseShared() {
 2         for (;;) {
 3             Node h = head;
 4             /**
 5              * h != null保证了队列不为空,h != tail保证了队列中有需要唤醒的节点,
 6              * 如果这不能同时满足说明队列中没有需要唤醒的节点,此时h == head这个条件是成立的,
 7              * 直接跳转到h == head判断中break,此方法结束执行。
 8              */
 9             if(h != null && h != tail) {
10                 int ws = h.waitStatus;
11                 //如果头节点的状态是Node.SIGNAL说明后续有节点是需要唤醒的,
12                 if (ws == Node.SIGNAL) {
13                     /**
14                      * 考虑到共享锁可以被多线程并发持有,可以采用CAS操作,将设置头节点的状态为的0的compareAndSetWaitStatus(h,Node.SIGNAL,0)的操作
15                      * 和unparkSuccessor(h)唤醒后节点的操作绑定在一起,这个CAS操作成功,说明头节点之前肯定是Node.SIGNAL状态,那么后继结点肯定能被唤醒。
16                      */
17                     if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
18                         continue;            // loop to recheck cases
19                     unparkSuccessor(h);
20                 }
21                 /**
22                  * ws == 0说明头节点的后继节点已经被唤醒或者即将被唤醒。
23                  */
24                 else if (ws == 0 &&
25                          !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
26                     continue;                // loop on failed CAS
27             }
28             //同步队列的头节点未发生变化,跳出唤醒的动作
29             if (h == head)                   // loop if head changed
30                 break;
31         }
32     }

   这个方法可能是共享锁中最难理解的一个方法了,粗略地读完上面的注释,你是否会有以下几个问题呢?

  Q1、什么时候会调用doReleaseShared()方法呢?

  翻了下AQS源码我们发现有两个地方调用:

  1、在获取共享锁acquireShared()方法中,满足一定的条件下(共享锁还可以被同步队列中的其他线程获取情况下),可以调用;

  2、在释放共享锁releaseShared()方法中,释放成功一定会被调用;

  Q2、谁会调用调用doReleaseShared()方法呢?

  在通过上一篇文章我们了解到,独占锁中,只有获取了锁的线程才能调用release释放锁,因此调用unparkSuccessor(h)唤醒后继节点的必然是持有锁的线程,该线程可看做是当前的头节点(虽然在setHead方法中已经将头节点的thread属性设为了null,但是这个头节点曾经代表的就是这个线程);

  而共享锁中,持有锁的线程可以有多个,这些线程都可以调用releaseShared()方法释放锁;假如这些线程都是从同步队列出队获取共享锁的,那么他们必然曾经成为过head或者现在就是head,如果是reReaseShared()中的方法调用doReleaseShared()方法,那么可能现在调用此方法的线程,已经不是同步队列头节点所代表的线程了,头节点可能被易主好多次了。

  Q3、调用该doReleaseShared()方法的目的是什么呢,何时结束这个唤醒操作呢?

  无论是在acquireShared()调用,还是在releaseShared()方法中调用,其目的就是在共享锁是可用的状态,唤醒头节点的后继有效节点,竞争共享锁。但是共享锁和独占锁的一个重要区别是:共享锁在头节点发生变化时(说明后继节点已经成功获取了共享锁,并执行了setHead()方法,将其设置为head),会再执行一次自旋唤醒新的头节点的后继节点,去竞争共享锁。

  上面的话是什么意思呢?换句话说:就是当前线程完成后继节点唤醒任务,需要退出的时候,检查了一下头节点,唤醒的这个节点已经是新的头节点了(这个唤醒的节点也成功获取到了共享锁),那么它的后继节点是有资格竞争共享锁的,所以需要继续唤醒它的后续节点,周而复始,直到h == head不再执行后续节点唤醒。

  Q4、什么时候才会发生满足h == head这个条件呢?

  经过分析大概有以下这么几种情况(欢迎大家补充,有不对的地方还请大家指出):

  1、阻塞队列为空,即阻塞队列中没有需要唤醒的节点,满足h == head这个条件。

  2、线程A唤醒了后继线程B,但是线程B并没有获取到共享资源(线程B当然也就不会执行setHead()方法改变同步队列的head了),又发生了线程阻塞,不需要再唤醒后续的线程了,也满足h == head这个条件,那么B的后续线程的唤醒工作应该交给线程B获取资源时候在负责去唤醒吧。

  3、线程A唤醒了线程B,线程B成功获取了资源,还是还未执行到setHead()这个方法;当前线程A,此时判断h == head也成立了,其调用的doReleaseShared()方法结束了,那么将唤醒线程B后续节点的工作,就应该交给刚刚被唤醒的线程B去执行了。

  Q5、ws == 0这个状态怎么理解?什么情况下才会出现ws == 0这个状态呢?

  其实我们仔细分析之前的代码我们可以得出以下结论:head的后继节点已经被唤醒或者即将被唤醒,分以下几种情况:

  1、有线程A刚释放了锁,刚执行了unparkSuccessor里的if (ws < 0) compareAndSetWaitStatus(node, ws, 0);把head的状态设置为了0,然后尝试唤醒head后继线程B,这里也分3种情况:

              (1)、执行了if (ws < 0) compareAndSetWaitStatus(node, ws, 0),还没有执行LockSupport(this)方法(后继节点中的线程B即将被唤醒);

    (2)、线程B被唤醒,获取锁成功,直到后继线程B将自己设置为AQS的新head的这段时间里,head的状态为0(后继节点中的线程B已经被唤醒);

    (3)、线程B被唤醒,获取锁失败,直到将head重置为Node.SIGNAL这段时间里,这个head的状态也是为0的(后继节点中的线程B已经被唤醒);

  2、同步队列中只有一个head == tail 的dummy node节点,它的状态为0;

  3、在第2中情况上更进一步,同步队列中只有一个head == tail 的dummy node节点,它的状态为0,此时有个线程A获取共享锁失败了,但是只进行了入队操作,还未执行shouldParkAfterFailedAcquire()方法,未将head节点设置未Node.SIGNAL状态,这段时间head的状态也0(后继节点中的线程即将被唤醒);

   综上所述,我们不难看出,其实head.waitStatus == 0 这个状态是一个中间状态,可能会很快改变。后继节点获取共享锁失败了,head节点不会发生变化,只不过很快会将head.waitStatus 设置成Node.SIGNAL;后继结点获取共享锁成功,后继节点会被设置为新的head。假如后继节点不是尾节点,那么必定新的head.waitSatus == Node.SIGNAL,如果后继节点是尾节点,那么必定head.waitStatus == 0,因为没有后续入队节点将它的状态置为Node.SIGNAL。

  Q6、什么时候会出现 ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)为true的情形呢?

  读过上面的Q5中的分析,最可能的发生的情况是:

  同步队列中head头节点是刚刚成为头节点的,它的waitStatus值还为0,尾节点是在这之后刚刚加进来的。这种怎么理解呢?同步队列中的“旧的尾节点”状态是0,用线程A表示它,此时线程A刚刚获取到共享锁,将自己设置为头结点head节点,此时有个线程B获取共享锁失败,将自己加入到同步队列中,此时线程B还未执行shouldParkAfterFailedAcquire()方法,改变同步队列头结点head的状态;此时线程A执行了doReleaseShared()中的方法,发现ws == 0,但是恰好就在此时,线程B执行了shouldParkAfterFailedAcquire()方法,设置head的waitStatus==Node.SIGNAL,紧接着线程A执行执行到compareAndSetWaitStatus(h,0,Node.PROPAGATE)失败了,继续continue进入下次“自旋”。

  由此可见,doReleaseShared()方法中else if 这个分支的 && 连接了两个不一致的状态,分别对应了shouldParkAfterFailedAcquirecompareAndSetWaitStatus(pred, ws, Node.SIGNAL)执行成功前和执行成功后,因为doReleaseSharedshouldParkAfterFailedAcquire是可以并发执行的,所以这一条件是有可能满足的,只是满足的条件非常严苛,可能只是一瞬间的事。

  结合进入此方法的大条件:Node h = head ; h != null && h != tail;这个两个条件(即同步队列不为空,有需要唤醒的线程节点),所以Q5中的第2中条件在此时不成立的,只剩下1、3两种情况了。

  至于共享锁的释放逻辑,相信看完上面的分析,再去看也不是什么难事,这里就不再赘述了。

  总结:

  • 共享锁的调用框架和独占锁和实现原理非常相似,两者最大不同在于获取锁的逻辑——共享锁可以被多个线程同时持有,而独占锁同一时刻只能被一个线程持有。
  • 由于共享锁同一时刻可以被多个线程持有,因此当头节点获取到共享锁时,可以立即唤醒后继节点来争锁,而不必等到释放锁的时候。因此,共享锁触发唤醒后继节点的行为可能有两处,一处在当前节点成功获得共享锁后,一处在当前节点释放共享锁后。

   以上就是共享锁的获取流程,进行了一个简单的记录和总结,如有说的不对的地方,还请大家批评指正,共同交流,共同进步。

  参考文章地址:

  1、https://www.cnblogs.com/waterystone/p/4920797.html

  2、https://blog.csdn.net/anlian523/article/details/106319294

  3、https://segmentfault.com/a/1190000016447307

  4、https://www.cnblogs.com/micrari/p/6937995.html

有关(原创)多线程并发:AQS源码分析(2)——共享锁的实现原理的更多相关文章

  1. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  4. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  5. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

  6. ruby - 在模块/类之间共享全局记录器 - 2

    在许多ruby​​类之间共享记录器实例的最佳(正确)方法是什么?现在我只是将记录器创建为全局$logger=Logger.new变量,但我觉得有更好的方法可以在不使用全局变量的情况下执行此操作。如果我有以下内容:moduleFooclassAclassBclassC...classZend在所有类之间共享记录器实例的最佳方式是什么?我是以某种方式在Foo模块中声明/创建记录器还是只是使用全局$logger没问题? 最佳答案 在模块中添加常量:moduleFooLogger=Logger.newclassAclassBclassC..

  7. ruby - Ruby 1.9.1 中的 native 线程,对我有什么好处? - 2

    所以,Ruby1.9.1现在是declaredstable.Rails应该与它一起工作,并且正在慢慢地将gem移植到它。它具有native线程和全局解释器锁(GIL)。自从GIL到位后,原生线程是否比1.9.1中的绿色线程有任何优势? 最佳答案 1.9中的线程是原生的,但它们被“放慢了速度”,一次只允许一个线程运行。这是因为如果线程真的并行运行,它会混淆现有代码。优点:IO现在在线程中是异步的。如果一个线程阻塞在IO上,那么另一个线程将继续执行直到IO完成。C扩展可以使用真正的线程。缺点:任何非线程安全的C扩展都可能存在使用Thre

  8. ruby - 如何使用 cucumber 在场景之间共享状态 - 2

    我有一个功能“从外部网站导入文章”。在我的第一个场景中,我测试从外部网站导入链接列表。Feature:ImportingarticlesfromexternalwebsiteScenario:Searchingarticlesonexample.comandreturnthelinksGiventhereisanImporterAnditsURLis"http://example.com"Whenwesearchfor"demo"ThentheImportershouldreturn25linksAndoneofthelinksshouldbe"http://example.com/d

  9. ruby - 使写入文件线程安全 - 2

    我在一个ruby​​文件中有一个函数可以像这样写入一个文件File.open("myfile",'a'){|f|f.puts("#{sometext}")}这个函数在不同的线程中被调用,使得像上面这样的文件写入不是线程安全的。有谁知道如何以最简单的方式使这个文件写入线程安全?更多信息:如果重要的话,我正在使用rspec框架。 最佳答案 您可以通过File#flock给锁File.open("myfile",'a'){|f|f.flock(File::LOCK_EX)f.puts("#{sometext}")}

  10. 建模分析 | 平面2R机器人(二连杆)运动学与动力学建模(附Matlab仿真) - 2

    目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标

随机推荐