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 则是当所有上面的条件都不满足时要执行的语句块。


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

目录
相关文章
|
8天前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
12 1
|
12天前
|
Java
并发编程的艺术:Java线程与锁机制探索
【6月更文挑战第21天】**并发编程的艺术:Java线程与锁机制探索** 在多核时代,掌握并发编程至关重要。本文探讨Java中线程创建(`Thread`或`Runnable`)、线程同步(`synchronized`关键字与`Lock`接口)及线程池(`ExecutorService`)的使用。同时,警惕并发问题,如死锁和饥饿,遵循最佳实践以确保应用的高效和健壮。
25 2
|
3天前
|
分布式计算 并行计算 安全
在Python Web开发中,Python的全局解释器锁(Global Interpreter Lock,简称GIL)是一个核心概念,它直接影响了Python程序在多线程环境下的执行效率和性能表现
【6月更文挑战第30天】Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其是在多核CPU上。GIL确保同一时间仅有一个线程执行Python字节码,导致CPU密集型任务时多线程无法充分利用多核,反而可能因上下文切换降低性能。然而,I/O密集型任务仍能受益于线程交替执行。为利用多核,开发者常选择多进程、异步IO或使用不受GIL限制的Python实现。在Web开发中,理解GIL对于优化并发性能至关重要。
18 0
|
12天前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
|
21天前
|
安全 Java API
Java并发基础-启动和终止线程
Java并发基础-启动和终止线程
23 0
|
5天前
|
Java 调度
Java多线程编程与并发控制策略
Java多线程编程与并发控制策略
|
5天前
|
安全 Java 开发者
Java并发编程:理解并发与多线程
在当今软件开发领域,Java作为一种广泛应用的编程语言,其并发编程能力显得尤为重要。本文将深入探讨Java中的并发编程概念,包括多线程基础、线程安全、并发工具类等内容,帮助开发者更好地理解和应用Java中的并发特性。
6 1
|
9天前
|
Java
Java中的`synchronized`关键字是一个用于并发控制的关键字,它提供了一种简单的加锁机制来确保多线程环境下的数据一致性。
【6月更文挑战第24天】Java的`synchronized`关键字确保多线程数据一致性,通过锁定代码块或方法防止并发冲突。同步方法整个方法体为临界区,同步代码块则锁定特定对象。示例展示了如何在`Counter`类中使用`synchronized`保证原子操作和可见性,同时指出过度使用可能影响性能。
20 4
|
13天前
|
安全 Java Python
GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。
【6月更文挑战第20天】GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。线程池通过预创建线程池来管理资源,减少线程创建销毁开销,提高效率。示例展示了如何使用Python实现一个简单的线程池,用于执行多个耗时任务。
20 6
|
16天前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现