一、使用ØMQ编写多线程程序
- ØMQ也许是有史以来编写多线程(MT)应用程序最好的方法。为了编写完美的MT程序,除跨ØMQ套接字发送的消息外,ØMQ一般不推荐使用互斥量、锁或任何其他形式的线程间的通信
- 这里所说的“完美”,是指代码很容易编写和理解,适用任何编程语言和任何操作系统相同的设计方法,而且可以在零等待状态按任意数量的CPU扩展,而且没有收益递减点的程序
- 一个拥有世界一流的代码拍错经验的大公司发不了“多线程代码中可能出现的11大问题”,下面列出了7个:被遗忘的同步、不正确的粒度、读写冲突、无锁重新排序、锁护送效应、两步过程、优先级倒置
- 根据上面的问题,ØMQ编写多线程遵循的原则是“不要共享资源与状态”
ØMQ编写多线程代码的规则
- 不要在多个线程中访问相同的数据。使用传统的MT技术,如互斥,在ØMQ应用程序中是一种反模式。这里唯一的例外是ØMQ上下文对象,它是线程安全的
- 你必须为你的进程创建一个ØMQ上下文,并将它传递给你想要通过inproc套接字来连接的所有线程
- 你可以把线程作为拥有自己的上下文的独立任务,但这些线程不能通过inproc通信。不过,以后他们会更容易分解成独立的进程
- 不要在线程之间共享ØMQ套接字。ØMQ套接字不是线程安全的。从技术上将,做到这一点是可能的,但它要求信号量、锁或互斥,浙江使得你的应用程序缓慢且脆弱。线程之间共享套接字唯一完全合乎情理的地方是“语言绑定”,它需要在套接字上做类似垃圾收集的神奇操作
- 例如:如果你需要在应用程序中启动多个代理,你会想让每个代理在自己的线程中运行。在一个线程中创建代理前端和后端套接字,然后将套接字传递给在另一个线程的代理,这是很容易出错的。记住:不要在创建套接字的线程外使用或关闭套接字
二、编程演示案例
- 在“请求-响应”模式的文章中,我们使用了“ROUTER-DEALER”套接字创建代理,来处理多个客户端与服务端进行交流,在那篇文章中我们有三个程序,如下图所示:
- 客户端(rrclient.c):向代理发送Hello,代理将消息发送给服务端(工人),服务器(工人)给客户端回送一个“World”
- 代理(rrbroker.c):接收客户端的消息,将消息传递给服务器(工人)
- 服务器(工人)(rrworker.c):从代理那里接收到客户端的消息“Hello”,然后给客户端回送一条消息“World”
- 现在我们将上面的使用多线程改写上面的“Hello-World”请求响应服务器,结构如下:
- 客户端(rrclient.c):保持不变
- 多线程服务器(mtserver.c):现在我们改写代理的程序rrbroker.c,将服务器程序rrworker.c的功能与rrbroker.c结合在一起,西鞥成这个多线程服务器程序mtserver.c。其中代理还是运行在主线程中,调用zmq_proxy()运行,服务器运行在pthread_create()创建的线程中
- 注意,因为代理和服务器(工人)在线程之间进行通信,因此使用到了了inproc传输协议,关于该协议请参阅:
- 代码如下:
// mtserver.c
// 源码链接: https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/mtserver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>
#include <zmq.h>
// 多线程运行函数, 参数为进程的上下文指针
static void *worker_routine(void *arg);
// 从socket套接字上接收消息
static char *s_recv(void *socket);
// 向socket套接字发送消息string
static int s_send(void *socket, char *string);
int main()
{
int rc;
// 1.创建新的上下文
void *context = zmq_ctx_new();
assert(context != NULL);
// 2.创建、绑定代理的前端套接字, 客户端会来连接这个套接字
void *clients = zmq_socket(context, ZMQ_ROUTER);
assert(clients != NULL);
rc = zmq_bind(clients, "tcp://*:5559");
assert(rc != -1);
// 3.创建、绑定代理后端套接字, 在pthread_create()线程中运行的工人会来连接这个套接字
void *workers = zmq_socket(context, ZMQ_DEALER);
assert(workers != NULL);
rc = zmq_bind(workers, "inproc://workers");
assert(rc != -1);
// 4.创建4个服务器(工人), 服务器(工人)连接代理后端, 从ZMQ_DEALER端接收客户端的请求, 然后将响应返回
int thread_nbr;
for(thread_nbr = 0; thread_nbr < 4; ++thread_nbr)
{
pthread_t worker_pid;
pthread_create(&worker_pid, NULL, worker_routine, context);
}
// 5.启动代理, 前端处理客户端, 后端处理服务器(工人)
zmq_proxy(clients, workers, NULL);
// 6.关闭套接字、销毁上下文
zmq_close(clients);
zmq_close(workers);
zmq_ctx_destroy(context);
return 0;
}
static void *worker_routine(void *arg)
{
// 参数: 进程的上下文指针
int rc;
// 1.创建套接字、连接代理的ZMQ_DEALER端
void *receiver = zmq_socket(arg, ZMQ_REP);
assert(receiver != NULL);
rc = zmq_connect(receiver, "inproc://workers");
// 2.循环工作
while(1)
{
// 3.从代理的ZMQ_DEALER端读取客户端的请求
char *request = s_recv(receiver);
printf("Received request: %s\n", request);
free(request);
// 4.休眠1秒再返回响应
sleep(1);
// 5.将响应发送给代理, 代理会将响应返回给客户端
rc = s_send(receiver, "World");
assert(rc != -1);
}
// 6.关闭套接字
zmq_close(receiver);
return NULL;
}
static char *s_recv(void *socket)
{
int size;
zmq_msg_t msg;
zmq_msg_init(&msg);
size = zmq_msg_recv(&msg, socket, 0);
if(size == -1)
return NULL;
char *string = (char*)malloc(size + 1);
if(string == NULL)
return NULL;
memcpy(string, zmq_msg_data(&msg), size);
zmq_msg_close(&msg);
string[size] = 0;
return string;
}
static int s_send(void *socket, char *string)
{
int rc;
zmq_msg_t msg;
zmq_msg_init_size(&msg, strlen(string));
memcpy(zmq_msg_data(&msg), string, strlen(string));
rc = zmq_msg_send(&msg, socket, 0);
zmq_msg_close(&msg);
return rc;
}
- 代码结构如下:
- 创建ZMQ_ROUTER套接字,客户端会来连接这个套接字
- 创建ZMQ_DEALER套接字,服务器(工人)会来连接这个套接字
- 程序使用pthread_create()创建一系列线程,线程中运行的是REP套接字,也就是服务器(工人),其连接上面创建的ZMQ_DEALER套接字,处理客户端的“Hello”消息,然后向客户端回送“World”
- 主线程中调用zmq_proxy()启动代理,代理前端为ZMQ_ROUTER套接字处理的客户端,代理后端为pthread_create()创建线程中的服务器(工人)
- 编译运行效果如下:
gcc -o mtserver mtserver.c -lzmq -lpthread
三、独家对模式:ZMQ_PAIR
- 此模式用于跨inproc传输的线程间通信
- 我是小董,V公众"笔记白嫖"解锁更多【ZeroMQ】资料内容。