>  기사  >  백엔드 개발  >  C++ 다중 스레드 프로그래밍 요약

C++ 다중 스레드 프로그래밍 요약

黄舟
黄舟원래의
2017-02-06 14:02:111400검색

C++ 프로그램을 개발할 때 일반적으로 처리량, 동시성, 실시간 성능 측면에서 더 높은 요구 사항이 있습니다. C++ 프로그램을 설계할 때 정리하면 다음과 같은 점에서 효율성을 높일 수 있다.

  • 동시성

  • 비동기성

  • 캐싱

일상 업무에서 직면하는 몇 가지 문제의 예는 다음과 같습니다. 디자인 아이디어는 위의 세 가지에 지나지 않습니다.

1 작업 대기열

1.1 생산자-소비자 모델을 기반으로 작업 대기열 설계

생산자-소비자 모델은 다음과 같이 사람들에게 매우 친숙한 모델입니다. 서버에서 프로그램에서 사용자 데이터가 로직 모듈에 의해 수정되면 데이터베이스(생산)를 업데이트하는 작업이 생성되어 IO 모듈 작업 대기열로 전달됩니다. IO 모듈은 작업 대기열에서 작업을 꺼내고 SQL 작업(사용)을 수행합니다.

일반 작업 대기열을 설계합니다. 샘플 코드는 다음과 같습니다.

자세한 구현은 다음을 참조하세요.

http://ffown.googlecode.com/svn/ 트렁크/fflib/include /detail/task_queue_impl.h

void task_queue_t::produce(const task_t& task_) {
lock_guard_t lock(m_mutex);
if (m_tasklist->empty()){
//! 条件满足唤醒等待线程
m_cond.signal();
}
m_tasklist->push_back(task_);
}
int task_queue_t::comsume(task_t& task_){
lock_guard_t lock(m_mutex);
while (m_tasklist->empty())
//! 当没有作业时,就等待直到条件满足被唤醒{
if (false == m_flag){
return -1;
}
m_cond.wait();
}
task_ = m_tasklist->front();
m_tasklist->pop_front();
return 0;
}


1.2 작업 대기열 사용 팁

1.2.1 IO와 로직의 분리

예를 들어 온라인 게임 서버 프로그램에서 네트워크 모듈은 메시지 패킷을 수신하고 이를 논리 계층에 전달한 후 즉시 반환하여 다음 메시지 패킷을 계속 수락합니다. 논리적 스레드는 실시간 성능을 보장하기 위해 IO 작업 없이 환경에서 실행됩니다. 예:

void handle_xx_msg(long uid, const xx_msg_t& msg){
logic_task_queue->post(boost::bind(&servie_t::proces, uid, msg));
}


이 모드는 단일 작업 대기열이고 각 작업 대기열은 단일 스레드입니다.

1.2.2 병렬 파이프라인

위에서는 io와 CPU 작업의 병렬화만 완료하고 CPU의 논리 작업은 직렬입니다. 어떤 경우에는 CPU 논리 연산 부분도 병렬화될 수 있습니다. 예를 들어 게임에서 사용자 A와 B의 두 작업은 데이터를 공유하지 않기 때문에 완전히 병렬화될 수 있습니다. 가장 간단한 방법은 A와 B에 관련된 작업을 서로 다른 작업 대기열에 할당하는 것입니다. 예는 다음과 같습니다.

void handle_xx_msg(long uid, const xx_msg_t& msg) {
logic_task_queue_array[uid % sizeof(logic_task_queue_array)]->post(
boost::bind(&servie_t::proces, uid, msg));
}


이 모드는 다중 작업 대기열이고 각 작업 대기열은 단일 스레드입니다.


1.2.3 연결 풀 및 비동기 콜백

예를 들어 논리적 서비스 모듈에서는 사용자 데이터를 비동기적으로 로드하고 후속 처리를 수행하는 데이터베이스 모듈이 필요합니다. 계산. 데이터베이스 모듈에는 고정된 수의 연결이 있는 연결 풀이 있습니다. SQL을 실행하는 작업이 도착하면 유휴 연결을 선택하고 SQL을 실행한 후 콜백 함수를 통해 SQL을 논리 계층으로 전달합니다. 단계는 다음과 같습니다.

