Résumé de la programmation multithread C

黄舟
Libérer: 2017-02-06 14:02:11
original
1451 Les gens l'ont consulté

Lors du développement de programmes C, les exigences sont généralement plus élevées en termes de débit, de concurrence et de performances en temps réel. Lors de la conception d'un programme C, en résumé, l'efficacité peut être améliorée à partir des points suivants :

  • Concurrence

  • Asynchrone

  • Mise en cache

Voici quelques exemples de problèmes que je rencontre dans mon travail quotidien. Les idées de conception ne sont rien de plus que les trois points ci-dessus.

1 File d'attente des tâches

1.1 Concevoir la file d'attente des tâches basée sur le modèle producteur-consommateur

Le modèle producteur-consommateur est un modèle que les gens connaissent très bien, comme sur un serveur Dans le programme, lorsque les données utilisateur sont modifiées par le module logique, une tâche de mise à jour de la base de données (produire) est générée et livrée à la file d'attente des tâches du module IO. Le module IO retire la tâche de la file d'attente des tâches et. effectue l'opération SQL (consommer).

Concevez une file d'attente de tâches générale, l'exemple de code est le suivant :

Pour une mise en œuvre détaillée, veuillez consulter :

http://ffown.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;
}
Copier après la connexion


1.2 Compétences d'utilisation de la file d'attente des tâches

1.2.1 Séparation des E/S et de la logique

Par exemple, réseau Dans le programme serveur de jeu, le module réseau reçoit le paquet de messages, revient immédiatement après l'avoir livré à la couche logique et continue d'accepter le paquet de messages suivant. Les threads logiques s'exécutent dans un environnement sans opérations d'E/S pour garantir des performances en temps réel. Exemple :

void handle_xx_msg(long uid, const xx_msg_t& msg){
logic_task_queue->post(boost::bind(&servie_t::proces, uid, msg));
}
Copier après la connexion


Notez que ce mode est une file d'attente de tâches unique et que chaque file d'attente de tâches est à thread unique.

1.2.2 Pipeline parallèle

Ce qui précède complète uniquement la parallélisation des opérations io et CPU, tandis que les opérations logiques dans le CPU sont en série. Dans certains cas, la partie opération logique du CPU peut également être parallélisée. Par exemple, dans le jeu, les deux opérations de l'utilisateur A et B peuvent être complètement parallélisées car les deux opérations ne partagent pas de données. Le moyen le plus simple consiste à attribuer les opérations liées à A et B à différentes files d’attente de tâches. Un exemple est le suivant :

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));
}
Copier après la connexion


Notez que ce mode est une file d'attente multitâche et que chaque file d'attente de tâches est monothread.


1.2.3 Pool de connexions et rappel asynchrone

Par exemple, le module de service logique nécessite que le module de base de données charge de manière asynchrone les données utilisateur et effectue les traitements ultérieurs et calculs. Le module de base de données dispose d'un pool de connexions avec un nombre fixe de connexions. Lorsque la tâche d'exécution de SQL arrive, il sélectionne une connexion inactive, exécute SQL et transmet le SQL à la couche logique via la fonction de rappel. Les étapes sont les suivantes :

Pré-allouer le pool de threads, et chaque thread crée une connexion à la base de données

Créez une file d'attente de tâches pour le module de base de données, et tous les threads en sont des consommateurs file d'attente des tâches

La couche logique délivre la tâche d'exécution SQL au module de base de données et transmet également une fonction de rappel pour recevoir le résultat de l'exécution SQL

L'exemple est le suivant :

void db_t:load(long uid_, boost::functionpost(boost::bind(&db_t:load, uid, func));
Copier après la connexion

Notez que ce mode In, il s'agit d'une file d'attente de tâches unique et que chaque file d'attente de tâches a plusieurs threads.


2. Log

Cet article parle principalement de la programmation multithread C Le système de journalisation n'a pas pour but d'améliorer l'efficacité du programme, mais il. est utilisé lors du débogage et de l'exécution du programme. En termes de dépannage, les journaux sont un outil irremplaçable. Je pense que les amis qui développent des programmes en arrière-plan utiliseront les journaux. Les manières courantes d'utiliser les journaux sont les suivantes :


Streaming, tel que logstream << « start service time[%d] » << << ” nom de l'application [%s]” << app_string.c_str() << endl;

Le format d'impression est : logtrace(LOG_MODULE, "heure de démarrage de l'application [%d] name[%s]", time(0), app_string.c_str());


Les deux ont leurs propres avantages et inconvénients, le streaming est thread-safe Oui, le formatage des chaînes au format printf sera plus direct, mais l'inconvénient est qu'il est dangereux pour les threads. Si vous remplacez app_string.c_str() par app_string (std::string), la compilation réussira, mais elle plantera pendant. runtime (si vous avez de la chance à chaque fois, il plante tout le temps, mais si vous n'avez pas de chance, il plante de temps en temps). Personnellement, j'aime le style printf et je peux apporter les améliorations suivantes :


Augmentez la sécurité des threads et utilisez le mécanisme de traits des modèles C pour assurer la sécurité des threads. Exemple :

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

De cette façon, si d'autres types sont transmis en plus du type standard std::string, la compilation échouera. Ceci n'est qu'un exemple d'un paramètre, cette version peut être surchargée pour prendre en charge plus de paramètres, 9 paramètres ou plus si vous le souhaitez.

Ajouter de la couleur au journal et ajouter des caractères de contrôle à printf. La couleur peut être affichée sur le terminal à l'écran Exemple sous Linux : printf("33[32;49;1m [DONE] 33[39;49). ;0m ")

Pour plus de combinaisons de couleurs, voir :

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

Quand chaque le thread démarre, vous devez utiliser les journaux pour imprimer les fonctions dont le thread est responsable. De cette façon, lorsque le programme est en cours d'exécution, vous pouvez savoir combien de CPU est utilisée par cette fonction via top-H-p pid. En fait, chaque ligne de mon journal imprimera l'ID du thread. Cet ID de thread n'est pas pthread_id, mais est en fait le numéro d'ID de processus attribué par le système correspondant au thread.


3. Surveillance des performances

Bien qu'il existe de nombreux outils capables d'analyser les performances d'exécution des programmes C, la plupart d'entre eux s'exécutent encore au stade du débogage du programme. . Nous avons besoin d'un moyen de surveiller le programme à la fois dans les phases de débogage et de publication. D'une part, nous pouvons savoir où se trouvent les goulots d'étranglement du programme, et d'autre part, nous pouvons découvrir le plus tôt possible quels composants sont anormaux. pendant l'exécution.


通常都是使用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__)
Copier après la connexion

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);
}
}
Copier après la connexion


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

void user_mgr_t:dump(){
struct lambda {
static void print(user_t& user){
//! print(tostring(user);
}
};
this->foreach(lambda::print);
}
Copier après la connexion

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

this->foreach([](user_t& user) {} );
Copier après la connexion

但是我大部分时间编写的程序都要运行在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()
}
Copier après la connexion


这样做的缺点是,一个接口要响应的写两遍函数,如果一个函数的参数变了,那么另一个参数也要跟着改动。并且代码也不是很美观。使用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));
}
Copier après la connexion

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



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];
};
Copier après la connexion



定义执行任务的 worker

void worker_t:exe(int index_, shared_ptr ret) {
ret->set_result(index, 100);
}
Copier après la connexion


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

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

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


Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal