分类目录归档:程序设计

Windows下控制子进程的句柄继承

相信用 Windows API 写过重定向子进程的标准输入输出的人都弄过这东西。大概过程是不难,不过对初学者坑很多。几年前我写了个文大概说这个的 http://blog.sorayuki.net/?p=85 ,现在要说的也是这个话题,不过是另外的事。

之前的经验是这样的,比如我有一个进程,这个进程要开个子进程、并且发数据给子进程。那么做法是创建一对匿名管道,把管道读的那一端拿去给子进程,写的这一端自己留着。容易出错的地方在这个“自己拿着”和“拿去给子进程”。拿去给子进程可以理解为,这个句柄要设置成可继承的,子进程开启以后就带着这个句柄了,那么自己进程拿着的就可以关掉(CloseHandle)了;自己拿着,那么字面意思就是不继承给子进程了。API函数CreatePipe有个 SECURITY_ATTRIBUTES 参数,这个结构体里是可以设置创建的管道句柄能不能继承的,要么两个都能继承要么两个都不能继承。但是现在要的是写的那个不继承、读的那个继承。做法不止一种,不过思路是这么两个,一个是创建的时候两个都不能继承,然后把其中一个改成可以;另一个是创建的时候两个都能继承,然后把其中一个改成不行。改继承属性可以用 SetHandleInformation 也可以用 DuplicateHandle 复制一个能/不能继承的新句柄出来、随后把原来的关掉。我现在是用 SetHandleInformation 的方法,DuplicateHandle 的方法已经忘记是在哪里看的了,现在也不知道怎么比较好坏和必要性。

复杂一点的场景,是我要起两个进程,第一个进程的输出给第二个进程的输入。麻烦一点的办法可以两个都和自己通信、自己在中间做转发;简单的办法可以直接创建一对管道,一个子进程分一个、自己不留。现在这里要说的是后者,复杂性一个表现在管道是自己创建的,但是自己在起了子进程之后一个都不留;另一个表现在要刚好一个进程继承一个,如果有一个进程继承了两个,比如写的那一端一不小心两个进程都继承了,那么很不幸这一对进程永远跑不完了:写入数据的那个子进程写完了关掉了这个写入端,但是由于读取数据的那个子进程也持有这个写端,自己有自己都不知道,也就是说这个写端没法全部被关闭,那么读的那一端就永远在等待了。

虽然上面那个例子只要搞清楚关系、小心操作的话,也没有那么大问题。不过麻烦的事情在,假设我现在是一个有好几个线程在同时执行的程序,在其中一个线程A里,要调用 CreateProcess 的时候,它不知道其他线程比如B有没有创建什么会被继承的句柄,假设这个时候线程B也在调用 CreateProcess,那么线程B创建的这个子进程会把线程A创建的那些句柄也一起继承走。然后问题呢就会出在刚才说的那个场景上,比如一对管道,读取端永远阻塞因为写入端没有全部被关闭。也许会觉得这样的场景只要在要创建子进程的时候锁个整个进程范围的锁就好了,但如果这里面调用了什么第三方库,或者是工程上好多人同时开发一个项目,有些事情就不一定有那么可控了。而且这种Bug还可能在极偶然的情况下才会遇到那么一两次,调试的时候难以重现十分痛苦……啊别说了,害怕。

继续阅读Windows下控制子进程的句柄继承

条件变量的伪唤醒

起因是这样的,C++的新标准库添加了多线程的支持,有线程和线程同步的实现。我自己是做Windows开发比较多,自然用过Event这样的线程同步对象。但是这个东西,在C++的标准库里面没有。我想到说它有一个“条件变量”,而这个“条件变量”是有一个wait / signal 功能的,这个类看起来能实现 Event 的一部分功能。但是在前几天,有个同事说条件变量的 wait 会在没有被 signal 的时候也自己唤醒。毕竟平时不做 posix 那样系统下面的开发,所以这样的事情没有听说过。那会儿其实我是不信的,但是用关键字“condition variable fake wake”在网上搜了一下以后,看到确实是有这样的事情,然后英语中也不是叫 fake,管它叫 spurious wakeup。

继续阅读条件变量的伪唤醒

视频直播开发的技能树

现在直播什么的很火,还有拍短视频之类。很多人在开发这方面的软件。我在一个这方面开发的讨论群里看了一段时间以后,有这种感觉。就是很多人是对多媒体方面的知识不太够,但是公司又要做,只能硬着头皮上。找资料的时候就会遇到这样一个问题,一些基础知识不会,看文章就看得有点迷糊或者看不懂。找别人写好的函数库来调用的话,只能实现一些简单的需求。所以我在想,我以前在字幕组搞过一段时间视频处理,这方面的知识能不能组织一下列出来,并不是全都要会才能做,里面很多我也不熟,只是在遇到问题时候好有个方向,知道要去查什么。

括号里的英文是用来方便搜索的。因为中文互联网里的资料搜起来很多实在不堪入目,搜英语资料虽然看起来比较累,不过根据经验确实是能少踩点坑

有想到的不完整的地方可能还会来补充。

继续阅读视频直播开发的技能树

返回值优化没了 / FPU爆栈 / 线程池疯长 / 连接被重置

