5. 多线程并发锁

简介: 5. 多线程并发锁

本文介绍了多线程并发下为了避免临界资源被抢占而出现的错误,引入了锁和原子操作 来解决。


一、问题分析

创建10个线程,每个线程实现往总进程加1万个数。则总进程会达到10万

#include<stdio.h>
#include <unistd.h>
#include<pthread.h>
#define THREAD_COUNT    10
void *thread_callback(void* arg){
    int *pcount=(int *)arg;
    int i=0;
    while(i++ <10000){
        (*pcount)++;
        //usleep(1)是一个系统调用函数,它的作用是让当前进程暂停执行一微秒(即等待一微秒
        usleep(1);
    }
}
int main(){
    pthread_t threadid[THREAD_COUNT]={0};
    int i=0;
    int count=0;
    //创建10个线程
    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(n)是一个系统调用函数,它的作用是让当前进程暂停执行n秒钟(即等待n秒钟)
        sleep(1);
    }
}

但实现过程中,会发现最后达不到100万。这种现象的原因在于count是一个临界资源,即多个线程共用一个变量。我们分析一下count++的汇编指令

mov count,eax;
inc eax;
mov eax,count;

实际中却可能出现如下异常情况

解决办法是加锁,即打包锁住count++的三个汇编指令,不可分开,其他线程不可抢占。


二、 锁和原子操作

把count的三条汇编指令变成一条指令,就不需要加锁。这种称为原子操作。


原子操作是指在并发环境下对共享资源进行访问或修改时,能够保证该操作具有不可分割性和独占性的一种操作方式。通常情况下,原子操作是由硬件级别提供支持的,可以确保在执行过程中不会被中断或者其他线程干扰,从而避免了竞态条件等问题的发生。

#include<stdio.h>
#include <unistd.h>
#include<pthread.h>
#define THREAD_COUNT    10
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
//对value加上add
int inc(int *value,int add){
    int old;
    //__asm__是GCC的内嵌汇编语言指令
    __asm__ volatile(
        "lock;xaddl %2,%1;"// %2的值+%1的值,赋给%1;同时,在执行该指令期间,CPU会对总线进行锁定(lock),以确保整个操作是原子性地完成。
        : "=a" (old)
        : "m" (*value),"a"(add)
        : "cc","memory"
    );
    return old;
}
void *thread_callback(void* arg){
    int *pcount=(int *)arg;
    int i=0;
    while(i++ <10000){
#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);//初始化mutex
    pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);//初始化spinlock,进程之间共享
    int i=0;
    int count=0;
    //创建10个线程
    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(n)是一个系统调用函数,它的作用是让当前进程暂停执行n秒钟(即等待n秒钟)
        sleep(1);
    }
}


三、相关知识点补充

1、创建进程

pthread_t是一个线程标识符,它可以唯一地标识一个线程。在多线程编程中,我们通常使用pthread_create()函数来创建一个新的线程,并且该函数会返回一个pthread_t类型的值,以便我们在后续代码中对这个新线程进行操作。


pthread_create()是一个函数,用于创建一个新的线程。其原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

该函数接受四个参数:

  • thread:指向pthread_t类型变量的指针,用于存储新线程的标识符。
  • attr:指向pthread_attr_t类型变量的指针,用于设置新线程的属性(如栈大小、优先级等)。如果为NULL,则使用默认属性。
  • start_routine:指向新线程要执行的函数的指针。该函数必须返回void*类型,并且只能有一个参数,即对应于该函数调用时传递给它的参数。
  • arg: 传递给start_routine函数的参数。

例如,下面是一个简单的例子,演示了如何使用pthread_create()来创建一个新线程:

#include <pthread.h>
#include <stdio.h>
void* thread_func(void *arg) {
    printf("New thread created\n");
}
int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thread_func, NULL);
    if (ret != 0) {
        printf("Error creating thread\n");
        return -1;
    }
    // Wait for the new thread to complete
    pthread_join(tid, NULL);
    return 0;
}

在上述代码中,我们定义了一个名为“thread_func”的函数作为新线程要执行的内容。在主函数中,我们调用了pthread_create()函数来创建一个新线程,并将该函数的返回值存储到变量tid中。然后,我们通过调用pthread_join()函数等待新线程执行完毕。在新线程内部,我们只是简单地打印一条消息以表明它已经被创建成功了。


2,条件编译指令

#if 0
    (*pcount)++;
#else 
    pthread_mutex_lock(&mutex);
    (*pcount)++;
    pthread_mutex_unlock(&mutex);
#endif

这是一个预处理器指令,用于在编译时控制代码的执行。如果 #if 0 部分是注释掉的代码或者不需要执行的代码,则可以使用 #if 0 来暂时禁用它们,以免浪费系统资源和时间。类似地,#elif 0 可以用来替代一部分被注释掉的代码或者不需要执行的代码,而 #else 则是当所有上面的条件都不满足时要执行的语句块。


使用条件编译指令的好处是可以在编译时根据预处理器定义的宏来控制代码的执行,而不需要手动注释或取消注释一大段代码。这种方法在调试和测试代码时非常有用,因为可以轻松地开启或关闭某些特定功能的实现,从而简化了代码的维护和调试。

目录
相关文章
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
933 0
|
2月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
160 2
|
2月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
222 1
|
5月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
362 83
|
5月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
341 83
|
7月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
259 0
|
7月前
|
设计模式 运维 监控
并发设计模式实战系列(4):线程池
需要建立持续的性能剖析(Profiling)和调优机制。通过以上十二个维度的系统化扩展,构建了一个从。设置合理队列容量/拒绝策略。动态扩容/优化任务处理速度。检查线程栈定位热点代码。调整最大用户进程数限制。CPU占用率100%
464 0
|
7月前
|
存储 缓存 安全
JUC并发—11.线程池源码分析
本文主要介绍了线程池的优势和JUC提供的线程池、ThreadPoolExecutor和Excutors创建的线程池、如何设计一个线程池、ThreadPoolExecutor线程池的执行流程、ThreadPoolExecutor的源码分析、如何合理设置线程池参数 + 定制线程池。
JUC并发—11.线程池源码分析
|
11月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
830 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析

热门文章

最新文章