可冲入函数与多线程安全函数

本文探讨了reentrant与thread-safe函数的区别,包括它们的概念、典型例子及如何确保函数在这两种场景下正确运行。强调了reentrant函数在信号处理和递归调用中的重要性。

1. reentrant函数

一个函数是reentrant的,如果它可以被安全地递归或并行调用。要想成为reentrant式的函数,该函数不能含有(或使用)静态(或全局)数据(来存储函数调用过程中的状态信息),也不能返回指向静态数据的指针,它只能使用由调用者提供的数据,当然也不能调用non-reentrant函数.

比较典型的non-reentrant函数有getpwnam, strtok, malloc等.

reentrant和non-reentrant函数的例子

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. #include <string.h>
  5. #include <sys/types.h>
  6. #include <unistd.h>
  7. #include <math.h>
  8. int* getPower(int i)
  9. {
  10.   static int result;
  11.   result = pow(2, i);
  12.   getchar();
  13.   return &result;
  14. }
  15. void getPower_r(int i, int* result)
  16. {
  17.   *result = pow(2, i);
  18. }
  19. void handler (int signal_number)        /*处理SIGALRM信号*/
  20. {
  21.   getPower(3);
  22. }
  23. int main ()
  24. {
  25.   int *result;
  26.   struct sigaction sa;
  27.   memset(&sa, 0, sizeof(sa));
  28.   sa.sa_handler = &handler;
  29.   sigaction(SIGALRM, &sa, NULL);
  30.   result = getPower(5);
  31.   printf("2^5 = %d/n", *result);
  32.   return 0;
  33. }
复制代码


试验方法:
1. 编译 gcc test.c -lpthread
在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id
2. 用如下方式运行a.out:
运行./a.out,在按回车前,在另外一个终端中运行kill -14 pid (这里的pid是运行上面的ps时看到的值)
然后,按回车继续运行a.out就会看到2^5 = 8 的错误结论


对于函数int* getPower(int i)

由于函数getPower会返回一个指向静态数据的指针,在第一次调用getPower的过程中,再次调用getPower,则两次返回的指针都指向同一块内存,第二次的结果将第一次的覆盖了(很多non-reentrant函数的这种用法会导致不确定的后果).所以是non-reentrant的.


对于函数void getPower_r(int i, int* result)

getPower_r会将所得的信息存储到result所指的内存中,它只是使用了由调用者提供的数据,所以是reentrant.在信号处理函数中可以正常的使用它.


2. thread-safe函数

Thread safety是多线程编程中的概念,thread safe函数是指那些能够被多个线程同时并发地正确执行的函数.

thread safe和non thread safe的例子

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <pthread.h>
  4. pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
  5. int count;        /*共享数据*/
  6. void* func (void* unused)
  7. {
  8.   if (count == 0)
  9.     count++;
  10. }
  11. void* func_s (void* unused)
  12. {
  13.   pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  14.   if (count == 0)
  15.     count++;
  16.   pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
  17. }
  18. int main ()
  19. {
  20.   pthread_t pid1, pid2;
  21.   pthread_create(&pid1, NULL, &func, NULL);
  22.   pthread_create(&pid2, NULL, &func, NULL);
  23.   pthread_join(pid1, NULL);
  24.   pthread_join(pid2, NULL);
  25.   return 0;
  26. }
复制代码


函数func是non thread safe的,这是因为它不能避免对共享数据count的race condition,
设想这种情况:一开始count是0,当线程1进入func函数,判断过count == 0后,线程2进入func函数
线程2判断count==0,并执行count++,然后线程1开始执行,此时count != 0 了,但是线程1仍然要执行
count++,这就产生了错误.

func_s通过mutex锁将对共享数据的访问锁定,从而避免了上述情况的发生.func_s是thread safe的

只要通过适当的"锁"机制,thread safe函数还是比较好实现的.

3. reentrant函数与thread safe函数的区别

reentrant函数与是不是多线程无关,如果是reentrant函数,那么要求即使是同一个进程(或线程)同时多次进入该函数时,该函数仍能够正确的运作.
该要求还蕴含着,如果是在多线程环境中,不同的两个线程同时进入该函数时,该函数也能够正确的运作.

thread safe函数是与多线程有关的,它只是要求不同的两个线程同时对该函数的调用在逻辑上是正确的.

