linux c 多线程 互斥锁、自旋锁、原子操作的分析与使用

简介: 生活中,我们常常会在12306或者其他购票软件上买票,特别是春节期间或者国庆长假的时候,总会出现抢票的现象,最后总会有人买不到票而埋怨这埋怨那,其实这还好,至少不会跑去现场或者网上去找客服理论,如果出现了付款,但是却没买到票的现象,那才是真的会出现很多问题,将这里的票引入到多线程中,票就被称为临界资源。

情景分析

生活中,我们常常会在12306或者其他购票软件上买票,特别是春节期间或者国庆长假的时候,总会出现抢票的现象,最后总会有人买不到票而埋怨这埋怨那,其实这还好,至少不会跑去现场或者网上去找客服理论,如果出现了付款,但是却没买到票的现象,那才是真的会出现很多问题,将这里的票引入到多线程中,票就被称为临界资源。


问题引入

多线程的引入无疑是高性能服务器的必要技术之一,但是如果不控制好临界资源的使用,就会出现一些列的问题,比如会出现多个线程同时去访问同一个临界资源。就拿上面买票来说,某一张票,如果同时有多个人在多个软件上抢购,如果不对其进行控制,就会出现一张票可能被多个人购买的情况,这肯定是不合理的。

我们首先模拟一下卖票的情形,代码中模拟10个窗口卖票,也就是使用10个线程,另外票的总数为100w张,如果按照每个窗口平均下来就应该卖出10w张,最开始为1张票都没卖,然后每个线程卖出一张票,就让票的数量+1,直到数量为总共的100w张。

#include <pthread.h>
#include <stdio.h>
#define  THREAD_COUNT  10
//主要完成的工作,10个线程对主线程count完成++,每个线程完成10w次,10个线程应该让count增加到100w
//注意主线程循环100次,每次循环等待1s,100s时间足够长,那么主线程会保证没有退出,那么count
//的值(地址会一直存在)会一直存在,而且会被10个线程一直累加,那么最后会不会累加到100w呢?
void *thread_callback(void *arg){ //注意arg参数是count的地址,通过创建线程传过来的参数
    int *pcount = (int *)arg;
    for(int i = 0; i < 100000; ++i){
        (*pcount)++;
        usleep(1);//sleep 1ms, 模拟效果更明显
    }
}
int main(){
    pthread_t thread_id[THREAD_COUNT] = {0}; //线程id列表
    int count = 0;
    for(int i = 0; i < THREAD_COUNT; ++i){
        pthread_create(&thread_id[i], NULL, thread_callback, &count);
    }
    for(int i =0; i < 100; i++){
        printf("count :%d \n", count);
        sleep(1);//sleep 1s ,保证100s以上来所有线程完成count++工作
    }
    return 0;
}

在linux下编译:gcc -o thread_mutex thread_mutex.c -g -lpthread 后然后运行得到结果如下:

79cf9ea7a5f04be52c2a7e9484a07d96_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_14,color_FFFFFF,t_70,g_se,x_16.png

count没有增加到100w,而是到997850后数字就没有增加了。


原因分析

如果按照正常逻辑,10个线程同时对1个数进行增加,每个线程累加10w次,那么应该累加到100w次,而且线程里面的逻辑就是count++的操作,没有其他操作,即使不同线程对count的争夺,按照正常想法也是一个线程会增加了count后,然后另一个线程会在上一个线程对count增加后才会去获取count,其实不然,count++实际上不是原子操作,count++实际上在底层有3个操作。下面用图片来分析说明。

其实count++在硬件上的操作主要分为3步

mov [count], eax; //将count的值赋值给寄存器eax
inc eax; //寄存器eax自增
mov eax, [count]; //将eax的值赋值给count

上面的3句代码是count++的实际操作过程,只要有一点点汇编基础,很容易就看懂那几句的意思,多线程正常的情况下,会按照下面的情况执行

fd0ff857b554c21b5358bd360f20828a_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_18,color_FFFFFF,t_70,g_se,x_16.png

