线程安全那些事

引言

在上一篇文章,我们了解了什么是进程/线程,把多进程和多线程进行了对比,那通常所说的线程安全是什么呢?要想搞清楚这个概念,那么我们可以尝试逆向思维,如果线程不安全了,会是什么样的情形呢?我们以两个独立的线程为例,它们都继承了进程的共享变量,这些共享变量都有哪些呢?按照我的理解,这些共享变量可能是变量,数组,对象,甚至是数据库连接。一般情况下,线程会对这些共享变量有2个操作:读取和写入。下面是一个线程不安全的例子。

class NotThreadSafe extends \Thread {
    /**
     * @see https://www.programcreek.com/2014/02/how-to-make-a-method-thread-safe-in-java/
     */
    public function run() {
        return $this->getCount();
    }

    private static $counter = 0;
    public static function getCount() {
        return self::$counter++;
    }
}

当线程1在访问时,可能线程2尚未执行完成, 因此,返回到线程1 的值可能是未增加的值。

那么线程不安全带来的后果是什么呢?执行结果不可控,每次执行结果可能不一样,也就是无法重复执行同一部分含有有线程的代码,无法保证输入运算输出结果唯一。

什么是线程安全

现在,我们可以大致的理解了线程安全这个概念,所谓的线程安全,必定对同一个输入运算输出唯一的结果,并应当满足以下3个条件:
1. 多个线程同时访问时,其表现出正确的行为。
2. 无论操作系统如何调度这些线程, 无论这些线程的执行顺序如何交织(interleaving)。
3. 调用端代码无须额外的同步或其他协调动作。

PHP如何实现线程安全

那么PHP的线程安全是如何实现的呢?在这篇文章中,讲解了PHP是如何实现线程安全的。我觉得关键几点有:
1. PHP引入了TSRM,也就是线程安全资源管理器(Thread Safe Resource Manager),这个机制解决线程并发问题。
2. TSRM实现了多线程中函数作用域中的相关变量范围共享,如全局变量、静态全局变量、静态局部变量,除了局部变量。
3. TSRM采用了避免共享状态,包括但不限于,如线程本地存储Thread Local Storage等。这意味着所有线程都可以使用它,但它的值在每一个线程中是单独存储的。
4. 对于那么无法避免共享状态的情况,相关同步的方法采用互斥锁实现。

如何实现线程安全

PHP官方并没有线程相关扩展,PHP 核心开发组的krakjoe自行实现的pthreads的线程扩展库。有几点需要注意:
1. 该扩展只能运行在CLI模型下
2. 要求PHP 7.2+
3. 必须要编译时开启ZTS(Zend Thread Safty),在编译时添加参数 --enable-maintainer-zts可开启ZTS。

好了,我先尝试编译一个ZTS的环境。默认的官方docker镜像是NTS( Non-Thread Safe)的。

PHP 7.2.7 (cli) (built: Dec  3 2018 12:53:48) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.7, Copyright (c) 1999-2018, by Zend Technologies

