Multi-thread下对int进行原子操作

原子操作与自旋锁
本文探讨了在多线程环境下对整型变量进行保护的方法,对比了互斥锁、信号量与自旋锁的使用场景,并介绍了如何利用处理器特性实现原子变量。此外,还提供了一个使用GCC内置原子操作函数的示例程序。

原文链接

这是一系列文章,作者做了详细的讨论。

http://www.alexonlinux.com/do-you-need-mutex-to-protect-int
http://www.alexonlinux.com/pthread-spinlocks
http://www.alexonlinux.com/multithreaded-simple-data-type-access-and-atomic-variables

学习总结

作者首先提出了一个问题:是否需要用mutex对int进行protect,对一个int使用mutex或semaphore进行保护开销太大了,然后得出结论是使用spinlock,即自旋锁(busy-wait),比较合适。但是在获取自旋锁时要记得不能进行其他较为耗时的操作。

Intel x86 and x86_64 processor architectures (as well as vast majority of other modern CPU architectures) has instructions that allow one to lock FSB, while doing some memory access. FSB stands for Front Serial Bus. This is the bus that processor use to communicate with RAM. I.e. locking FSB will prevent from any other processor (core), and process running on that processor, from accessing RAM. And this is exactly what we need to implement atomic variables.
Atomic variables being widely used in kernel, but from some reason no-one bothered to implement them for user-mode folks. Until gcc 4.1.2.

作者提到,Intel x86/x86_64处理器架构有只允许单独锁住FSB的指令。FSB即为Front Serial Bus,处理器使用该总线与RAM交互。某个处理器(记为A)锁住FSB之后,其他处理器,以及在A上运行的进程都不能访问RAM,因此可以使用这一点实现atomic variable。
atomic variable在linux kernel中被广泛使用,但是直到gcc 4.1.2才可以在用户模式下使用。

相关函数

这些函数都是built-in函数,不需要包含头文件

// 先获取变量值再改变它
type __sync_fetch_and_add (type *ptr, type value);
type __sync_fetch_and_sub (type *ptr, type value);
type __sync_fetch_and_or (type *ptr, type value);
type __sync_fetch_and_and (type *ptr, type value);
type __sync_fetch_and_xor (type *ptr, type value);
type __sync_fetch_and_nand (type *ptr, type value);

// 先改变变量再获取它
type __sync_add_and_fetch (type *ptr, type value);
type __sync_sub_and_fetch (type *ptr, type value);
type __sync_or_and_fetch (type *ptr, type value);
type __sync_and_and_fetch (type *ptr, type value);
type __sync_xor_and_fetch (type *ptr, type value);
type __sync_nand_and_fetch (type *ptr, type value);

/*
type in each of the expressions can be one of the following:

    int
    unsigned int
    long
    unsigned long
    long long
    unsigned long long
*/

测试程序

#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
#include <errno.h>

#define INC_TO 1000000 // one million...

int global_int = 0;

pid_t gettid( void )
{
    return syscall( __NR_gettid );
}

void *thread_routine( void *arg )
{
    int i;
    int proc_num = (int)(long)arg;
    cpu_set_t set;

    CPU_ZERO( &set );
    CPU_SET( proc_num, &set );

    if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    {
        perror( "sched_setaffinity" );
        return NULL;
    }

    for (i = 0; i < INC_TO; i++)
    {
        // global_int++; // 不使用原子操作,结果是不对的,不过作者指出加上-O2能得到正确结果
        __sync_fetch_and_add( &global_int, 1 );
    }

    return NULL;
}

int main()
{
    int procs = 0;
    int i;
    pthread_t *thrs;

    // Getting number of CPUs
    procs = (int)sysconf( _SC_NPROCESSORS_ONLN );
    if (procs < 0)
    {
        perror( "sysconf" );
        return -1;
    }

    thrs = malloc( sizeof( pthread_t ) * procs );
    if (thrs == NULL)
    {
        perror( "malloc" );
        return -1;
    }

    printf( "Starting %d threads...\n", procs );

    for (i = 0; i < procs; i++)
    {
        if (pthread_create( &thrs[i], NULL, thread_routine,
            (void *)(long)i ))
        {
            perror( "pthread_create" );
            procs = i;
            break;
        }
    }

    for (i = 0; i < procs; i++)
        pthread_join( thrs[i], NULL );

    free( thrs );

    printf( "After doing all the math, global_int value is: %d\n",
        global_int );
    printf( "Expected value is: %d\n", INC_TO * procs );

    return 0;
}

编译

$ gcc -pthread yourfilename

程序首先获取处理器个数N,然后创建N个线程分别在N个处理器上运行(原来还可以这么完>_<)。这四个线程都对global_int进行加1操作,正确结果应该是1000000×N。如果不进行一些同步控制(mutex、semaphore、spinlock)或使用上面提到的built-in原子操作函数,那么结果是随机的。尽管加上-O2也能得到正确结果,但是还是使用原子操作比较合理。

一些细节

for (i = 0; i < procs; i++)
{
  if (pthread_create( &thrs[i], NULL, thread_routine,
    (void *)(long)i ))
  // ...
}

注意pthread_create传递的最后一个参数是否会出现“竞争”。这里的代码不会出现。因为i是以值传递过去的。