这时候线程1和线程2对count的++操作是没问题的,但是如果像下面这种情况就不对了。

0b9260985263635efe3473715792ccf3_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_20,color_FFFFFF,t_70,g_se,x_16.png

假如开始count为50,线程1去执行++操作,执行完第一步就被线程2抢夺,那么线程2执行时,count的值还是50,执行++,为51,注意,然后线程1去执行时,++时还是对自身eax获取的值50执行执行++操作,也是51,这时候2个线程都执行了++操作,但是却只增加了一次,这就是问题出现的原因。


解决办法

对临界资源的控制,在window下由临界区,锁,等,在linux下同样有锁的概念,另外,linux下还有原子操作的概念。

关于互斥锁,自旋锁网上都有很多说明,这里都不详细说了,只简单提及以下


互斥锁

从名称来看互斥锁的功能就是锁某一段时间只能有一个线程进行访问,线程释放锁之后,其他线程才可以对锁进行操作。那么我们在count++执行前加一个互斥锁,那么是不是就相当于执行到count++时,由于锁目前被一个线程使用,其他线程是不能执行count++操作的,这样就可以了


自旋锁

自旋锁的实现跟互斥锁的实现一模一样,同样也是在count++前对自旋锁加锁,执行完++操作后,在释放锁,让其他线程去获取锁,加锁,然后执行,释放锁。

虽然2种锁实现方法大同小异,但是自旋锁与互斥锁是有很大区别的,主要区别如下:

**自旋锁:**当某个线程执行到某段加锁的代码时,如果发现当前锁被其他线程占用,那么当前线程就会一直死等,等到其他线程释放锁,然后继续执行其代码

互斥锁:互斥锁恰好与自旋锁相反,当一个线程执行加锁的代码时,发现锁当前被其他线程加锁了,那么当前线程会进行休眠状态,释放相关资源,然后一段时间后再次去尝试获取资源,执行相关代码。

那么就这个例子来说,自旋锁要比互斥锁更好,因为++操作很快,互斥锁频繁的线程切换会导致消耗更多的资源。


原子操作

原子操作的实现不需要对代码进行加锁和解锁,原子操作把多条指令直接变成单条指令,然后依靠CPU去执行,就这个例子来说,原子操作比2个锁方案都好。


代码实现

#include <stdio.h>
#include <pthread.h>
#define THREAD_COUNT  10
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
//原子操作:关于原子操作,可以直接调用相应的api
//这里使用代码来实现,具体啥意思我也不懂
int inc(int *value, int add) {
  int old;
  __asm__ volatile(
  "lock; xaddl %2, %1;"
  : "=a" (old)
  : "m" (*value), "a"(add)
  : "cc", "memory"
  );
  return old;
}
void *thread_callback(void *arg) {
  int *pcount = (int *)arg;
  int i = 0;
  while (i ++ < 100000) {
#if 0
  (*pcount) ++; //
#elif 0
  pthread_mutex_lock(&mutex);
  (*pcount) ++; //
  pthread_mutex_unlock(&mutex);
#elif 0
  pthread_spin_lock(&spinlock);
  (*pcount) ++; //
  pthread_spin_unlock(&spinlock);
#else
  inc(pcount, 1);
#endif
  usleep(1);
  }
}
int main () {
  pthread_t threadid[THREAD_COUNT] = {0};
  pthread_mutex_init(&mutex, NULL);
  pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
  int i = 0;
  int count = 0;
  for (i = 0;i < THREAD_COUNT;i ++) {
  pthread_create(&threadid[i], NULL, thread_callback, &count);
  }
  for (i = 0;i < 100;i ++) {
  printf("count : %d\n", count);
  sleep(1);
  }
  #if 0
    pthread_spin_destroy(&spinlock);
    pthread_mutex_destroy(&mtx); 
    #endif
}

源码来源

腾讯课堂-零声学院king老师


相关文章
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
7月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
428 1
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
1242 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
270 6
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
10月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
435 83
|
7月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
288 6
|
12月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
420 0
|
8月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
516 16
|
7月前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
692 0