스레드 풀을 사전 할당하고 각 스레드는 데이터베이스에 대한 연결을 생성합니다.

데이터베이스 모듈에 대한 작업 대기열을 생성하며 모든 스레드는 이 대기열의 소비자입니다. task queue

로직 레이어는 sql 실행 작업을 데이터베이스 모듈에 전달하고, sql 실행 결과를 받기 위해 콜백 함수도 전달합니다

예제는 다음과 같습니다.

void db_t:load(long uid_, boost::functionpost(boost::bind(&db_t:load, uid, func));

이 모드에서는 단일 작업 대기열이며 각 작업 대기열에는 여러 스레드가 있습니다.


2. 로그

이 글에서는 주로 C++ 멀티스레드 프로그래밍에 대해 이야기하고 있지만, 로깅 시스템은 프로그램 효율성을 높이기 위한 것이 아닙니다. 로그는 프로그램 디버깅 및 실행 중에 사용됩니다. 백그라운드 프로그램을 개발하는 친구들은 로그를 사용할 것이라고 믿습니다. 로그를 사용하는 일반적인 방법은 다음과 같습니다.


logstream << “start service time[%d]” << << ” 앱 이름[%s]” << app_string.c_str() << endl;

Printf 형식은 logtrace(LOG_MODULE, “시작 시간[%d] 앱입니다. name[%s]", time(0), app_string.c_str());


둘 다 고유한 장점과 단점이 있으며 스트리밍은 스레드로부터 안전합니다. 예, printf 형식의 문자열 형식 지정은 더 직접적이지만 스레드가 안전하지 않다는 단점이 있습니다. app_string.c_str()을 app_string(std::string)으로 바꾸면 컴파일이 통과되지만 도중에 충돌이 발생합니다. 런타임(운이 좋으면 항상 충돌이 발생하지만, 운이 좋지 않으면 가끔 충돌이 발생합니다). 저는 개인적으로 printf 스타일을 좋아하며 다음과 같이 개선할 수 있습니다.


스레드 안전성을 높이고 C++ 템플릿의 특성 메커니즘을 사용하여 스레드 안전성을 달성합니다. 예:

template
void logtrace(const char* module, const char* fmt, ARG1 arg1){
boost::format s(fmt);
f % arg1;
}

이런 방식으로 표준 유형 + std::string 외에 다른 유형이 전달되면 컴파일이 통과되지 않습니다. 이는 하나의 매개변수에 대한 예일 뿐이며, 이 버전은 원하는 경우 더 많은 매개변수, 9개 이상의 매개변수를 지원하도록 오버로드될 수 있습니다.

로그에 색상을 추가하고 printf에 제어 문자를 추가합니다. Linux의 예: printf(“33[32;49;1m [DONE] 33[39;49) ;0m ")

더 많은 색상 구성표는

http://hi.baidu.com/jiemnij/blog/item/d95df8c28ac2815cb219a80e.html

을 참조하세요. 스레드가 시작되면 로그를 사용하여 스레드가 담당하는 기능을 인쇄해야 합니다. 이런 식으로 프로그램이 실행 중일 때 top-H-p pid를 통해 해당 함수가 얼마나 많은 CPU를 사용하는지 알 수 있습니다. 실제로 내 로그의 각 줄은 스레드 ID를 인쇄합니다. 이 스레드 ID는 pthread_id가 아니지만 실제로는 해당 스레드에 해당하는 시스템에서 할당한 프로세스 ID 번호입니다.


3. 성능 모니터링

C++ 프로그램의 실행 성능을 분석할 수 있는 도구는 많지만 대부분은 여전히 ​​프로그램 디버깅 단계에서 실행됩니다. . 디버그 단계와 릴리스 단계 모두에서 프로그램을 모니터링하는 방법이 필요합니다. 한편으로는 프로그램의 병목 현상이 어디에 있는지 알 수 있고, 다른 한편으로는 어떤 구성 요소가 비정상적인지 가능한 빨리 알아낼 수 있습니다. 런타임 중.


通常都是使用gettimeofday 来计算某个函数开销,可以精确到微妙。可以利用C++的确定性析构,非常方便的实现获取函数开销的小工具,示例如下:

struct profiler{
profiler(const char* func_name){
gettimeofday(&tv, NULL);
}
~profiler(){
struct timeval tv2;
gettimeofday(&tv2, NULL);
long cost = (tv.tv_sec - tv.tv_sec) * 1000000 + (tv.tv_usec - tv.tv_usec);
//! post to some manager
}
struct timeval tv;
};
#define PROFILER() profiler(__FUNCTION__)

Cost 应该被投递到性能统计管理器中,该管理器定时讲性能统计数据输出到文件中。

4 Lambda 编程

使用foreach 代替迭代器

很多编程语言已经内建了foreach,但是c++还没有。所以建议自己在需要遍历容器的地方编写foreach函数。习惯函数式编程的人应该会非常钟情使用foreach,使用foreach的好处多多少少有些,如:

http://www.cnblogs.com/chsword/archive/2007/09/28/910011.html

但主要是编程哲学上层面的。

示例:

void user_mgr_t::foreach(boost::function func_){
for (iterator it = m_users.begin(); it != m_users.end() ++it){
func_(it->second);
}
}


比如要实现dump 接口,不需要重写关于迭代器的代码

void user_mgr_t:dump(){
struct lambda {
static void print(user_t& user){
//! print(tostring(user);
}
};
this->foreach(lambda::print);
}

实际上,上面的代码变通的生成了匿名函数,如果是c++ 11 标准的编译器,本可以写的更简洁一些:

this->foreach([](user_t& user) {} );

但是我大部分时间编写的程序都要运行在centos 上,你知道吗它的gcc版本是gcc 4.1.2, 所以大部分时间我都是用变通的方式使用lambda函数。


Lambda 函数结合任务队列实现异步


常见的使用任务队列实现异步的代码如下:

void service_t:async_update_user(long uid){
task_queue->post(boost::bind(&service_t:sync_update_user_impl, this, uid));
}
void service_t:sync_update_user_impl(long uid){
user_t& user = get_user(uid);
user.update()
}


这样做的缺点是,一个接口要响应的写两遍函数,如果一个函数的参数变了,那么另一个参数也要跟着改动。并且代码也不是很美观。使用lambda可以让异步看起来更直观,仿佛就是在接口函数中立刻完成一样。示例代码:

void service_t:async_update_user(long uid){
struct lambda {
static void update_user_impl(service_t* servie, long uid){
user_t& user = servie->get_user(uid);
user.update();
}
};
task_queue->post(boost::bind(&lambda:update_user_impl, this, uid));
}

这样当要改动该接口时,直接在该接口内修改代码,非常直观。



5. 奇技淫巧


利用 shared_ptr 实现 map/reduce


Map/reduce的语义是先将任务划分为多个任务,投递到多个worker中并发执行,其产生的结果经reduce汇总后生成最终的结果。Shared_ptr的语义是什么呢?当最后一个shared_ptr析构时,将会调用托管对象的析构函数。语义和map/reduce过程非常相近。我们只需自己实现讲请求划分多个任务即可。示例过程如下:


定义请求托管对象,加入我们需要在10个文件中搜索“oh nice”字符串出现的次数,定义托管结构体如下:

struct reducer{
void set_result(int index, long result) {
m_result[index] = result;
}
~reducer(){
long total = 0;
for (int i = 0; i < sizeof(m_result); ++i){
total += m_result[i];
}
//! post total to somewhere
}
long m_result[10];
};



定义执行任务的 worker

void worker_t:exe(int index_, shared_ptr ret) {
ret->set_result(index, 100);
}


将任务分割后,投递给不同的worker

shared_ptr ret(new reducer());
for (int i = 0; i < 10; ++i) { task_queue[i]->post(boost::bind(&worker_t:exe, i, ret));
}

以上就是C++ 多线程编程总结的内容,更多相关内容请关注PHP中文网(www.php.cn)!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.