<think> 嗯,用户这次提出了一个关于KafkaConsumer线程安全性的具体技术问题,结合之前提供的站内引用来看,用户似乎对多线程编程和资源竞争有相当深入的理解。引用[1]和[5]提到的mutex和临界区概念尤其相关,这正是解决KafkaConsumer线程安全问题的核心机制。 用户可能正在开发分布式消息处理系统,遇到了多线程消费消息的性能瓶颈。从ta精准的术语使用("not safe for multi-threaded access")来看,应该是位有经验的开发工程师,可能正在调优Kafka消费者端的并发模型。考虑到Kafka在高并发场景的广泛应用,这个问题确实很典型。 关于KafkaConsumer的线程安全性,需要从三个层面分析:首先是Java客户端本身的设计原理——它的非线程安全特性决定了必须采取同步措施;其次是具体线程冲突的表现形式,比如coordinator节点通信冲突;最后是工程实践的解决方案。 注意到用户引用的资料里提到工作窃取算法(task_group)和RESTful的自描述性,这些虽然不直接相关,但暗示用户可能在使用类似AKKA的响应式框架,或者在设计微服务架构。在解决方案部分,除了常规的线程封闭方案,或许可以补充Kafka Streams这种声明式API的替代方案,更符合现代架构趋势。 用户没有提供具体环境细节,但引用[4]提到Amazon ElastiCache,可能暗示ta的系统部署在AWS环境。在解决方案中需要强调云环境下的网络延迟考量,比如consumer多线程时的心跳超时风险。 值得注意的细节是,用户引用的[2]展示了C++的lambda传递参数方式,虽然与Java无关,但说明ta关注线程启动的参数传递模式。在演示Java解决方案时,应该保持类似的清晰参数传递风格,比如使用final引用或CompletableFuture的链式调用。 </think> KafkaConsumer 在多线程环境下不安全的主要原因在于其**内部状态的非原子性**和**网络通信的复杂性**,以下是详细分析和解决方案: --- ### **核心原因分析** 1. **非线程安全设计** KafkaConsumer 维护了复杂的内部状态(如消费位移、分区分配、心跳线程等)。多个线程并发调用其方法(如 `poll()`, `commitSync()`)会导致状态不一致,引发以下问题: - 位移提交冲突:线程A提交位移后,线程B可能覆盖提交,导致消息重复消费或丢失。 - 分区分配紊乱:再均衡(rebalance)期间若多线程操作,可能引发 `ConcurrentModificationException`。 - 心跳中断:心跳线程可能被其他线程干扰,导致消费者被错误踢出组。 2. **网络I/O与线程阻塞** `poll()` 方法包含网络请求(拉取消息、发送心跳)。若多线程并发调用: - 可能触发多个未完成的网络请求,破坏连接状态。 - 阻塞操作(如 `commitSync()`)会冻结其他线程的操作。 3. **隐式全局状态** 如引用 **[5]** 提到的临界区问题:KafkaConsumer 相当于一个需独占访问的非共享资源。多个线程同时操作类似于未加锁访问临界区,必然导致竞态条件。 --- ### **解决方案** #### ✅ 推荐方案:每线程独立 KafkaConsumer ```java ExecutorService executor = Executors.newFixedThreadPool(N); List<KafkaConsumer<String, String>> consumers = new ArrayList<>(); for (int i = 0; i < N; i++) { KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("topic")); consumers.add(consumer); executor.submit(() -> { while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); // 处理记录(单线程安全) } }); } ``` **优点**: - 各线程持有独立 `KafkaConsumer` 实例,完全隔离状态 **[^5]** - 天然避免再均衡冲突 - 参考引用 **[2]** 的任务分组思想:线程池自动管理任务调度 #### ️ 替代方案:单Consumer + 多处理线程 ```java KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("topic")); ExecutorService processor = Executors.newFixedThreadPool(N); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); processor.submit(() -> processRecords(records)); // 提交记录处理任务 } ``` **关键限制**: 1. 必须保证 **`poll()` 仅由单线程调用**(违反则崩溃) 2. 处理完成后需**手动提交位移**(异步提交更安全) 3. 需处理消息顺序性:同一分区消息需由同一线程处理 #### 🔒 同步锁方案(不推荐) ```java private final KafkaConsumer<String, String> consumer; private final ReentrantLock lock = new ReentrantLock(); public void run() { lock.lock(); try { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); // 处理记录 consumer.commitSync(); } finally { lock.unlock(); } } ``` **缺点**: - 锁竞争严重限制吞吐量 - 阻塞期间心跳可能超时,引发再均衡 **[^1]** - 违背 Kafka 高并发设计初衷 --- ### **最佳实践总结** 1. **首选独立Consumer模式**:每线程独享实例,最大化并行度(参考引用 **[2]** 的线程管理思路)。 2. **位移提交策略**: - 使用 `commitAsync()` 避免阻塞 - 结合 `commitSync()` 做最终一致性保证(如关闭前) 3. **分区分配优化**:若需细粒度控制,可手动分配分区(`assign()` 替代 `subscribe()`)。 4. **监控线程活跃度**:确保心跳线程(`background-thread`)不被阻塞 **[^4]**。 > 如引用 **[3]** 所述,系统设计需保持 **自描述性**:在代码中明确标注 `KafkaConsumer` 的线程约束,例如添加注释:`// 非线程安全!禁止多线程直接访问`。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值