Go为什么不支持可重入锁呢?

先来看看什么是可重入锁,以下是维基百科的定义。

计算机科学中,可重入互斥锁(英语:reentrant mutex)是互斥锁的一种,同一线程对其多次加锁不会产生死锁。可重入互斥锁也称递归互斥锁(英语:recursive mutex)或递归锁(英语:recursive lock)。

如果对已经上锁的普通互斥锁进行“加锁”操作,其结果要么失败,要么会阻塞至解锁。而如果换作可重入互斥锁,当且仅当尝试加锁的线程就是持有该锁的线程时,类似的加锁操作就会成功。可重入互斥锁一般都会记录被加锁的次数,只有执行相同次数的解锁操作才会真正解锁。

递归互斥锁解决了普通互斥锁不可重入的问题:如果函数先持有锁,然后执行回调,但回调的内容是调用它自己,就会产生死锁[1]

Go里面是不支持可重入互斥锁的。看代码:

var mu sync.Mutex
func main() {
	mu.Lock()
	mu.Lock()
}

运行后会报

fatal error: all goroutines are asleep - deadlock!

Go 设计原则

在工程中使用互斥的根本原因是:为了保护不变量,也可以用于保护内、外部的不变量。

基于此,Go 在互斥锁设计上会遵守这几个原则。如下:

  • 在调用 mutex.Lock 方法时,要保证这些变量的不变性保持,不会在后续的过程中被破坏。
  • 在调用 mu.Unlock 方法时,要保证:
    • 程序不再需要依赖那些不变量。
    • 如果程序在互斥锁加锁期间破坏了它们,则需要确保已经恢复了它们。

不支持的原因

讲了 Go 自己的设计原则后,那为什么不支持可重入呢?

其实 Russ Cox 于 2010 年在《Experimenting with GO》就给出了答复,认为递归(又称:重入)互斥是个坏主意,这个设计并不好。

总结

Go 互斥锁没有支持可重入锁的设计,这样就不用关心递归层级等复杂的流程了。加锁解锁中间做了什么一目了然。

关于Go切片截取的一个注意事项

go在使用切片截断时,底层数组没有改变,依然持有指向堆内存的指针,导致内存无法释放。

func split(arr []int) []int {
	newArr := arr[0:5]
	return newArr
}

以上切片newArr截取了切片arr中的部分数据并返回。如果切片arr在之后不再使用,即使切片newArr只使用了切片arr中的前5个数据而已,arr的整个底层数组都不会被GC回收。如果切片arr中含有大量的数据,一直不释放,那这会造成较大的浪费内存。

类似这种可以在截断前不需要的手动置空。

arr[5:] = nil
newArr := arr[0:5]

另外对于项目中Go代码情况可以pprof程序性能分析工具去查看。

关于PHP-FPM的reload

由于最近在看接口监控的时候发现有一些502的情况,所以打算排查下原因。看了下nginx及php-fpm日志后偶然发现报502的时间节点跟fpm重启正好吻合。确认了下这个重启只是reload的操作而非restart的操作。什么?reload不是平滑启动吗?怎么会导致这样的问题出现呢?上网查了下资料后发现也有很多人遇到过这种情况。

接下来重现下这种情况。写几行代码来实际模拟下:

sleep(10);
echo "test";

然后在浏览器中访问一下,同时执行fpm reload的操作。可以看到执行完reload后页面立马就502了。
实际上通过process_control_timeout参数可以实现我们的目标。可惜这个参数缺省是 0,也就是不生效,这里把它设置成为2s。然后再重复一次上面的操作后发现页面可以正常输出test。同时会发现页面并不会等待10s后打印test。当我们 reload 的时候,sleep 立刻就结束了,这是因为 sleep 收到 reload 发出的信号后直接返回了。

process_control_timeout参数参考官网文档的解释:设置子进程接受主进程复用信号的超时时间。可用单位:s(秒),m(分),h(小时)或者 d(天)。默认单位:s(秒)。默认值:0(关闭)。
即reload的时候,如果有正在执行的请求进程便会等待该进程设置的时长。而其他进程直接就结束掉。等待正在执行的进程执行完或者是超过了设置的时间后fpm的master进程才开始生成新的fpm worker进程。

结论
默认情况下,PHP-FPM 无法保证平滑的执行 reload 操作,必须设置一个合理的 process_control_timeout 才行,同时需要注意的是其值不能设置的过大,否则系统可能出现严重的请求堵塞问题。

PHP中的null字符问题

由于 PHP 的文件系统操作是基于 C 语言的函数的,所以它可能会以您意想不到的方式处理 Null 字符。 Null字符在 C 语言中用于标识字符串结束,一个完整的字符串是从其开头到遇见 Null 字符为止。
借用官方手册中的一个例子:

$file = $_GET['file']; // "../../etc/passwd\0"
if (file_exists('/home/wwwrun/'.$file.'.php')) {
    // file_exists will return true as the file /home/wwwrun/../../etc/passwd exists
    include '/home/wwwrun/'.$file.'.php';
    // the file /etc/passwd will be included
}

以上代码在PHP5.3以前版本中,文件/etc/passwd将会被加载。对以后的版本没有影响。
虽然这个遇到\0会把字符截断的问题已经被修复了,但是在PHP中文件系统相关函数中还是有一点影响的,比如file_exists(),is_file()等。
如下代码:

$filename = "/etc/passwd\0"; 
$res = file_exists($filename); 
var_dump($res);

以上代码的执行结果如下:
Warning: file_exists() expects parameter 1 to be a valid path, string given in /usr/local/var/www/a.php on line 3
/usr/local/var/www/a.php:4:null

解决办法

对null字符进行替换

$input = str_replace(chr(0), '', $input);

重新认识PHP中的set_time_limit

想必大家都已经知道set_time_limit函数的作用,即设置脚本最大执行时间。但是这个函数还是有一些细节有些人可能没太注意。

首先先看一下set_time_limit函数的解释。设置允许脚本运行的时间,单位为秒。如果超过了此设置,脚本返回一个致命的错误。默认值为30秒,或者是在php.ini的max_execution_time被定义的值,如果此值存在。当此函数被调用时,set_time_limit()会从零开始重新启动超时计数器。换句话说,如果超时默认是30秒,在脚本运行了了25秒时调用 set_time_limit(20),那么,脚本在超时之前可运行总时间为45秒。

还有一个注意的地方是set_time_limit()函数和配置指令max_execution_time只影响脚本本身执行的时间。任何发生在诸如使用system()的系统调用,流操作,数据库操作等的脚本执行的最大时间不包括其中。同时sleep函数的时间也不包括在其中。

也就是说set_time_limit()函数只针对的是PHP代码本身的执行时间。

例子1:

set_time_limit(5);
sleep(10);
echo "test";

以上代码在10秒后正常的输出test,并且不会报错。
换成下面的代码试试

set_time_limit(5);
for(;;){
   //do something  
}

以上代码执行5秒后会报Fatal error: Maximum execution time of 5 seconds exceeded的错误。

例子2:

set_time_limit(5);
//假设curl需要10秒才能返回数据
$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, "http://example.com"); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
$output = curl_exec($ch);  
curl_close($ch);  
var_dump($output);

以上代码在10秒后正常的打印出curl请求返回的结果,并不会在5秒的时候报错。
同理,数据库的相关操作也是如此。

所以,看似一个简单的函数调用,有些细节上的东西还是很有必要了解的。不然坑的可就是自己了?。