标题之所以那么复杂是因为我把本来应该分开写的东西弄在一起了……
这些事情都是在公司里工作(或者摸鱼?)的时候遇到的。有的记下来了有的没有。记下来的这几个,本来说要写到博客上的,结果丢在桌面的便签上这么久了,现在已经从那家公司离职,又入职新的公司做了一年半,这便签上记录的东西一个都还没写。都是两年前的事了吧

继续阅读返回值优化没了 / FPU爆栈 / 线程池疯长 / 连接被重置

返回值优化没了 / FPU爆栈 / 线程池疯长 / 连接被重置

标题之所以那么复杂是因为我把本来应该分开写的东西弄在一起了……
这些事情都是在公司里工作(或者摸鱼?)的时候遇到的。有的记下来了有的没有。记下来的这几个,本来说要写到博客上的,结果丢在桌面的便签上这么久了,现在已经从那家公司离职,又入职新的公司做了一年半,这便签上记录的东西一个都还没写。都是两年前的事了吧

继续阅读返回值优化没了 / FPU爆栈 / 线程池疯长 / 连接被重置

用OpenCL造GPU加速的AviSynth滤镜

这一季度有个叫《月曜日のたわわ》的动画片。原作是比村奇石的漫画,这个作者的漫画有一个特点,和其他用黑白来表现亮度的漫画不同,比村用的是蓝白色调表现。看到这个动画的时候,我在考虑,能不能用一种处理方式,把动画做成漫画那种色调的效果。

思路是很简单的,先拿比村奇石的漫画原作来分析,假设表示同一亮度的像素的 RGB 值是一样的,这样就能得到一个从 YUV 的 Y 通道到 RGB 的映射。结果大概是这样(Y 和 RGB 的范围都是 0 到 255):

R = Y > 85 ? ((Y - 85) / 255 * 340) : 0;
G = Y;
B = Y > 135 ? 255 : Y + 120;

然后要对每个像素都处理。也就是一个很大的循环,然后 1920x1080 个像素全部按照上面的算一次。只是解码然后转成 RGB 进行上面的处理再转回 YV12(不包含拿去编码),速度不到30帧每秒(没有使用 SIMD 指令集)。

因为处理过程每个像素互不影响,如果能以非常多的线程同时运行,那么可以大大加快速度。这种事情是GPU擅长的,所以就思考着用 OpenCL 做这样的运算,看看能有多块的速度。

OpenCL 原始接口实在是太麻烦了,看着就让人提不起干劲。于是往上搜了一圈,找了一个叫做 EasyCL 的库来。它对 OpenCL 原始接口进行封装,然后就能以简洁得多的方式来调用 OpenCL。EasyCL 在编译过程中需要 OpenCL 的头文件和库。到 Intel、Nvidia、AMD 网站看了一圈,感觉 SDK 都好大好麻烦。然而我在自己系统文件夹下看到了 OpenCL.DLL,它的导出函数也和头文件里能对应上。于是就到 GitHub 上直接找 KhronosGroup 发的 OpenCL 的头文件(这里:https://github.com/KhronosGroup/OpenCL-Headers),下下来以后编译能过了。LIB 文件就通过链接的时候报错信息中找不到的那些符号,自己造一个(用这个文章里提到的自制工具)。通过了链接以后,得到了 EasyCL.DLL 和 EasyCL.LIB。

然后利用以下简单的函数就能调用 OpenCL 进行运算了:

EasyCL::isOpenCLAvailable() 可以判断系统能不能调用 OpenCL
EasyCL::createForFirstGpu() 可以创建一个 CL 对象
EasyCL::buildKernelFromString() 可以从源代码字符串创建一个 CL 程序,需要指定入口函数名,Option 是干嘛用的不知道不过可以送一个空字符串进去
CLKernel::in() 可以送从内存拷到显存的参数进去
CLKernel::out() 可以送从显存拷到内存的参数进去,这两个送参数的函数,有些类型是不支持的。不过既然都是指针,可以转成它认的那种指针送进去(数组大小记得也要根据类型的变化而变化)
CLKernel::run_1d() 可以运行程序。两个参数分别是一共有多少任务,然后一次运行多少个任务。前者要是后者的倍数否则它要报错,一次最多能运行多少任务可以通过 EasyCL::getMaxWorkgroupSize() 来获取。

解释一下什么叫“一共多少任务、一次运行多少任务”。比如这个视频是 1920x1080 的,那么一共有 2073600 个像素点,每个像素处理一次的话就一共有 2073600 个任务。通过调用函数得知一次能运行 1024 个任务,那么就可以送 2073600 和 1024 作为参数到 run_1d 函数里去(因为 2073600÷1024 能整除、没有余数)。然后在 OpenCL 代码里用 get_global_id(0) 获取当前是第几个任务,根据任务的编号来决定要做什么任务。(在本文中,编号就是“第几个像素”)

这个库在出现错误的时候,会抛异常给你而不是返回错误码或者什么的。所以要做好异常捕捉,不然异常抛出来程序没捕捉、程序就崩了。

改成用 GPU 处理之后,处理速度从原来的每秒 30 帧不到变成了现在接近 90 帧。效果还是很明显的。

P.S. 从QQ群内聊天得知,微软有个 DirectCompute 能做类似的事情。还有个关键字是 C++ AMP,可以直接在编译时生成拿到 GPU 上跑的代码,需要 DirectX 11 支持。