从上面的说明可以看出,reentrant的要求比thread safe的要求更加严格.reentrant的函数必是thread safe的,而thread safe的函数
未必是reentrant的. 举例说明:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <pthread.h>
  4. #include <signal.h>
  5. #include <string.h>
  6. #include <sys/types.h>
  7. #include <unistd.h>
  8. pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
  9. int count;        /*共享数据*/
  10. void* func_s (void* unused)
  11. {
  12.   pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  13.   printf("locked by thead %d/n", pthread_self());
  14.   if (count == 0)
  15.     count++;
  16.   getchar();
  17.   pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
  18.   printf("lock released by thead %d/n", pthread_self());
  19. }
  20. void handler (int signal_number)        /*处理SIGALRM信号*/
  21. {
  22.   printf("handler running in %d/n", pthread_self());
  23.   func_s(NULL);
  24. }
  25. int main ()
  26. {
  27.   pthread_t pid1, pid2;
  28.   struct sigaction sa;
  29.   memset(&sa, 0, sizeof(sa));
  30.   sa.sa_handler = &handler;
  31.   sigaction(SIGALRM, &sa, NULL);
  32.   printf("main thread's pid is: %d/n", pthread_self());
  33.   func_s(NULL);
  34.   pthread_create(&pid1, NULL, &func_s, NULL);
  35.   pthread_create(&pid2, NULL, &func_s, NULL);
  36.   pthread_join(pid1, NULL);
  37.   pthread_join(pid2, NULL);
  38.   func_s(NULL);
  39.   return 0;
  40. }
复制代码


试验方法:
1. 编译 gcc test.c -lpthread
在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id
2. 进行下面4次运行a.out:
每次运行分别在第1,2,3,4次回车前,在另外一个终端中运行kill -14 pid (这里的pid是上面ps中看到的值)

试验结果:
1. 该进程中有3个线程:一个主线程,两个子线程
2. func_s是thread safe的
3. func_s不是reentrant的
4. 信号处理程序会中断主线程的执行,不会中断子线程的执行
5. 在第1,4次回车前,在另外一个终端中运行kill -14 pid会形成死锁,这是因为
主线程先锁住了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,
由于同一个线程锁定两次,所以形成死锁
6. 在第2,3次回车前,在另外一个终端中运行kill -14 pid不会形成死锁,这是因为一个子线程先锁住
了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,被挂起,这时,子线程
可以被继续执行.当该子线程释放掉锁以后,handler和另外一个子线程可以竞争进入临界区,然后继续执行.
所以不会形成死锁.

结论:
1. reentrant是对函数相当严格的要求,绝大部分函数都不是reentrant的(APUE上有一个reentrant函数
的列表).
什么时候我们需要reentrant函数呢?只有一个函数需要在同一个线程中需要进入两次以上,我们才需要
reentrant函数.这些情况主要是异步信号处理,递归函数等等.(non-reentrant的递归函数也不一定会
出错,出不出错取决于你怎么定义和使用该函数). 大部分时候,我们并不需要函数是reentrant的.

2. 在多线程环境当中,只要求多个线程可以同时调用一个函数时,该函数只要是thread safe的就可以了.
我们常见的大部分函数都是thread safe的,不确定的话请查阅相关文档.

3. reentrant和thread safe的本质的区别就在于,reentrant函数要求即使在同一个线程中任意地进入两次以上,
也能正确执行.

大家常用的malloc函数是一个典型的non-reentrant但是是thread safe函数,这就说明,我们可以方便的
在多个线程中同时调用malloc,但是,如果将malloc函数放入信号处理函数中去,这是一件很危险的事情.

