1.问题描述

1.1 相关背景

        这是我们团队实现的跨多个屏幕设备播放的功能,其中有个播放视频同步的技术难点需要使用到ffmpeg库类对用户上传的视频解析到IPB三种图像帧的准确时间点,以便于在多个屏幕时发生播放不一致时进行播放时间的校准。

                                

javafx屏幕共享 java 共享屏幕 延迟_队列

我们使用的是cmd调用ffmpeg库类,具体使用的是ffprob命令实现,通过进程间通信将输出的数据流写入磁盘文件中,然后上传到存储云中,具体流程如左图所示,生成的数据流文件流如右图所示。

javafx屏幕共享 java 共享屏幕 延迟_队列_02

      

javafx屏幕共享 java 共享屏幕 延迟_java_03

1.2 问题出现

     具体调用逻辑如下图所示,整个过程都是以线程池的异步任务执行的形式去做, 当时图视频图像任务的处理线程池参数为coreSize = 30, maxSize = 40**,** queueCapacity = 50。  但在处理正常视频时会偶发任务处理失败的情况,这个功能的使用用户还不是很多,当时想是比较好定位问题的。查看日志可看到启动的ffmpeg子进程中偶尔发生任务线程因为等待子进程的退出而卡死的情况。但在本地用循环来调用时复现了这个问题,查看了当时堆栈信息如下所示,可以看到线程一直处于TIMED_WAITING状态。基于这种情况,添加了些日志,发现最后是卡在了process.wait()方法上。

javafx屏幕共享 java 共享屏幕 延迟_多线程_04

"ffmpeg process" daemon prio=10 tid=0x00006f52904dc000 nid=0x14bf waiting on condition [0x00007f427aa2f000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to waeit for  <0x00000009c229ebf1> (a java.util.concurrent.SynchronousQueue$TransferStack)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
    at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:460)
    at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:359)
    at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:942)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1043)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1103)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:722) 
复制代码

2.问题分析与解决

        当时搜了下"java执行cmd阻塞"这样的问题,找到了一些答案。Process.waitFor() 会在当前线程阻塞等待,如有必要会一直要等到由该 Process 对象表示的进程已经终止,在调用此方法时稍不注意,很容易出现主线程阻塞的情况,Process也挂起的情况。在调用waitFor() 的时候,Process需要向主线程汇报运行状况,所以要注意清空缓冲区,即InputStream和ErrorStream。 我们在本地的进行了试验,这次在完全不读取子进程的输出数据流。果然这次随机的概率出现主进程的概率就大很多。

for (int i = 0; i < 200; i++) {        
     //执行ffmpeg,会输出很多数据流
     Process process = Runtime.getRuntime().exec("..."); 
     Thread.sleep(5000);         
     int status = process.waitFor();            
     System.out.println(status);
     System.out.println(i);
}

 for (int i = 0; i < 200; i++) {        
     //执行ffmpeg,会输出很多数据流
     Process process = Runtime.getRuntime().exec("..."); 
     //启动单独线程读取outputStream
     new Thread(()->{读取outputStream;}).start();
     //启动单独线程读取errorStream
     new Thread(()->{读取errorStream;}).start();
     Thread.sleep(5000);         
     int status = process.waitFor();            
     System.out.println(status);
     System.out.println(i);
}
复制代码

目前我们可以知道就是尽量不要让子进程挂起,让输出到缓冲区的数据流及时读取出来,这样的话,就不能让任务进入线程池的任务队列中,这样大概率会导致子进程挂起,我们直接另外开启了一个专门用来处理数据流的线程池,干掉任务等待队列,尽量将处理线程数调整为视频处理并发请求的两倍coreSize = 60, maxSize = 80, queueCapacity = 0,****让线程池尽快处理进程输出到缓冲区的数据。再次启动测试几遍,没有发生过这个问题了。

javafx屏幕共享 java 共享屏幕 延迟_队列_05

3.问题原理

3.1 进程相关知识

        一个进程下有多个线程,可以共享相同的用户地址空间,平时我们用的比较多的是线程间使用常见的如互斥锁条件变量、读写锁****、信号量等通信机制,由于每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,然后进程2把内核缓冲区的数据拷到用户空间,其中有管道/套接字/共享内存/消息队列/信号量等方式通信。

javafx屏幕共享 java 共享屏幕 延迟_javafx屏幕共享_06

首先管道是内核的一个缓冲区,而且是在内存中。管道一头连接着一个进程的输出,另一头连接着另一个进程的输入。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

javafx屏幕共享 java 共享屏幕 延迟_linux_07

可以看到在java的执行进程与进程通信是通过Process/UNIXProcess/ProcessImpl/ProcessBuilder以及相关类实现的。

javafx屏幕共享 java 共享屏幕 延迟_java_08

在新建进程实例时,会通过ProcessImpl类的Start()方法去初始化输出的流,然后再新建UNIXProcess实例对象。

javafx屏幕共享 java 共享屏幕 延迟_linux_09

可以看到是UNIXProcess类中调用了类的方法forkAndExec(),这个方法是初始化通信管道后,在调用了native forkAndExec()方法。

javafx屏幕共享 java 共享屏幕 延迟_多线程_10

这里启动了一个线程池去做waitForProcessExit()的操作,这个方法是阻塞等待的,会一直等待进程退出才结束。

javafx屏幕共享 java 共享屏幕 延迟_多线程_11

3.2 管道的相关知识

      这是从linux内核源码中copy下来的,管道读写是通过操作同一个缓冲区实现的,在写入之前做一个同步加锁,然后根据可写入缓冲区的位数再进行对应的操作。

javafx屏幕共享 java 共享屏幕 延迟_javafx屏幕共享_12

如果缓冲区满了则进入等待队列中。如果在管道读取端一直不去读取缓冲区内容,则缓冲区一直都是满的,则写入端自然就进入等待队列中。

javafx屏幕共享 java 共享屏幕 延迟_队列_13

4.总结

       通过对java进程执行和进程间通信去了解了Linux 和 Java 中的进程的相关技术原理,并通过对管道读写的特性解决对进程执行异常问题。

作者:Aston_Martin