安装好环境,检查开启ZTS的方式,直接采用“php -v“` 即可。

PHP 7.2.7 (cli) (built: Feb  2 2019 08:51:03) ( ZTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.7, Copyright (c) 1999-2018, by Zend Technologies

通过实现了一个pthreads的实例,放在我的GitHub上,供你参考。

参考链接

  1. Multi-Threading in PHP with pthreads
  2. Easy pthreads Pools
  3. Ubuntu 16.04: How to install ZTS enabled PHP7 and the pThreads module
  4. Parallel Programming with Pthreads in PHP – the Fundamentals
  5. True PHP7 Multi-Threading: How to Rebuild PHP and use pthreads
  6. 什么是线程安全
  7. Wikipedia Thread safety
  8. Thread Safety and Shared Resources
  9. What is this thing you call “thread safe”
  10. 当我们在谈论XX是否线程安全时,我们在谈论什么
  11. 第三节 PHP中的线程安全
  12. Threads and PHP
  13. PHP 进阶之路 – 揭开 PHP 线程安全的神秘面纱
  14. Thread-Safe Resource Manager
  15. 深入研究PHP及Zend Engine的线程安全模型
  16. PHP 高级编程之多线程
  17. How to make a method thread-safe in Java

进程那些事

什么是进程

在ARTS的微信群里,看到布置的命题作业,为了弄清楚一个概念,那所谓的进程到底是什么?

作为程序员都知道,进程是一段程序代码,好像也对,也不对。我们有很多程序,基本都是由C或者C++写的程序代码,但不可能他们都叫进程吧。有时候,我们把程序代码加载在内存中,在CPU里执行的一段程序代码叫进程。也不对。CPU的频率那么高,可能在1s内运行无数个程序,那到底怎么才能称之为进程?

所谓的进程,我们可以把这个词语拆分来看,也就是:

不断进行输入运算输出的程序代码。

在面向进程设计的操作系统里,进程是程序的基本执行实体,在面向线程设计的操作系统中,进程不是基本运行单位,而是线程的容器。

一个操作系统的进程,可以读写下列数据:
1. 可执行机器代码的镜像
2. 分配到的存储器,包含可执行代码、特定进程的输入输出、调用堆栈、运行时堆栈。
3. 分配给该进程的资源的操作系统描述符,如文件描述符、数据源和数据终端。
4. 安全权限,例如进程拥有者和允许的操作权限。
5. 处理器状态,例如寄存器内容、物理存储器定址。

进程在运行时,状态会变化。进程的状态有以下这些:
1. 新生,进程刚创建。
2. 运行,正在运行中
3. 等待,等待事件的发生,比如用户输入完成,可称之为阻塞 blocked
4. 就绪,排队中,等待CPU
5. 结束,完成运行。

什么是线程

所谓的线程是操作系统可以进行运算调度的最小单位。以进程为容器,是进程的实际运行单位。通常情况下,把用户线程称为线程。

进程 线程 备注
虚拟地址空间 共享当前进程
文件描述符 共享当前进程
信号处理 共享当前进程
调用栈
寄存器环境
线程本地存储

一个进程可以有很多线程,每条线程并行执行不同的任务。

线程的四种基本状态分别为:
1. 产生
2. 中断
3. 非中断
4. 结束

如何选择多进程/多线程

关于如何正确的选择,有些大牛的文章写得很清楚,用下面的表格进行了两者的对比:

对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优

参考链接

  1. wikipedia 进程
  2. wikipedia 线程
  3. 多线程还是多进程的选择及区别
  4. 进程与线程的一个简单解释
  5. Linux 下多线程和多进程程序的优缺点,各自适合什么样的业务场景

Tars实践

什么是Tars

Tars这个名字取自于电影”星际穿越”中的机器人,它是基于名字服务使用Tars协议的高性能RPC开发框架,配套一体化的运营管理平台,并通过伸缩调度,实现运维半托管服务。

Tars是腾讯从2008年到今天一直在使用的后台逻辑层的统一应用框架TAF(Total Application Framework),目前支持C++,Java,PHP,Nodejs语言。该框架为用户提供了涉及到开发、运维、以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。 它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。

目前支持的开发语言如下:

  • C++
  • Java
  • Nodejs
  • PHP
  • Go

协议支持的类型如下:

特点 类型
基本类型 void
bool
byte
short
int
long
float
double
string
unsigned byte
unsigned short
unsigned int
复杂类型 enum
const
struct
vector
map
struct、vector、map的嵌套

其中复杂类型可以嵌套使用,例如:

# map 
map<int,string>
# struct、vector、map的嵌套
vector<string>
vector<map<string,string>>
map<vector<string>,vector<string>>

为什么要用Tars

  1. 性能较好,官方测试 在8 CPU+16GB+50进程+16线程下,TPS可以达41w/s。性能数据
  2. 这套框架比较重,提供涉及到开发、运维、测试等一整套的解决方案。
  3. Tars协议只是实现该方案的高性能RPC协议而已。
  4. 在开源界里,涉及服务治理,多语言,暂时只有Tars一家。

怎么用Tars

架构图

架构拓扑图

核心基础服务

服务名 类型 功能
tarsAdminRegistry 核心 服务注册,接入管理服务
tarsregistry 核心 服务发现,也叫主控,名字服务路由
tarsnode 核心 服务管理,节点管理服务
tarsconfig 核心 配置中心,配置服务
tarspatch 核心 自动发布,发布服务
tarsnotify 普通 服务上报,异常上报统计服务
tarsstat 普通 服务统计,模调数据统计服务
tarsproperty 普通 业务属性统计服务
tarslog 普通 服务日志,日志服务
tarsqueryproperty 普通
tarsquerystat 普通

部署方法

Tars的Docker镜像并未打包MySQL,因此需要自行独立启动MySQL服务,然后再启动Web管理服务。

相关镜像

镜像名称 备注
mysql:5.7 MySQL 5.7官方镜像
tarscloud/tars:php7 开发环境镜像,包含php7.2/swoole/phptars等扩展

拉取相关镜像

docker pull mysql:5.7
docker pull tarscloud/tars:php7

启动MySQL

docker run --name mysql -e MYSQL_ROOT_PASSWORD=password -d -p 3306:3306 -v /tars.data:/var/lib/mysql mysql:5.7 --sql_mode=NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION --innodb_use_native_aio=0

启动Web管理服务

docker run -d -it --name tars --link mysql --env MOUNT_DATA=true --env DBIP=mysql --env DBPort=3306 --env DBUser=root --env DBPassword=password -p 3000:3000 -v /tars.dev:/data tarscloud/tars:php7

进入运行环境,查看问题

docker exec -it tars bash

登录数据库,查看数据

mysql -hmysql -P3306 -uroot -p

访问Web管理后台,浏览器打开

http://0.0.0.0:3000/

遇到的问题

尽量优先查看相关的服务日志,然后阅读并搜索相关的源码,查找并定位问题。

Tars源代码阅读

cd dev
# 拉取Tars源代码
git clone git@github.com:TarsCloud/Tars.git
cd Tars
# 更新子模块代码
git submodule update --init --recursive

日志

日志服务 日志路径 备注
服务日志 /usr/local/app/tars/app_log/ /usr/local/app/tars/app_log/应用名/服务名/目录下
Tars管理错误日志 /data/logs/tars-node-web-error.log
Tars管理日志 /data/logs/tars-node-web-out.log
核心服务日志 /data/tars/app_log/tars
./tarsAdminRegistry/tars.tarsAdminRegistry.log tarsAdminRegistry
./tarsregistry/tars.tarsregistry.log tarsregistry
./tarsnode/tars.tarsnode.log tarsnode
./tarsconfig/tars.tarsconfig.log tarsconfig
./tarspatch/tars.tarspatch.log tarspatch
./tarsnotify/tars.tarsnotify.log tarsnotify
./tarsstat/tars.tarsstat.log tarsstat
./tarsproperty/tars.tarsproperty.log tarsproperty
./tarslog/tars.tarslog.log tarslog
./tarsqueryproperty/tars.tarsqueryproperty.log tarsqueryproperty
./tarsquerystat/tars.tarsquerystat.log tarsquerystat
PHPTest /data/tars/app_log/PHPTest/PHPServer PHPTest Server
./log_debug.log debug日志
./PHPTest.PHPServer.log 服务启动日志

服务路径

服务 路径 IP/Port
核心服务路径 /usr/local/app/tars 1000X开头
Web管理服务 /usr/local/tarsweb 0.0.0.0:3000

内部错误的问题

现象:点击服务管理或者运维管理都是出现内部错。

error: 172.17.0.1||TreeController.js:30|[listTree] SequelizeConnectionRefusedError: connect ECONNREFUSED 127.0.0.1:3306

# 编辑Web管理配置文件
vim /usr/local/tarsweb/config/webConf.js
# 修改数据库链接
host: '127.0.0.1' => host: 'mysql'

出现这个错误的原因是MySQL链接失败。如果在docker内部已经link上MySQL,直接修改地址即可。否则,重新初始化安装Web管理服务,在docker 加入正确的MySQL参数。

--env DBIP=mysql --env DBPort=3306 --env DBUser=root --env DBPassword=password

数据库问题

发现数据库tars_stat和tars_property为空。执行模板相关的SQL,发现会提示下面这个问题。

ERROR 1171 (42000): All parts of a PRIMARY KEY must be NOT NULL; if you need NULL in a key, use UNIQUE instead

继续 查找原因,发现部分PRIMARY KEY 不能为空,这个与MySQL5.7的SQL模式有关,开始不兼容这类SQL语句了。

PRIMARY KEY (`source_id`,`f_date`,`f_tflag`,`master_name`,`slave_name`,`interface_name`,`master_ip`,`slave_ip`,`slave_port`,`return_value`,`tars_version`)
# 举例
`source_id` varchar(15) default NULL
# 在MySQL5.7需要修改为下面的方式
`source_id` VARCHAR(15) NOT NULL DEFAULT ''

# 同理
`slave_port` int(10) default NULL
修改为
`slave_port` INT(10) NOT NULL DEFAULT 0

更新模板

  1. 找到运维管理
  2. 进入到模板管理
  3. 找到tars.tarsstat->编辑->修改内容为tars.tarsstat.md
  4. 保存
  5. 重启tarsstat服务,服务tarsproperty也一样处理。

这样数据库建表就成功了。

Connection refused

在相关日志中查看,发现下面的问题

2019-01-18 12:03:05|9262|ERROR|[TARS][CommunicatorEpoll::handleInputImp] connect error tcp -h 172.17.0.5 -p 10003,tars.tarslog.LogObj,_connExcCnt=6,Connection refused

2019-01-18 12:04:00|9262|ERROR|[TARS][CommunicatorEpoll::handleInputImp] connect error tcp -h 172.17.0.5 -p 10005,tars.tarsproperty.PropertyObj,_connExcCnt=1,Connection refused

解决方案是服务管理中重启相关的服务,如tarslog、tarsproperty等。

实战

根据官方文档TARS PHP TCP服务端与客户端开发,我已经把完成版本的代码,放在GitHub上,你可以直接下载到目录执行composer install即可。

PHP TCP服务端开发

由于版本库地址变更,导致一些代码的链接失效。可以直接替换成最新的地址。

将
https://github.com/TarsPHP/TarsPHP/blob
替换为
https://raw.githubusercontent.com/TarsPHP/TarsPHP

为了加速composer安装,将下面的repo加入composer.json

"repositories": [
    {
        "type": "composer",
        "url": "https://packagist.phpcomposer.com"
    },
    {
        "packagist": false
    }
]

代码发布

composer run-script deploy

PHP客户端开发

composer问题

在composer install时出现错误。

Your requirements could not be resolved to an installable set of packages.
Problem 1
    - The requested package phptars/tars-client 0.1.1 exists as phptars/tars-client[0.1.5, 0.1.6, 0.2.0, 0.2.1, 0.2.2, dev-master] but these are rejected by your constraint.

修改composer.json中的phptars/tars-client版本为0.1.5或者0.2.0。

"phptars/tars-client" : "0.2.0"

日志错误

PHP Notice:  Undefined index: iRequestId in /data/tarsnode_data/PHPTest.PHPServer/bin/src/vendor/phptars/tars-server/src/protocol/TARSProtocol.php on line 103

PHP Notice:  Undefined index: protocolName in /data/tarsnode_data/PHPTest.PHPServer/bin/src/vendor/phptars/tars-server/src/core/Server.php on line 68

# 对代码做下兼容处理
$iRequestId = isset[$unpackResult['iRequestId']]?$unpackResult['iRequestId']:0;

$this->protocolName = isset($this->tarsServerConfig['protocolName'])?$this->tarsServerConfig['protocolName']:'';

请求异常

如果你在请求的时候遇到下面的错误:

PHP Fatal error:  Uncaught Exception: Rout fail in /data/web/client/vendor/phptars/tars-client/src/Communicator.php:82
Stack trace:

首先尝试telnet下

yum install telnet
telnet 172.17.0.5 2019 #可以成功连接上,说明服务器程序已经运行了。
# 找到你的docker内网地址
ip addr

客户端需要针对服务请求做一些调整,服务发现采用指定服务地址,需要修改index.php。

# host 为docker的内网地址 port 为你的服务端口号
$host = "172.17.0.5";
$port = "2019";
$config = new \Tars\client\CommunicatorConfig();
$config->setLocator(sprintf('tars.tarsregistry.QueryObj@tcp -h %s -p %s',$host,$port));

修改为下面
$host = "172.17.0.5";
$port = "2019";
$routeInfo[] = [
'sIp' => $host,
'iPort' => $port,
];
$config = new \Tars\client\CommunicatorConfig();
$config->setRouteInfo($routeInfo);

执行测试

cd client
php index.php

你会发现了久违的Hello World,在相关的日志中也有了。

[2019-01-18 10:07:10] tars_logger.INFO: sayHelloWorld name:ted [] []

疑问

类似核心服务tarsAdminRegistry如何实现多接点部署呢?

如果加入到监控体系,那么核心服务类似于tarsAdminRegistry挂了,监控体系可以自动拉取服务?我能想到的方案是必须哟一个监控脚本实时监控服务吧。

那为什么没有把对核心服务tarsAdminRegistry加入到这套服务治理的体系里面来呢?这有点像鸡生蛋,蛋生鸡的问题了。

总结

  1. 安装时,多查看相关的服务日志。
  2. 根据日志搜索相关源代码,定位问题。
  3. 多看说明文档。

参考链接

  1. Tars 基本介绍
  2. Tars 详细介绍
  3. Tencent Tars 的Docker镜像脚本与使用
  4. TARS PHP TCP服务端与客户端开发
  5. Tencent Tars 的Docker镜像脚本与使用 GitHub 源代码
  6. TARS Kubernetes 部署
  7. 腾讯 Tars 基础框架手动搭建——填掉官方 Guide 的坑
  8. TARS 快速入门
  9. 解读 | TARS 开源项目发布 Go 语言版本
  10. Tars PHP Demo
  11. 源代码 Tars TarsPHP