4. reentrant函数肯定是thread safe函数,也就是说,non thread safe肯定是non-reentrant函数
不能简单的通过加锁,来使得non-reentrant函数变成 reentrant函数
<think>我们正在讨论Netty的ByteToMessageDecoder在多线程环境下读取响应数据的问题。根据引用[2]和[3],ByteToMessageDecoder是一个用于解码入站数据的抽象基类,它处理了TCP粘包和拆包的问题。它通过累积接收到的数据直到有足够的数据可以解码成一个完整的消息。 但是,ByteToMessageDecoder本身并不是线程安全的。在Netty中,每个Channel都有自己独立的ChannelPipeline,并且每个ChannelHandlerContext通常只由一个线程(即分配给该Channel的EventLoop线程)操作。因此,通常情况下,我们不需要担心多线程问题,因为Netty保证了每个Channel的处理都是单线程的(即同一个Channel的事件只会被同一个EventLoop线程处理)。 然而,如果用户想要在客户端使用多线程处理响应数据,需要注意以下几点: 1. 每个客户端连接(Channel)通常由一个EventLoop线程处理,所以同一个Channel的decode方法不会被多个线程同时调用。因此,在ByteToMessageDecoder中,我们不需要考虑多线程同步问题。 2. 但是,如果客户端有多个连接(多个Channel),那么每个连接可能由不同的EventLoop线程处理。这时,每个连接对应的ByteToMessageDecoder实例是独立的,所以也不存在线程安全问题。 3. 如果用户想要在解码后使用多线程处理消息(例如,将解码后的消息提交到线程池中进行处理),那么可以在ChannelPipeline中添加一个专门的Handler(例如,使用EventExecutorGroup)来执行耗时的操作。 因此,实现多线程读取响应数据的方法通常不是在解码器内部进行多线程处理,而是将解码后的消息传递给后续的Handler,并在后续Handler中使用线程池来处理。 具体步骤: 1. 自定义ByteToMessageDecoder:按照常规方式实现decode方法,将ByteBuf解码成完整的消息对象,并添加到out列表中。注意,decode方法是在EventLoop线程中调用的,所以解码操作应该尽量快,不要阻塞。 2. 在解码器之后添加一个处理消息的Handler,并指定一个EventExecutorGroup(即线程池)。这样,这个Handler的处理将会在指定的线程池中执行,而不是在EventLoop线程中。 示例代码: 自定义解码器: ```java public class MyDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // 实现具体的解码逻辑,将解码后的消息添加到out中 // 例如:解码一个整数 if (in.readableBytes() >= 4) { out.add(in.readInt()); } } } ``` 在ChannelPipeline中添加解码器和业务处理器,并为业务处理器指定线程池: ```java EventLoopGroup workerGroup = new NioEventLoopGroup(); EventExecutorGroup businessGroup = new DefaultEventExecutorGroup(10); // 10个线程的业务线程池 try { Bootstrap b = new Bootstrap(); b.group(workerGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyDecoder()); // 解码器,运行在EventLoop线程 // 使用businessGroup来执行后续的Handler pipeline.addLast(businessGroup, new MyBusinessHandler()); // 业务处理器,运行在业务线程池 } }); // ... 其他代码 } finally { workerGroup.shutdownGracefully(); businessGroup.shutdownGracefully(); } ``` 在MyBusinessHandler中,可以安全地执行耗时的操作,因为它运行在业务线程池中,不会阻塞EventLoop线程。 总结: - ByteToMessageDecoder本身是单线程的(每个Channel一个EventLoop线程),不需要考虑多线程问题。 - 多线程处理响应数据应该在解码之后,通过将消息传递给一个运行在独立线程池的Handler来实现。 相关问题: 1. Netty中如何避免解码器阻塞EventLoop线程? 2. 在Netty中,如何为不同的Handler指定不同的线程池? 3. ByteToMessageDecoder如何处理TCP粘包和拆包问题?</think>### Netty ByteToMessageDecoder 多线程读取响应数据的实现方法 在Netty中实现多线程读取响应数据,关键在于**避免阻塞EventLoop线程**,同时确保**线程安全的解码处理**。以下是具体实现方案: #### 1. 核心实现原理 - **ByteToMessageDecoder机制**:根据引用[2][3],`ByteToMessageDecoder`会缓冲入站数据直至完整消息到达,其`decode()`方法在EventLoop线程调用,需保证非阻塞。 - **多线程处理核心**:将解码后的消息**提交到独立线程池**处理,避免阻塞I/O线程。 #### 2. 实现步骤(代码示例) ```java // 自定义解码器 public class MultiThreadDecoder extends ByteToMessageDecoder { // 创建业务线程池(建议使用固定大小) private static final ExecutorService BUSINESS_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { if (in.readableBytes() < 4) return; // 示例:检查4字节包头 // 1. 解码操作(在EventLoop线程执行) int dataLength = in.readInt(); if (in.readableBytes() < dataLength) { in.resetReaderIndex(); // 重置读取位置 return; } byte[] data = new byte[dataLength]; in.readBytes(data); // 2. 提交到线程池异步处理 BUSINESS_EXECUTOR.submit(() -> { // 线程安全的业务处理 Object processedData = processData(data); // 3. 将结果写回ChannelHandlerContext ctx.executor().execute(() -> ctx.fireChannelRead(processedData) // 确保后续handler在I/O线程执行 ); }); } private Object processData(byte[] data) { // 实际业务处理(如数据库操作/复杂计算) return "Processed: " + new String(data, StandardCharsets.UTF_8); } } ``` #### 3. 关键注意事项 1. **线程安全控制**: - 使用`ctx.executor().execute()`将结果回调到原EventLoop线程(引用[2]) - 避免在业务线程直接操作`ChannelHandlerContext`或`ByteBuf` 2. **资源管理**: - 使用`@Sharable`注解需确保解码器无状态(引用[2]) - 通过`ReferenceCountUtil.release()`手动释放ByteBuf(若未自动释放) 3. **流量控制**: - 添加线程池拒绝策略(如`new ThreadPoolExecutor.CallerRunsPolicy()`) - 监控线程池队列积压情况 #### 4. 性能优化建议 - **批量处理**:在`decode()`中累积多个消息后批量提交 - **对象复用**:使用`Recycler`复用中间对象(如ByteBuf) - **动态扩缩容**:根据负载动态调整线程池大小 > **重要提醒**:根据引用[3],TCP粘包/拆包问题已由`ByteToMessageDecoder`的缓冲机制自动处理,开发者只需关注完整消息的解码逻辑。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值