本博客中关于C/C++编程内容的索引
语言特性
编译器
经典实例
无锁编程
- C++无锁编程之自旋锁(spinlock)的实现
- C++无锁编程之AsyncParallelTask框架
- C++-双缓冲(DoubleBuffer)的设计与实现
- C++无锁编程-无锁栈的设计与实现
- 使用C++11的特性来设计和实现API友好的高精度定时器TimerController
本博客中关于C/C++编程内容的索引
在C++项目中,一些场景下,可能会用到一个空的日志类,覆盖项目中的日志实现,让项目不打印日志,比如:
应对以上需求,本文实现了一个不打印日志的类,有如下特点:
实现中有哪一些陷阱呢?主要在std::endl上面。
在重载operator<<操作符的时候,使用LOG_INFO<<"log"<<std::endl;
时,会提示,std::endl是未知类型(unresolved overloaded function type)
关于std::endl的本质,是一个函数模板,本文不做详细阐述,只给出实现方法。
代码如下,使用c++11标准实现:
#ifdef DISABLE_PRJ_LOG
#undef LOG_DEBUG
#undef LOG_INFO
#undef LOG_WARN
#undef LOG_ERROR
#undef LOG_FATAL
#include
#include
class BlankLogObject {
public:
static BlankLogObject& Ins() {
static BlankLogObject obj;
return obj;
}
BlankLogObject& operator<<(void* in) { return *this; }
BlankLogObject& operator<<(char in) { return *this; }
BlankLogObject& operator<<(const char* in) { return *this; }
BlankLogObject& operator<<(const std::string& in) { return *this; }
BlankLogObject& operator<<(int16_t in) { return *this; }
BlankLogObject& operator<<(int32_t in) { return *this; }
BlankLogObject& operator<<(uint32_t in) { return *this; }
BlankLogObject& operator<<(int64_t in) { return *this; }
BlankLogObject& operator<<(uint64_t in) { return *this; }
BlankLogObject& operator<<(float in) { return *this; }
BlankLogObject& operator<<(double in) { return *this; }
BlankLogObject& operator<<(BlankLogObject& in) { return *this; }
BlankLogObject& operator<<(std::ostream& in) { return *this; }
BlankLogObject& operator<<(std::ostream (*in)(std::ostream&)) { return *this; }
typedef std::basic_ostream > endl_type;
BlankLogObject& operator<<(endl_type& (*in)(endl_type&)) { return *this; }
};
}
#define LOG_DEBUG rec::BlankLogObject::Ins()
#define LOG_INFO rec::BlankLogObject::Ins()
#define LOG_WARN rec::BlankLogObject::Ins()
#define LOG_ERROR rec::BlankLogObject::Ins()
#define LOG_FATAL rec::BlankLogObject::Ins()
#endif
int main() {
LOG_INFO << "skf" << 123 << 0.1 << std::endl;
return 0;
}
已知 C++ 11禁止使用COW,本文进行验证。
#include
#include
#include
int main() {
std::string origin("cow\n");
std::string copy( origin);
printf(origin.c_str());
printf(copy.c_str());
printf("origin\taddress is %x\n",(int64_t)origin.c_str());
printf("copy\taddress is %x\n",(int64_t)copy.c_str());
copy[0] = 'w';
printf(origin.c_str());
printf(copy.c_str());
printf("origin\taddress is %x\n",(int64_t)origin.c_str());
printf("copy\taddress is %x\n",(int64_t)copy.c_str());
return 0;
}
$ g++ -o cow cow.cpp --std=c++98
$ ./cow
cow
cow
origin address is 8c69cc40
copy address is 8c69cc20
cow
wow
origin address is 8c69cc40
copy address is 8c69cc20
$ g++ -o cow cow.cpp --std=c++11
$ ./cow
cow
cow
origin address is 28240270
copy address is 28240250
cow
wow
origin address is 28240270
copy address is 28240250
$ g++ -o cow cow.cpp --std=c++98
$ ./cow
cow
cow
origin address is aa9028
copy address is aa9028
cow
wow
origin address is aa9028
copy address is aa9058
$ g++ -o cow cow.cpp --std=c++11
$ ./cow
cow
cow
origin address is 23a0028
copy address is 23a0028
cow
wow
origin address is 23a0028
copy address is 23a0058
从gcc的实际的实现看,cow 并非是c++11之后禁止的,而是从gcc的某个版本开始禁止cow机制,与c++11无必然关系。
基础类型的repeated字段,包含 repeated int32, int64, float,double,bool等,但不包含string、bytes、message
比如:
message Item {
int32 id = 1;
int32 score = 2;
}
message R {
repeated Item items = 1;
}
改为下面的设计,会提升序列化和反序列效率
message R {
repeated int32 item_id = 1;
repeated int32 item_score = 2;
}
原理是非string的基础类型的repeated字段,在申请内存时pb会申请连续线性大块内存,效率高;而message 的repeated字段,会按对象逐个去申请空间。
字符串是一种不定长的数据结构,内存管理方式成本较高。通过转换成repeated uint32类型,则可以获得更高效的管理。
除此之外,repeated uint32 也支持由arena管理。
假设3个网络服务的调用关系如下:
A->B->C。
其中存在某些pb结构仅会由B透传给C,而B不需要解析,则可以把这些pb放入定义为any类型的字段中。
CopyFrom是深拷贝,若要实现浅拷贝则可以通过 set_allocated_xxx/release 两个函数进行控制
set_allocated_xxx的风险在于,pb析构的时候会把元素也析构掉,无法重复利用。且在一些特殊场景,在无法控制pb析构而不能使用release函数。这些场景可能是pb的析构工作由框架控制,旧的代码封装层次太深等。
这种情况可以使用unsafe_arena_set_allocated_xxx 避开这个问题。
举例一C++系统中,模块A依赖 PB;模块B依赖PB,而模块C依赖A和B。则编译模块C时一定要同时编译A、B、PB。
良好的可扩展性使得protobuf更好地向后兼容。上游更新了proto,新增字段,下游虽然没有更新proto文件,但是新增的字段依然可以保留,来自上游的字段可以透传给下游。拼接下游请求的结构pb时,尽可能使用CopyFrom,避免把字段逐个set。
为了更好地向后兼容,应该避免修改proto文件中现有字段的名字、类型。需要修改时,通过追加新字段(字段编号增加),弃用旧字段的方式。
protobuf被广泛使用,饱经业界考验,如果遇到问题,绝大多数还是自身软件设计的问题。遇到问题,首先不应该怀疑protobuf,应该把视角集中到去发现自身的系统设计缺陷中。
公司里一个c++网络服务中, PV较低时没有内存泄露;而PV较高,cpu idle降到30%以下,开始内存泄露,直到OOM。
用了 tcmalloc和gperf,逐步定位是protobuf 申请 repeated字段的空间,没有及时释放。repeated字段约1k~1w的规模。然后逐步缩小范围。
竟然是释放内存,都放到了一个线程中。当流量大时,单个线程计算能力成为瓶颈,内存释放变慢,表现为内存泄漏。
结论 brpc的自适应限流,只适合大流量,且流量稳定的场景。不适合小流量的服务。
错误提示
dial tcp 34.64.4.17:443: i/o timeout
go env -w GO111MODULE=on
// 开启go mod
go env -w GOPROXY=https://goproxy.cn,direct
// 切换下载代理
配置代理解决
此实例通过c++11实现。
#pragma once
#include
#include
class Spinlock {
public:
Spinlock() : flag(ATOMIC_FLAG_INIT), wait_count(2000) {}
void lock() {
int64_t i = 0;
while(flag.test_and_set(std::memory_order_acquire)) {
__asm__ ("pause");
if (++i > wait_count) {
sched_yield();
i = 0;
}
}
}
bool try_lock() {
if (flag.test_and_set(std::memory_order_acquire)) {
return false;
}
return true;
}
void unlock() {
flag.clear(std::memory_order_release);
}
private :
std::atomic_flag flag;
int32_t wait_count;
};
简介
AsyncParallelTask框架,是为了适用于Rank3.0的拆包合包业务场景而设计和实现的异步任务调度框架,且具有良好的通用性和可移植性。
Rank拆包合包的业务场景,即Rank3.0接受到请求后,拆包往下游预估服务分发请求,在接收到返回后合并打分结果,最终把结果返回给上游。
使用AsyncParallelTask框架的优点
拆包合并由框架控制,免去了自己控制拆包后多个子任务的状态管理。
无锁化,没有锁竞争,性能高。
提供超时管理机制,有助于增强系统稳定性。
拥有友好的API,使用简单。
AsyncParallelTask框架可适用的场景举例
需要拆包合包的预估服务,比如Rank模块
搜索引擎的merger模块
其他需要拆包合包的业务场景
设计
设计思想
使用异步IO方式,不会引起线程阻塞,且通过超时控制来避免长尾任务。
通过使用原子变量和原子操作(atomic)来控制计数、状态变化。
支持多线程,且逻辑实现不使用任何种类的锁,使用lockfree数据结构和线程间通信机制来保证线程安全。
通过使用C++11标准的新特性,比如仿函数、参数模板等,来对外提供简洁和更加友好的API。
类域设计
AsyncParallelTask框架,总共包含控制器AsyncTaskController、定时器TimerController、异步并行任务类AsyncParallelTask、分发子任务类AsyncParallelSubTask等4部分组成。
控制器AsyncTaskController
AsyncTaskController是AsyncParallelTask的调度器,提供调度AsyncParallelTask的接口。内部包含线程池、定时器。
当AsyncTaskController调度AsyncParallelTask的运行时,首先把AsyncParallelTask放入线程池中调度,然后启动对AsyncParallelTask的超时监控。
定时器TimerController
TimerController的实现原理
TimerController如何实现定时的原理和libevent并无不同,都使用了Reactor的设计模式。但是TimerController通过生产者、消费者模型,来实现多线程添加定时任务,并保证线程安全。TimerController使用了C++11的新特性,简化了代码实现。
使用最小堆来管理延时任务和周期任务
使用1个timerfd配合epoll来实现定时任务的激活
使用1个eventfd配合epoll来实现定时任务的添加
使用一个LockFree的栈,实现生产者消费者模型。由外部多线程写,TimerController内部1个线程来读。
TimerController内部流程图
异步任务基类AsyncTask
任何继承AsyncTask的类都可供AsyncTaskController进行调度。
AsyncTask中定了一个基本的接口和AsyncTask的状态的转换。
部分代码举例:
class AsyncTask {
public:
enum Status {
UNSCHEDULED,
PROCESSING,
WAIT_CALLBACK,
CALLBACK,
TIMEOUT,
EXCEPTION,
FINISHED
};
AsyncTask() :
id(0),
parent_id(0),
timeout_threshold(0),
status(UNSCHEDULED) {}
virtual ~AsyncTask() {}
virtual Status process() = 0;
virtual Status timeout() { return TIMEOUT; }
virtual void destroy() {}
virtual void reset() {
id = 0;
parent_id = 0;
timeout_threshold = 0;
status = UNSCHEDULED;
}
virtual void callback() {}
virtual void callbackExcepiton() {}
virtual void callbackTimeout() {}
…….
private:
int64_t id;
int64_t parent_id;
int32_t timeout_threshold; // millisecond;
std::atomic
};
AsyncTask的状态转换图
AsyncTask约束了异步任务的7种状态,和8中状态转换。其中TIMEOUT和EXCEPITON是等效的,为了方便区分两种异常而设置两个状态。
并行任务AsyncParallelTask
AsyncParallelTask内部流程图
并行子任务AsyncParallelSubTask
拆包后分发操作主要在AsyncParallelSubTask中执行。需要创建AsyncParallelSubTask的子类,实现其中分发操作和合并结果的操作。
使用举例
初始化AsyncTaskController
在进程Init中执行,全局单例。设置分发的线程池。
static ThreadPool thread_pool(config.getWorkerThreadNum());
auto& task_controller = Singleton::GetInstance();
task_controller.setThreadPool(&thread_pool);
定义AsyncParallelSubTask的子类PredictAsyncParallelSubTask
主要实现process和mergeResult两个函数,具体参考
https://gitlab.vmic.xyz/iai_common/rank/blob/experiment3/task/predict_async_parallel_subtask.h
https://gitlab.vmic.xyz/iai_common/rank/blob/experiment3/task/predict_async_parallel_subtask.cpp
class PredictAsyncParallelSubTask : public AsyncParallelSubTask {
public:
PredictAsyncParallelSubTask() :
alg_info(nullptr),
context(nullptr),
split_info({0}) {}
virtual ~PredictAsyncParallelSubTask() {}
virtual Status process() {
if (nullptr == context) {
throw std::runtime_error("context is nullptr");
}
if (nullptr == alg_info) {
throw std::runtime_error("alg_info is nullptr");
}
PredictService::asyncRequestZeusServer(this, *context, *alg_info, split_info);
return WAIT_CALLBACK;
}
virtual void mergeResult();
virtual void reset() {
AsyncParallelSubTask::reset();
alg_info = nullptr;
split_info = {0};
context = nullptr;
temp_res.Clear();
}
void collectResult(const zeus::proto::ZeusResponse& res) {
auto& zeus_res = const_cast(res);
temp_res.mutable_item()->Swap(zeus_res.mutable_item());
temp_res.mutable_model_response()->Swap(zeus_res.mutable_model_response());
}
void setAlgInfo(AlgInfo* alg_info) { this->alg_info = alg_info;};
void setRankContext(RankContext *context) { this->context = context;}
void setSplitInfo(SplitInfo& split_info) { this->split_info = split_info;}
private:
void praseZeusToScoreItem(TargetCode code, double score, ScoreItem *score_item);
AlgInfo* alg_info;
RankContext *context;
SplitInfo split_info;
zeus::proto::ZeusResponse temp_res;
};
创建AsyncParallelTask
具体参考
class PredictRankTask : public AsyncTask {
public:
……
private:
AsyncParallelTask parallel_task;
……
};
……
for (int32_t partition_id = 0; partition_id < partition_count; ++partition_id) {
int64_t total_count = req_item_size;
int64_t offset = split_count * partition_id;
int64_t end = offset + split_count;
end = end > total_count ? total_count : end;
SplitInfo split_info({total_count,
split_count,
partition_count,
partition_id,
offset,
end});
auto sub_task = std::make_shared();
sub_task->setAlgInfo(const_cast(&alg_info));
sub_task->setSplitInfo(split_info);
sub_task->setRankContext(&getContext());
parallel_task.addSubTask((std::shared_ptr)sub_task);
}
……
auto task = this;
parallel_task.setAllDoneCallback([=]() {
task->response();
task->setStatusCallback();
});
parallel_task.setIncomplateCallback([=]() {
task->response(Error::E_INCOMPLATE, "some predict server is error!");
task->setStatusCallback();
});
parallel_task.setAllFailCallback([=]() {
task->response(Error::E_PREDICT_ALL_FAILED, "all predict server is error!");
task->setStatusCallback();
});
int32_t timeout = PredictService::getMaxTimeout(scene_id, sub_alg);
parallel_task.setTimeoutCallback(timeout, [=]() {
task->response(Error::E_PREDICT_ALL_TIMEOUT, "all predict server timeout!");
task->setTimeout();
});
auto& task_controller = Singleton::GetInstance();
parallel_task.setController(&task_controller);
parallel_task.setId(task_controller.generateUniqueId());
setStatusWaitCallback(std::memory_order_relaxed);
task_controller.scheduleImmediately(¶llel_task);
执行调度
task_controller.scheduleImmediately会在当前线程分发拆包到线程池。而task_controller.schedule则会在线程池中选择一个线程分发。
auto& task_controller = Singleton::GetInstance();
parallel_task.setController(&task_controller);
parallel_task.setId(task_controller.generateUniqueId());
setStatusWaitCallback(std::memory_order_relaxed);
task_controller.scheduleImmediately(¶llel_task);
编码
源码地址:
测试
压力测试
测试机器为 2017H2-A1-1, 32 core机器
QPS cpu num of items body length session latency P99 session latency AVG
client latency AVG
bandwidth mem remark
300 56% 1W 200KB 43 35 40 3.4 Gb/s 1%
1600 62% 2k 40KB 31 21.6 24 3.64Gb/s 1.1%
稳定性测试
测试方法:
CPU 60%的压力下,持续测试24小时。
测试结果:
Rank服务可稳定提供服务。无内存泄露。
极限测试
测试过程:
缓慢把CPU压力从30%提升到90%~100%之间,并维持10分钟,然后把cpu压力降低至60%。整个过程中观察Rank稳定性,有无内存泄露。
测试结果:
CPU压力达到90%以上时,Rank内存增长,超时错误日志变多,定时器失准,返回大量超时、错误。
Cpu压力降低至60%之后,Rank服务恢复正常,内存使用率变小,无内存泄露,超时错误日志不再新的产出。
符合预期。
打分一致性测试
测试方法:
使用rank-diff工具,从passby环境,复制两份流量请求新旧rank生产环境,分别记录打分结果。最后通过python脚本统计打分结果差异。
测试结果:
1000qps,新旧rank打分一致,差异率小于99.9%,满足需求。
产生差异的数据,分为两种。1)为打分近似值,差别0.01以下。 2)打分无效取默认值0.001.
有锁Rank和无锁Rank性能对比
2k条广告时,1600qps,有锁和无锁Rank压力测试性能对比
测试机器 CPU 32 cores,其中QPS、带宽都是相同的。
有锁
无锁
remark
QPS 1600
相同
CPU 54.1% 63%
session latency AVG 15 21.7
session latency P99 21 31
bandwidth 3.64Gb/s 3.64Gb/s 相同
req body length 40 KB 40 KB 相同
Context Switch
压力测试工具是软件测试过程中的必用工具,而压力测试工具如何控制流量大小呢?
最常见的是计算每个请求之间的时间间隔,然后通过sleep方法来让两次请求产生间隔。这种方法有2个缺点,sleep时会让线程挂起,所以需要比较多的线程数;其二,当流量非常大的情况,比如qps达到10万以上时,会收到系统时钟精度和线程上下文切换的挑战。
本文设计了另外一种方法,采用了按照概率控制流量大小。但概率的计算并不依赖随机数,而是通过设置一个概率控制变量的方法,让流量的发送更加均衡。
代码如下:
class Transformer {
public:
Transformer() :
send_num(0),
qps(0),
benchmark(0),
counter(0),
thread_pool(10) {}
void run();
void stop() { tc.stop(); }
private:
void sendOne();
void transform();
int32_t send_num;
int32_t qps;
int32_t benchmark;
std::atomic counter;
ThreadPool thread_pool;
TimerController tc;
};
void Transformer::run() {
qps = FLAGS_qps;
if (qps <= 0 || qps > 1e5) {
return;
}
int32_t query = (qps << 16) / 1000;
send_num = query >> 16;
query &= 0xFFFF;
for (int i = 0; i < 16; ++i) {
benchmark <<= 1;
if (query & 1) {
++benchmark;
}
query >>= 1;
}
tc.cycleProcess(1, [this]() { this->transform(); });
}
void Transformer::transform() {
uint32_t cur_c = counter.fetch_add(1, std::memory_order_relaxed);
cur_c &= 0xFFFF;
if (cur_c <= 0) {
return;
}
int32_t delta = 0;
for (int i = 0,bit = 1; i < 16; ++i, bit <<= 1) {
if ((cur_c & bit) == 0) {
continue;
}
if ((benchmark & bit) == 0) {
break;
} else {
delta = 1;
break;
}
}
int32_t cur_send_num = send_num + delta;
if (cur_send_num <= 0) {
return;
}
for (int i = 0; i< cur_send_num; ++i) {
thread_pool.enqueue([this]() { this->sendOne(); });
}
}
#pragma once
#include
template
class DoubleBuffer {
public:
DoubleBuffer() : cur_index(0) {}
T& getWorkingBuffer(std::memory_order order = std::memory_order_seq_cst) {
return buffers[cur_index.load(order)];
}
T& getBackupBuffer(std::memory_order order = std::memory_order_seq_cst) {
return buffers[1 ^ cur_index.load(order)];
}
void switchBuffer(std::memory_order order = std::memory_order_seq_cst) {
cur_index.fetch_xor(1, order);
}
private:
T buffers[2];
std::atomic cur_index;
};
本文是用过C++11标准下的原子变量atomic的CAS操作compare_exchange_weak,来实现无锁栈(Lock-free Stack)。
#ifndef LOCK_FREE_STACK_HPP
#define LOCK_FREE_STACK_HPP
#include
template
class LockFreeStack {
public:
struct Node {
Node() : data(nullptr), pre(nullptr) {}
void reset() { data = nullptr; pre = nullptr;}
T *data;
Node* pre;
};
LockFreeStack() : count(0), back(nullptr) {}
~LockFreeStack() { clear(); }
void push(const T* data) {
Node* node = alloc();
node->data = const_cast(data);
for (;;) {
node->pre = back;
if (back.compare_exchange_weak(node->pre, node, std::memory_order_seq_cst) ) {
break;
} else {
continue;
}
}
++count;
}
T* pop() {
for (;;) {
Node* back_node = back;
if (nullptr == back_node) { return nullptr; }
Node* second_node = back_node->pre;
if(!back.compare_exchange_weak(back_node, second_node, std::memory_order_seq_cst)) {
continue;
} else {
--count;
T* data = back_node->data;
recycle(back_node);
return data;
}
}
}
bool empty() { return (bool)count; }
int64_t size() { return count; }
void clear() { while (nullptr != pop()) {}; }
private:
Node *alloc() { return (new Node()); }
void recycle(Node *node) { delete node; }
std::atomic count;
std::atomic back;
};
#endif
pip install pip-review
# 安装第三方包pip-review
pip-review
#查看可更新
pip-review --auto
#自动批量升级
pip-review --interactive
#以交互方式运行,对每个包进行升级
创建配置文件
mkdir ~/.pip
cd ~/.pip
vim pip.conf
编辑 pip.conf文件,添加如下内容:
[global]
index-url = https://mirrors.aliyun.com/pypi/simple
[install]
trusted-host=mirrors.aliyun.com
中国国内推荐的pip源如下:
需要安装两个库pymysql和csv
pip install pymysql csv
代码示例如下:
import pymysql
import csv
import sys
reload(sys)
sys.setdefaultencoding('utf8')
def from_mysql_get_all_info():
conn = pymysql.connect(
host='mysql.dba.zuocheng.net',
port=13306,
user='root',
db='test_db',
password='test_pw',
charset='utf8')
cursor = conn.cursor()
sql = "select * from test_table"
cursor.execute(sql)
data = cursor.fetchall()
conn.close()
return data
def write_csv():
data = from_mysql_get_all_info()
filename = 'data_full.csv'
with open(filename,mode='w') as f:
write = csv.writer(f,dialect='excel')
for item in data:
write.writerow(item)
write_csv()
最新的TimerController代码保存在Github上面:https://github.com/zuocheng-liu/StemCell ,包含timer_controller.h 和 timer_controller.cpp两个文件,欢迎审阅!
尤其在使用C++开发网络应用时常遇到下面的问题:
一、软件设计中,不会缺少通过使用定时器的来实现的场景,比如超时控制、定时任务、周期任务。
二、C/C++标准库中只有最原始的定时接口。没有提供功能完备的库来满足上面提到复杂场景。
三、第三方库中的定时器,往往存在一些问题,比如:
以上问题会让开发者开发新系统时带来一些困扰,但C++11新特性的出现,带来了解决上面问题的新思路。
接口参数支持C++11的lamaba表达式,让定时器的接口对开发人员更加友好。
TimerController的代码总计300~400行,而且功能完备,代码健壮。
C++11的新特性的使用,让代码更简洁,增强代码的可读性、可维护性。
线程安全,是绕不开的问题。第三方库libevent等,在多线程环境中使用总是危险的。TimerController在设计之初就保证多线程环境下运行的安全性。
TimerController,没有依赖任何第三方库,完全依靠C/C++标准库和C++11的新特性来实现。
class TimerController {
bool init(); // 初始化资源,并启动定时器
void stop(); // 停止定时器,所有定时任务失效
// 定时运行任务,参数delay_time单位是毫秒。后面参数是lamba表达式
template<class F, class... Args>
void delayProcess(uint32_t delay_time, F&& f, Args&&... args);
// 周期运行任务,参数interval单位是毫秒,后面参数是lamba表达式。
template<class F, class... Args>
void cycleProcess(uint32_t interval, F&& f, Args&&... args);
}
用一个实例来讲解TimerController的使用方法:
#include <iostream>
#include "timer_controller.h"
using namespace std;
using namespace StemCell;
int main() {
srand((unsigned)time(NULL));
try {
TimerController tc;
tc.init(); // 初始化 TimerController
tc.cycleProcess(500, [=]() { cout << "cycle 0.5 sec" << endl; });
for (int i = 0; i < 80; ++i) {
// 随机产生80个延时任务,并延时执行
auto seed = rand() % 8000;
tc.delayProcess(seed + 1, [=]() { cout << "delay:" << seed << endl; });
}
sleep(8); // 主线程睡眠8秒,让延时任务得以执行
tc.stop(); // 停止 TimerController
cout << "tc stoped!" << endl;
} catch (exception& e) {
cout << "error:" << e.what();
}
return 0;
}
TimerController如何实现定时的原理和libevent并无不同,都使用了Reactor的设计模式。但是TimerController通过生产者、消费者模型,来实现多线程添加定时任务,并保证线程安全。TimerController使用了C++11的新特性,简化了代码实现。
TimerController内部只有1个线程在执行定时任务。当高延时的任务增多时,可能会影响到任务运行的调度时间,高延时的任务需要在放入新的线程中运行。示例如下:
TimerController tc;
tc.init(); // 初始化 TimerController
// 把任务放入新线程或线程池中
tc.delayProcess(50, []() { std::thread([]() { do_long_time_task();}) });
为了系统简洁,TimerController全局单例即可。
auto& tc = Singleton< TimerController >::GetInstance();
如何避免CPU负载高时,定时器失准的问题?
具体实现在 timer_controller.h 和 timer_controller.cpp两个文件里面。
FROM centos:centos7.4.1708
MAINTAINER Zuocheng Liu
RUN yum -y --nogpgcheck install gcc gcc-c++ kernel-devel make cmake libstdc++-devel libstdc++-static glibc-devel glibc-headers \
&& yum -y --nogpgcheck install openssl-devel gperftools-libs \
&& yum -y --nogpgcheck install psmisc openssh-server sudo epel-release \
&& yum -y --nogpgcheck install vim git ctags \
&& mkdir /var/run/sshd \
&& echo "root:123456" | chpasswd \
&& sed -ri 's/^#PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config \
&& ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key \
&& ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
docker build -t cpp_dev:last .
mount_in_docker.sh
#!/bin/bash
NEWUSER=$1
PORT=$2
PATH=$(dirname $(readlink -f "$0"))
echo "add User ${NEWUSER} sshd Port ${PORT}"
if [ ! -d /data/rtrs/${NEWUSER} ]; then
/usr/bin/mkdir /data/rtrs/${NEWUSER}
fi
/usr/bin/cp /data/liuzuocheng/.profile /data/${NEWUSER}/.profile
/usr/bin/docker run -itd --name ${NEWUSER} --net=host -v /data/${NEWUSER}:/home/${NEWUSER} -v /data:/data rtrs/dev_cpp:centos7.4.1708
/usr/bin/docker exec -i ${NEWUSER} sh ${PATH}/mount_in_docker.sh ${NEWUSER} ${PORT} ${UID}
NEWUSER=$1
PORT=$2
NEWUID=$3
echo "add User ${NEWUSER} sshd Port ${PORT}"
/usr/sbin/useradd -r -u ${NEWUID} -d /home/${NEWUSER} ${NEWUSER}
echo "${NEWUSER}:123456" | /usr/sbin/chpasswd
echo "Port ${PORT}" >> /etc/ssh/sshd_config
/usr/sbin/sshd
docker的默认目录在/var/lib/docker 下面, 往往/var的磁盘比较小,建议把docker的目录改为大磁盘
修改/etc/sudoers文件, 添加一行 user-name ALL=(ALL) NOPASSWD: ALL
<h1>Hello!</h1>
。编译: gcc -o hello_server hello_server.c
运行: ./hello_server
请求: curl http://localhost:8888/any
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 8888
#define BUFFER_SIZE 4096
#define RESPONSE_HEADER "HTTP/1.1 200 OK\r\nConnection: close\r\nAccept-Ranges: bytes\r\nContent-Type: text/html\r\n\r\n"
#define RESPONSE_BODY "<h1>Hello!</h1>"
int handle(int conn){
int len = 0;
char buffer[BUFFER_SIZE];
char *pos = buffer;
bzero(buffer, BUFFER_SIZE);
len = recv(conn, buffer, BUFFER_SIZE, 0);
if (len <= 0 ) {
printf ("recv error");
return -1;
} else {
printf("Debug request:\n--------------\n%s\n\n",buffer);
}
send(conn, RESPONSE_HEADER RESPONSE_BODY, sizeof(RESPONSE_HEADER RESPONSE_BODY), 0);
close(conn);//关闭连接
}
int main(int argc,char *argv[]){
int port = PORT;
struct sockaddr_in client_sockaddr;
struct sockaddr_in server_sockaddr;
int listenfd = socket(AF_INET,SOCK_STREAM,0);
int opt = 1;
int conn;
socklen_t length = sizeof(struct sockaddr_in);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(port);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
printf("bind error!\n");
return -1;
}
if(listen(listenfd, 10) < 0) {
printf("listen failed!\n");
return -1;
}
while(1){
conn = accept(listenfd, (struct sockaddr*)&client_sockaddr, &length);
if(conn < 0){
printf("connect error!\n");
continue;
}
if (handle(conn) < 0) {
printf("connect error!\n");
close(conn);
continue;
}
}
return 0;
}
为什么要写这篇博文?
原因是,在使用公司里的自动化平台部署c++服务时,拿这个简单的示例来测试平台是否有问题。俗称趟一遍坑儿。
在网上也搜索了很多不少博文,发现里面的代码有几个问题,第一个问题就是编译不过,第二个问题则是有的代码应答必须要有文件,这对我的测试也造成了些许麻烦。
所以就自己参考别人的列子,在自己的博客里写一个简单的吧。以后再去趟别的自动化部署系统的坑的时候,顺手就能拿来用。
以nginx 为例: d PL=pidof nginx
PL=${PL//\ /,}
top -p $PL
ssh scp等消除每次问yes/no
ssh -o StrictHostKeyChecking no dev.zuocheng.net
链接复用
创建ssh sock链接目录
mkdir ~/.ssh/socks
修改~/.ssh/config文件,若该文件不存在,则创建。增加以下内容:
Host *
KeepAlive yes
ServerAliveInterval 60
ControlMaster auto
ControlPersist yes
ControlPath ~/.ssh/socks/%h-%p-%r
常用参数 -Irn
清空所有log文件
find ./ -name *.log | xargs truncate -s 0
删除所有nohup文件
find ./ -name nohup.out | xargs rm -rf
find ./ -name *.swp | xargs rm -rf
删除过期日志(60天前)
find ./ -mtime +60 -name log.* -exec rm -rf {} \;
使用正则删除过期日志(7天有效期)
30 4 * * * (find /workdir -type f -mtime +7 -regex '.+?/log/app\.20[0-9]+' -exec rm -f {} \;)
把当前目录下所有文件都MD5
find ./ -type f -exec md5sum {} \; | sort -k 2 > md5sum
依据时间戳获取时间
date -d @1525856172
生成日期
DATE=$(date +%Y%m%d)
生成MD5值 find ./dir -type f -exec md5sum {} \; > md5sum.txt
生成MD5值,并排除日志文件 find "dir" -path 'dir/log*' -prune -o -type f -exec md5sum {} \; > md5sum.txt
校验md5值 md5sum -c > md5sum.txt
sed -i '/^$/d'
删除空行
sed -i 's/\s*$//g'
#删除行尾空白
Thrift 存在的一些问题:
由此提出猜想和假设:
从实现难度和工作量上的考虑,本文尝试实现第一个假设,“将 Thrift 的底层通信框架抛离出Thrift框架,利用其来构建一般性的网络应用”。第二个假设希望日后,作者在时间和精力富余的时候再进行试验。
有两种方法:
下面对这两种方法做详细介绍:
举例:
namespace cpp com.thrift.test
struct Parameter{
1: required string bin_data;
}
service DemoService{
i32 demoMethod(1:string param1, 2:Parameter param2);
}
将新的协议序列化后的数据放入bin_data中,这种方法缺点是,自己定义的协议,还要被thrift的序列化反序列协议包裹,不能完全消除thrift序列化和反序列化的代价。
第一种方法太过简单和粗糙,因此经过挖掘thrift代码后,探索出了更精细的方法。
Thrift 底层通信模块的四大基类,TServer、TProcotol、TProcessor、TTransport,其中TProcessor::process是负责处理具体业务逻辑入口。
class TProcessor {
public:
virtual ~TProcessor() {}
virtual bool process(boost::shared_ptr<protocol::TProtocol> in,
boost::shared_ptr<protocol::TProtocol> out) = 0;
bool process(boost::shared_ptr<apache::thrift::protocol::TProtocol> io) {
return process(io, io);
}
protected:
TProcessor() {}
};
因此,只要自定义实现TProcessor的基类,重写process方法,就能自定义自己的网络应用。
下面是一个Hello world应用的简单实现:
首先实现一个HelloWorldProcessor 类。’
class HelloWordProcessor : public apache::thrift::TProcessor {
public:
virtual bool process(boost::shared_ptr<apache::thrift::protocol::tprotocol> in, boost::shared_ptr</apache::thrift::protocol::tprotocol><apache::thrift::protocol::tprotocol> out) {
out->writeBinary("Hello world!");
out->getTransport()->flush();
out->getTransport()->close();
GlobalOutput.printf("send bytes %s", "Hello world!");
return true;
}
};
然后构建main函数,本实例使用TSimpleServer模型
using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::processor;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;
int main(int argc, char **argv) {
boost::shared_ptr<tprotocolfactory> protocolFactory(new TBinaryProtocolFactory());
boost::shared_ptr<tprocessor> processor(new UwsgiProcessor());
boost::shared_ptr<tservertransport> serverTransport(new TServerSocket(9090));
boost::shared_ptr<ttransportfactory> transportFactory(new TBufferedTransportFactory());
TSimpleServer server(processor,
serverTransport,
transportFactory,
protocolFactory);
printf("Starting the server...\n");
server.serve();
printf("done.\n");
return 0;
}
最后编译、链接和运行。
简单实现一个socket客户端,发送请求,就能得到HelloWord。
待完善
待完善
作者写本文,正是在工作中遇到了一些真实的问题,比如thrift序列化反序列化慢,thrift底层那么优秀的通信框架如何更高的加以利用呢?因此带着工作中的一些问题,开始阅读thrift的源代码。
除了本文中的一些实例,作者还做了一个小的代码库,里面就用到了本文中的方法,单独使用了thrift了网络框架,Github地址如下:https://github.com/zuocheng-liu/GI。
最近在重构公司的一个C++模块,逻辑里有排序、过滤等操作。开发过程中,遇到下面的一些问题:
先写测试总结,测试方法、测试结果都在下面。
这次项目最大的收获就是,如果有过滤操作,优选使用list。vector的缺点如下,1.vector快速排序需要移动元素,如果元素占据空间大,移动代价也非常大。2.vector过滤需要借助中间临时存储,直接erase的代价更大。
直接列出测试结果,单位是微秒
vector for_each : | 3263 |
list for_each : | 2556 |
vector traverse : | 4783 |
vector num traverse : | 666 |
list traverse : | 3078 |
vector push_back : | 8136 |
list push_back : | 24581 |
list delete : | 7428 |
vector push_back : | 7329 |
list push_back : | 22968 |
vector clear : | 0 |
list clear : | 13079 |
vector sort : | 89512 |
list sort : | 146529 |
Github 托管地址,保持更新:https://github.com/zuocheng-liu/code-samples/blob/master/linux-c/stl/vector_list_performance_test.cpp
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include <list>
#include <vector>
#include <iostream>
#define MAX_NUM 100000
using namespace std;
timeval tv;
uint64_t timer;
bool NumCmp(uint32_t a, uint32_t b) { return (a > b); }
void startTimer() {
gettimeofday(&tv,NULL);
timer = 1000000 * tv.tv_sec + tv.tv_usec;
}
uint64_t stopTimer() {
gettimeofday(&tv,NULL);
timer = 1000000 * tv.tv_sec + tv.tv_usec - timer;
return timer;
}
int main() {
vector<uint32_t> v;
vector<uint32_t> v2;
list<uint32_t> l;
list<uint32_t> l2;
for (int i = 0; i< MAX_NUM; ++i) {
srand((int)time(0) * i * i + 1);
int num = rand();
v.push_back(num);
l.push_back(num);
}
// compare oper traverse
startTimer();
for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
*iter;
}
cout<<"vector\t traverse\t :\t"<< stopTimer() << endl;
startTimer();
for (int i = 0 ; i < MAX_NUM; ++ i) {
//v[i];
}
cout<<"vector\t num traverse\t :\t"<< stopTimer() << endl;
startTimer();
for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
}
cout<<"list\t traverse\t :\t"<< stopTimer() << endl;
// compare oper push_back
startTimer();
for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
v2.push_back(*iter);
}
cout<<"vector\t push_back\t :\t"<< stopTimer() << endl;
startTimer();
for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
l2.push_back(*iter);
}
cout<<"list\t push_back\t :\t"<< stopTimer() << endl;
// compare oper delete
startTimer();
v2.clear();
for (vector<uint32_t>::iterator iter = v2.begin(); iter != v2.end(); ++ iter) {
// v2.erase(iter);
}
//cout<<"vector\t delete\t :\t"<< stopTimer() << endl;
startTimer();
for (list<uint32_t>::iterator iter = l2.begin(); iter != l2.end(); ++ iter) {
iter = l2.erase(iter);
}
cout<<"list\t delete\t :\t"<< stopTimer() << endl;
// compare oper push_back
startTimer();
for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
v2.push_back(*iter);
}
cout<<"vector\t push_back\t :\t"<< stopTimer() << endl;
startTimer();
for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
l2.push_back(*iter);
}
cout<<"list\t push_back\t :\t"<< stopTimer() << endl;
// compare oper clear
startTimer();
v2.clear();
cout<<"vector\t clear \t:\t"<< stopTimer() << endl;
startTimer();
l2.clear();
cout<<"list\t clear \t :\t"<< stopTimer() << endl;
// compare oper sort
startTimer();
std::sort(v.begin(), v.end(), NumCmp);
cout<<"vector\t sort \t :\t"<< stopTimer() << endl;
startTimer();
l.sort(NumCmp);
cout<<"list\t sort \t :\t"<< stopTimer() << endl;
return 0;
}
最近我的同事和一些网友都说C/C++中“空语句”(就是单独一个分号的语句)具有延时的作用,可以用来写延时代码。其实这是一种错误的理解。
首先,有人认为空语句经编译后,生成汇编代码是“NOP”指令,NOP指令是空操作指令,执行一个指令周期时间,所以认为C/C++中的“空语句”还有延时的功能,其实这是错误的,“空语句”是不会生成任何有效的指令代码的,是不具有延时做用的。
有人说如下代码是具有延时做用,实际上下边的延时功能主要是加法运算和条件判断运算指令起到了延时的作用。
They say a good example is worth 100 pages of API documentation, a million directives, or a thousand words.
最近项目中决定使用Google的C++代码规范,然后写了两个代码示例文件(.h 和 .cc),这两个文件中的代码都是用Google的C++代码规范。
下面地址保持持续更新:
https://github.com/zuocheng-liu/code-samples/tree/master/code_style
仓促之作,错误和疏忽不可避免,但保证后续不断更新。
建议点击上面的链接,获取最新版。编辑器的原因,缩进的显示可能有问题。
// Copyright (c) 2016, Zuocheng.net
//
// Author: Zuocheng Liu
//
// Lisence: GPL
//
// File: code_samples/code_style/google_code_style.h
// (文件名要全部小写,可以包含下划线(_)或短线(-))
// C++文件以.cc 结尾,头文件以.h 结尾。
//
// Google C++ code Style 代码示例 (简体中文版)
//
// *注释 使用//或/* */,但要统一。
// *注释 每一个文件版权许可及作者信息后,对文件内容进行注释说明
// -------------------------------------------------------------最多不能超过80行
#ifndef CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE_
#define CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE_
// 命名空间的名称是全小写的,其命名基于项目名称和目录结构
// 不要声明命名空间std 下的任何内容
namespace code_samples {
namespace code_style {
// **变量命名**
// 尽可能给出描述性名称,不要节约空间
// 变量名一律小写,单词间以下划线相连
// 少用全用变量,可以以g_与局部变量区分
int g_my_exciting_global_variable;
// 只有在描述数据时用struct ,其他情况都用class
typedef struct CodeStyle {
uint32_t type;
} CodeStyle;
// **类注释**
// 每个类的定义要附着描述类的功能和用法的注释
//
// GoogleCodeStyle , 通过代码示例,展现google c++ code style
//
// (按需注明synchronization assumptions,是否线程安全)
class GoogleCodeStyle {
public :
// ** 声明次序 **
// - typedefs 和enums;
// - 常量;
// - 构造函数;
// - 析构函数;
// - 成员函数,含静态成员函数;
// - 数据成员,含静态数据成员。
// **类型命名**
// 每个单词以大写字母开头,不包含下划线
// 所有类型命名——类、结构体、类型定义(typedef)、枚举——使用相同约定
typedef enum StyleType {
// 枚举值应全部大写,单词间以下划线相连
GOOGLE_CODE_STYLE = 0,
K_AND_R,
POCO
} StyleType;
// 常量命名,在名称前加k
const int kDaysInAWeek = 7;
GoogleCodeStyle() {}
~GoogleCodeStyle() {}
// **函数注释**
// 函数声明处注释描述函数功能,定义处描述函数实现.
// - inputs(输入)及outputs(输出);
// - 对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数;
// - 如果函数分配了空间,需要由调用者释放;
// - 参数是否可以为NULL;
// - 是否存在函数使用的性能隐忧(performance implications);
// - 如果函数是可重入的(re-entrant),其同步前提(synchronization assumptions)
// 举例 :
// 初始化函数
~Init() {}
// 普通函数名以大写字母开头,每个单词首字母大写,没有下划线
//
uint32_t MyExcitingMethod(CodeStype &code_style, char *output);
// 存取函数要与存取的变量名匹配
inline int num_entries() const { return num_entries_; }
inline void set_num_entries(int num_entries) { num_entries_ = num_entries; }
// 类的成员变量以下划线结尾
int num_completed_connections_;
protected :
private :
// **类数据成员**
// 每个类数据成员(也叫实例变量或成员变量)应注释说明用途。
// 如果变量可以接受NULL 或-1, 等警戒值(sentinel values),须说明之
int num_entries_;
}; // class GoogleCodeStyle
}} // namespace code_samples::code_style
#endif //CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE
本文描述3种场景下的单例模式:
本文所写单例模式代码都使用懒汉模式。
注意问题:
使用场景举例 :
进程体内没有运行多线程的单例模式,无需考虑线程同步与互斥
class Singleton {
public:
static Singleton* getInstance() {
if (NULL == instance) {
instance = new SingletonInside();
}
return instance;
}
private:
SingletonInside(){}
~SingletonInside() {}
static Singleton* instance;
};
Singleton::instance = NULL;
进程体内运行多线程单例模式,使用系统mutex保证线程安全
class Singleton {
public:
static Singleton* getInstance() {
pthread_once(&g_once_control, InitOnce);
pthread_mutex_lock(&mutex); // lock
if (NULL == instance) {
instance = new SingletonInside();
}
pthread_mutex_unlock(&mutex); // unlock
return instance;
}
private:
SingletonInside() {
}
~SingletonInside() {
pthread_mutex_destroy(&mutex); // destroy lock
}
static void InitOnce(void) {
pthread_mutex_init(&mutex,NULL); // init lock
}
Singleton* instance;
static pthread_once_t g_once_control;
static pthread_mutex_t mutex;
};
Singleton::instance = NULL;
pthread_once_t Singleton::g_once_control = PTHREAD_ONCE_INIT;
某些资源在单个线程体内需要保持单例,即每个线程体内都保持唯一。每个线程体内,对象相互隔离,则无需考虑线程安全问题。
此种单例模式的实例需要调用系统提供的 TLS 接口,放于线程局部存储中。
使用场景举例:
class Singleton {
public:
static Singleton* getInstance() {
pthread_once(&g_once_control, InitOnce);
Singleton* instance = (Singleton*)pthread_getspecific(g_thread_data_key);
if (NULL == instance) {
instance = new SingletonInside();
pthread_setspecific(g_thread_data_key, (void*)Singleton)
}
return instance;
}
private:
SingletonInside() {}
~SingletonInside() {
pthread_key_delete(g_thread_data_key);
}
static void InitOnce(void) {
pthread_key_create(&g_thread_data_key, NULL);
}
static pthread_once_t g_once_control;
static pthread_key_t g_thread_data_key;
};
pthread_once_t Singleton::g_once_control = PTHREAD_ONCE_INIT;
pthread_key_t Singleton::g_thread_data_key;
如果使用 Poco库 TreadLocal ,代码还会简洁很多,性能上也会好很多
class Singleton {
public:
static Singleton* getInstance() {
if (NULL == instance) {
instance = new SingletonInside();
}
return instance;
}
private:
SingletonInside() {}
~SingletonInside() {
delete instance;
}
static Poco::ThreadLocal<Singleton> instance;
};
Poco::ThreadLocal<singleton> Singleton::instance = NULL;
依据《Java 并 发编程实践》/《Java Concurrency in Practice》一书,一个线程安全的 class 应当满足三个条件:
多线程间共享的标志变量, 可能被编译器优化,存储于cpu寄存器,一个线程将其值改变(内存值),在另一个线程的cpu寄存器对应的值却未同步。
解决方法: 1. 添加 volatile
关键字修饰变量。 并且加互斥锁。 2.不使用共享的标志变量。
防止内存泄露,小心使用堆空间
小心使用栈空间
参数检查一定要严格,任何一个不合法的参数能让线程异常,然后整个app异常
容错
释放时设置为NULL 声明定义时设置为NULL
在大规模软件设计中,线程数量的设定和栈空间的使用必须要小心。
如果程序在线程在启动时固定,则在可动态扩展的容器中,可能会有因为线程数过多而不能充分使用cpu资源。在性能压力测试时,常常会引起在cpu资源使用率较低时,系统则已经到达瓶颈。
本文内容对Spawn-fcgi源码进行解读,简要说明其原理,并具体说明其实现方式。
Spawn-fcgi 源码虽然只有600多行,但是初次阅读起来依然需要花很多时间。为了节省读者的学习成本,提高学习Spawn-fcgi 的效果,作者对Spawn-fcgi的源码做了裁剪,保留最核心的功能和原有的代码结构,且能编译后正常运行。最后代码只有200多行。
要阅读Spawn-fcgi,读者至少需要掌握以下几个方面的知识或技能:
会执行execl 函数,运行cgi程序,并让cgi程序拥有子进程的上下文环境
运行cgi程序后,使用FCGI_LISTENSOCK_FILENO这个描述符,来与webserver进行通信。
gcc -o spawn-fcgi spawn-fcgi.c
一行命令即可
./spawn-fcgi -f cgi -p 9001 -F 256
裁剪后也仅支持接收这三个参数
软件或者系统,不同模块可能由不同的语言编写,必然会存在不同语言之间交互和通信的问题。本文站在PHP的角度,探讨PHP与其他语言交互的方式。
胶水语言Shell scripts Python Ruby Lua Tcl Perl。
其本质是调用操作系统的输入与输出,由操作系统创建由其他语言写成的应用的新进程,再将其输出返回给PHP。
优点是简单容易实现。
缺点是:
将其他语言提供的API封装成C接口(往往需要再编译为动态链接库),在PHP扩展中再对其进行封装,使其可以在PHP语句中直接调用。
优点 :
缺点 :
PNI 是一个PHP 扩展,允许PHP代码中直接调用C的类库,地址 https://it.zuocheng.net/php-native-interface
最终的目的是编写PHP扩展,不同的是SWIG提供的接口让开发者省去了学习比Zend API 和 PHP API 的成本。
如果在语言层面无法直接交互,可使用进程间的交互方法。本质是调用操作系统提供的进程间交互方法。
如果由不同语言编写的系统之间进行交互,则有以下几种方法:
本页面转自: https://github.com/zuocheng-liu/pni/blob/master/README-zh.md
PNI github PAGE : https://github.com/zuocheng-liu/pni
PHP 不是完美的语言,总有一些情况下,不得不使用其他语言来协助完成。在这些特殊的场景下,使用PNI就可以将PHP与其他语言连接起来:
直接编写PHP扩展去调用其他语言的接口是常用方法,不过PNI有更多的好处:
不需要每次有新的需求,就去编写或改动PHP的扩展。对PHP扩展的开发、调试会占用很多的时间。
PHP扩展更改后上线,需要重启PHP服务,这是有一定风险的。
如果使用PNI,就会便捷很多,对新功能的开发和上线,只需操作PHP的代码即可。
开发PHP扩展,需要开发人员去学习 PHP-API、 Zend-API 、 PHP扩展框架,甚至需要深入去理解PHP内核。
有了PNI,问题就简单多了。
使用PNI,可以更灵活地使用本地类库。
方法类,此类定位动态链接库中的函数名
php
$pow = new PNIFunction(PNIDataType::DOUBLE, 'pow', 'libm.so.6');
上面的例子,在构造函数中,第一个参数是需要找寻函数的返回值类型,第二参数是函数的名字,第三个参数是到那个动态链接库中找寻函数。
异常类,在无法找到动态链接库或函数名的时候,会抛出异常。
数据类型类
所有数据类型类都继承PNIDataType抽象类,此抽象类包含3个共有方法
php
getValue(); // 获取值
setValue($value); // 重新赋值
getDataType(); // 获取数据类型
PNIString 和 PNIPointer 中还额外包含一个接口
php
systemFree();
用于释放C函数中malloc申请的内存资源。
强烈不推荐使用PNI直接调用这样的库函数。
表示数据类型常量
php
PNIDataType::VOID
PNIDataType::CHAR
PNIDataType::INTEGER
PNIDataType::LONG
PNIDataType::FLOAT
PNIDataType::DOUBLE
PNIDataType::POINTER
php
try {
$pow = new PNIFunction(PNIDataType::DOUBLE, 'pow', 'libm.so.6');
$a = new PNIDouble(2);
$b = new PNIDouble(10);
$res = $pow($a, $b);
var_dump($res);
} catch (PNIException $e) {
}
上面例子,使用PNI调用系统math库中的pow函数
C++
// file user_math.c
u_int32_t sum(u_int32_t a, u_int32_t b) {
return a + b;
}
– 2.创建动态链接库,并把它放到 $LD_LIBRARY_PATH
包含的目录里
shell
gcc -fPIC -shared -o libusermath.so user_math.c
– 3.创建PHP程序
“`php
// file testPni.php
<?php
try {
$sum = new PNIFunction(PNIDataType::INTEGER, ‘sum’, ‘libusermath.so’);
$a = new PNIInteger(2);
$b = new PNIInteger(10);
$res = $sum($a, $b);
var_dump($res);
} catch (PNIException $e) {
}
“`
– 4.执行PHP程序
shell
$ php testPni.php
$res 是 PNIInteger类型,其中包含数值结果为12的成员变量
PNI 数据类型类 | C 数据类型 | 说明 |
---|---|---|
PNILong | long int/ int | PHP has no unsigned int |
PNIInteger | long int/ int | PHP has no 32bit Int |
PNIDouble | double / float | |
PNIFloat | double / float | PHP has no 32bit float |
PNIChar | char | |
PNIString | char* | |
PNIPointer | char* |
由于PHP只有64整形,所以PNILong 和 PNIInteger 实际上是等效的。
如果通过PNI调用的函数参数类型是32位、16位数据怎么办?需要开发人员保证PNILong和PNIInteger存放的值不能超出大小。
PNIDouble 和 PNIFloat 也是等效的,因为PHP只有64位浮点。如果调用的C函数参数列表里有32位浮点呢? 不用担心,即使是32位的浮点,在x86_64架构的CPU里,也是赋给了64位的浮点运算器。
shell
git clone https://github.com/zuocheng-liu/pni.git
– 编译和安装
shell
cd <src-pni>
phpize
./configure
make && make install
– 配置PHP,使其生效
把下面一行添加到 php.ini
shell
extension=pni.so;
– 重启PHP服务
bash
service php-fpm restart // cgi mode
apachectl restart // sapi mode
// do nothing in cli mode
热盼您的联系!
version 3.01 of the PHP license.(see LICENSE)
The following exception is caused by a lack of memory and not having swap configured
Compile Error: Cannot redeclare class Symfony\Component\Debug\Exception\FlattenException
./vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Exception/FlattenException.php
./vendor/symfony/symfony/src/Symfony/Component/Debug/Exception/FlattenException.php
php app/console doctrine:schema:update –force
配置symfony2\app\config\config.yml
mapping_types:
enum: string
set: string
varbinary: string
tinyblob: text
举例: easyui panel layer
多数的配置是 iframe : true 或者 iframeScrolling:true ,但是设置后滚动条仍然失灵
解决办法,在iframe页面 中加入框架DIV style设置为 width=100%;height=100%;overflow:auto;
PHP示例:
<a href="<?php echo $_SERVER["HTTP_REFERER"]?>" >返回</a>
只用javascript的方法并没有效果,建议不要用:
<a href="javascript:history.go(-1);window.location.reload()">返回</a>
魔法函数、魔法变量,最好只在框架代码、数据访问层中使用,在业务逻辑层或表现层使用有以下缺点
PHP作为若类型语言,如果不做声明和初始化,非常容易出现脏数据,比如上一次循环结束后没有清理遗留数据等。
base的作用相当于多继承,多继承在语义上容易产生歧义。应该保持使用单继承的原则。
降低代码的可读性和可维护性
匿名函数不利于重用和维护。
避免使用闭包,应该用Namespace或者class进行封装。
使用Reflection类代替 ,提高代码可读性和可维护性。
使用 foreach遍历,因为foreach 性能更高,语法更简洁
$_REQUEST包含了$_GET、$_POST、$_COOKIE的所有内容,是它们的集合体。
但有时$_GET和$_COOKIE有同名变量,或者$_POST和$_COOKIE有同名变量,用$_REQUEST调用变量时从哪里取呢?
其实$_REQUEST解析顺序由variables_order设置,php.ini中原文如下:
This directive describes the order in which PHP registers GET, POST, Cookie, Environment and Built-in variables (G, P, C, E & S respectively, often referred to as EGPCS or GPC). Registration is done from left to right, newer values override older values.
配置如下:
variables_order = “GPCS”
该设置配置PHP解析变量顺序,包括$_GET,$_POST,$_COOKIE,$_ENV ,$_SERVER 数组,解析顺序从左到右,后解析新值覆盖旧值。默认设定为EGPCS(Environment,GET,POST,Cookie,Server)。如果将其设为“GP”,会导致 PHP 完全忽略环境变量,cookies 和 server 变量,并用 POST 方法的变量覆盖 GET 方法的同名变量。
关于Observable 类
addObserver 将observer加入set,调用的时候是按照先进后出的顺序,即入栈出栈的顺序调用。
这个例子,示例了两点
共两个文件,test.c 和 main.cpp,代码解释如下:
在main.cpp (C++ 代码) 里定义了一个类MyMath,类里有个成员函数sum ;如何让C能调用这个c++的函数MyMath::sum呢?
即在main.cpp 中添加extern C后,声明定义一个C的函数call_MyMath_sum。在test.c 中先声明这个函数,然后通过调用call_MyMath_sum,达到调用C++ MyMath::sum的作用。
在test.c 中,定义了一个sum 的函数。如何让C++能调用这个c的函数sum呢? 这么做的,在main.cpp中 extend C 后声明它,然后在main函数中直接调用就可以了。
代码有点绕,C和C++调来调去的,不过仔细看就容易明白。
起关键作用的就是 extent C 这个关键语句,它的作用是告诉C++编译器,把后面的语句当作C语言进行处理。
C语言中的函数,其中调用了C++中的call_MyMath_sum:
test.c
int call_MyMath_sum (int, int); // 此函数定义在main.cpp中
int sum(int a , int b) {
return call_MyMath_sum(a,b);
}
C++语言中的函数:
main.cpp
# include <iostream>
using namespace std;
extern C {
int sum(int , int); // 声明sum函数,已经在test.c 中定义过
}
class MyMath {
public :
static int sum(int , int);
};
int MyMath::sum(int a, int b) {
return (a + b);
}
extern C int call_MyMath_sum (int a , int b) { // 定义call_MyMath_sum , 使其可以被c的代码调用
return (MyMath::sum(a,b));
}
int main(void) {
cout< <sum(5,6); return 0; // 此sum是 在test.c中定义的
}
如何编译:
# Makefile
main.o:
g++ -c -o main.o main.cpp # 注意是g++
test.o:
gcc -c -o test.o test.c # 注意是gcc
main: main.o test.o
g++ -o main main.o test.o # 最后链接用的是g++
all: main
clean:
rm -f test.o main.o
执行 make 得到可执行文件main
在编译于渊的《Orange’s 一个操作系统实现》中的源代码时, 经常提示:
klib.c:(.text+0xe5): undefined reference to __stack_chk_fail' make: *** [kernel.bin] Error 1
这是由于GCC中的编译器堆栈保护机制造成,编译时禁掉就可以。
解决方案:
在CFLAGS后面加上-fno-stack-protector
php 版本 5.2.4 现有一txt文件,格式如下: file.txt
要将其内容按行分割存入数据$array中 执行代码:
并未达到预想的效果 $array => array( 0 => String(15) “1 2 3 4 5” ) 这就是问题! 解决方法:
答案是肯定的,以下面代码为例:
其中,var为class A中私有变量,被protected型fun函数调用。当class A被class B继承,class B并不能继承属性var,但是class B调用fun函数时,属性var仍然是有效的。 浏览器中显示结果如下:
foreach ($PostList as &$postInfo) {
}
unset(&$postInfo); // 最好加上这条语句,不然下面的语句中用到这个变量,用的是数组最后一个元素
尽可能不写运算符堆砌的语句,可读性较差。
和其他语言浮点数比较相同
var_dump("true == 70", true == 70);
bool(true)
整形和布尔比较,PHP会先把整形转换成布尔
$str = “公司”; //默认以UTF-8编码
$str = Simple_Util_String::msubstr($str, 4); // 按字节截取前4个字节,原字符串有6个字节
$str .= “adfadsfasdfadsfasdf”; //拼接后面的字符串 (执行下一步后,这些字符就消失了)
$str = iconv("UTF-8", "GBK//TRANSLIT",$str); // 转码 , 遇到不认识的字符串进行转写
执行此步时,“公司”的“司”字编码不完整,iconv不认识,但iconv没有转写,而是做了截断。导致$str后面的字符串也没有转换成功,“adfadsfasdfadsfasdf”丢失了。在一些商业产品,尤其是有关统计的系统里,这是很危险的。 解决方法是: 改变iconv第二个参数。
$str = iconv("UTF-8","GBK//IGNORE", $str);
总结:iconv的TRANSLIT并不靠谱,遇到不认识也不能转写的字符串,也可能截断。保险的方法是用IGNORE。