Bei der Entwicklung von C++-Programmen gelten im Allgemeinen höhere Anforderungen hinsichtlich Durchsatz, Parallelität und Echtzeitleistung. Zusammenfassend kann beim Entwerfen eines C++-Programms die Effizienz durch die folgenden Punkte verbessert werden:
Parallelität
Asynchron
Caching
Hier sind einige Beispiele für einige Probleme, auf die ich in meiner täglichen Arbeit stoße. Die Designideen sind nichts anderes als die oben genannten drei Punkte.
1 Aufgabenwarteschlange
1.1 Entwerfen Sie die Aufgabenwarteschlange basierend auf dem Produzenten-Konsumenten-Modell
Das Produzenten-Konsumenten-Modell ist ein Modell, mit dem die Leute sehr vertraut sind, wie z Auf einem Server wird im Programm, wenn die Benutzerdaten vom Logikmodul geändert werden, eine Aufgabe zum Aktualisieren der Datenbank (Erzeugen) generiert und an die Aufgabenwarteschlange des E/A-Moduls gesendet führt die SQL-Operation (consume) aus.
Entwerfen Sie eine allgemeine Aufgabenwarteschlange. Der Beispielcode lautet wie folgt:
Eine detaillierte Implementierung finden Sie unter:
http://fdown.googlecode.com/svn/ trunk/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 Fähigkeiten zur Verwendung von Aufgabenwarteschlangen
1.2.1 Trennung von E/A und Logik
Beispiel: Netzwerk Im Spielserverprogramm empfängt das Netzwerkmodul das Nachrichtenpaket, kehrt sofort nach der Übermittlung an die Logikschicht zurück und akzeptiert weiterhin das nächste Nachrichtenpaket. Logische Threads werden in einer Umgebung ohne E/A-Vorgänge ausgeführt, um Echtzeitleistung sicherzustellen. Beispiel:
void handle_xx_msg(long uid, const xx_msg_t& msg){ logic_task_queue->post(boost::bind(&servie_t::proces, uid, msg)); }
Beachten Sie, dass es sich bei diesem Modus um eine einzelne Aufgabenwarteschlange handelt und jede Aufgabenwarteschlange einen einzelnen Thread hat.
1.2.2 Parallele Pipeline
Das Obige vervollständigt nur die Parallelisierung von E/A- und CPU-Operationen, während die logischen Operationen in der CPU seriell sind. In einigen Fällen kann der CPU-Logikoperationsteil auch parallelisiert werden. Im Spiel können beispielsweise die beiden Operationen von Benutzer A und B vollständig parallelisiert werden, da die beiden Operationen keine Daten gemeinsam nutzen. Der einfachste Weg besteht darin, Vorgänge im Zusammenhang mit A und B verschiedenen Aufgabenwarteschlangen zuzuweisen. Ein Beispiel lautet wie folgt:
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)); }
Beachten Sie, dass es sich bei diesem Modus um eine Multitask-Warteschlange handelt und jede Task-Warteschlange Single-Threaded ist.
1.2.3 Verbindungspool und asynchroner Rückruf
Zum Beispiel erfordert das logische Servicemodul, dass das Datenbankmodul Benutzerdaten asynchron lädt und die anschließende Verarbeitung durchführt Berechnungen. Das Datenbankmodul verfügt über einen Verbindungspool mit einer festen Anzahl von Verbindungen. Wenn die Aufgabe zur Ausführung von SQL eintrifft, wählt es eine inaktive Verbindung aus, führt SQL aus und übergibt die SQL über die Rückruffunktion an die Logikschicht. Die Schritte sind wie folgt:
Vorab den Thread-Pool zuweisen, und jeder Thread stellt eine Verbindung zur Datenbank her.
Eine Aufgabenwarteschlange für das Datenbankmodul erstellen, und alle Threads sind Verbraucher davon Aufgabenwarteschlange
Die Logikschicht übermittelt die SQL-Ausführungsaufgabe an das Datenbankmodul und übergibt außerdem eine Rückruffunktion, um das SQL-Ausführungsergebnis zu empfangen
Das Beispiel lautet wie folgt:
void db_t:load(long uid_, boost::functionpost(boost::bind(&db_t:load, uid, func));
Beachten Sie, dass es sich bei diesem In-Modus um eine einzelne Aufgabenwarteschlange handelt und jede Aufgabenwarteschlange über mehrere Threads verfügt.
2. Protokoll
Dieser Artikel befasst sich hauptsächlich mit der C++-Multithread-Programmierung, aber nicht mit der Verbesserung der Programmeffizienz wird beim Debuggen und Ausführen von Programmen verwendet. Ich glaube, dass Protokolle ein unersetzliches Werkzeug sind. Übliche Möglichkeiten, Protokolle zu verwenden, sind wie folgt:
Streaming, z. B. Logstream << „Dienstzeit starten[%d]“ << << ” app_string.c_str() << endl;
Printf-Format ist: logtrace(LOG_MODULE, „start service time[%d] app name[%s]", time(0), app_string.c_str());
Beide haben ihre eigenen Vor- und Nachteile, Streaming ist Thread-sicher Ja, das Formatieren von Zeichenfolgen im printf-Format ist direkter, aber der Nachteil besteht darin, dass es threadunsicher ist. Wenn Sie app_string.c_str() durch app_string (std::string) ersetzen, wird die Kompilierung erfolgreich sein, es kommt jedoch zu einem Absturz Laufzeit (wenn Sie Glück haben, stürzt es jedes Mal ständig ab, aber wenn Sie Pech haben, stürzt es gelegentlich ab). Ich persönlich liebe den printf-Stil und kann die folgenden Verbesserungen vornehmen:
Erhöhen Sie die Thread-Sicherheit und verwenden Sie den Merkmalsmechanismus von C++-Vorlagen, um Thread-Sicherheit zu erreichen. Beispiel:
template void logtrace(const char* module, const char* fmt, ARG1 arg1){ boost::format s(fmt); f % arg1; }
Wenn auf diese Weise neben dem Standardtyp + std::string noch andere Typen übergeben werden, wird die Kompilierung nicht bestanden. Dies ist nur ein Beispiel für einen Parameter. Diese Version kann überladen werden, um mehr Parameter, 9 Parameter oder mehr, wenn Sie möchten, zu unterstützen.
Farbe zum Protokoll hinzufügen und Steuerzeichen zu printf hinzufügen. Die Farbe kann auf dem Bildschirmterminal angezeigt werden. Beispiel unter Linux: printf(“33[32;49;1m [FERTIG] 33[39;49 ;0m ")
Weitere Farbschemata finden Sie unter:
http://hi.baidu.com/jiemnij/blog/item/d95df8c28ac2815cb219a80e.html
Wenn jeweils Wenn ein Thread gestartet wird, sollten Sie Protokolle verwenden, um auszudrucken, für welche Funktionen der Thread verantwortlich ist. Auf diese Weise können Sie beim Ausführen des Programms über die Top-H-P-PID ermitteln, wie viel CPU von dieser Funktion verwendet wird. Tatsächlich wird in jeder Zeile meines Protokolls die Thread-ID gedruckt. Diese Thread-ID ist nicht pthread_id, sondern tatsächlich die vom System zugewiesene Prozess-ID-Nummer, die dem Thread entspricht.
3. Leistungsüberwachung
Obwohl es viele Tools gibt, die die laufende Leistung von C++-Programmen analysieren können, werden die meisten davon noch in der Programm-Debugging-Phase ausgeführt . Wir benötigen eine Möglichkeit, das Programm sowohl in der Debug- als auch in der Release-Phase zu überwachen. Einerseits können wir erkennen, wo die Engpässe des Programms liegen, und andererseits können wir so früh wie möglich herausfinden, welche Komponenten abnormal sind zur Laufzeit.
通常都是使用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)!