Linux线程同步—竞态条件和互斥锁(C语言)

线程同步---竞态条件和锁

1.竞态条件

线程同步是并发编程中的一个重要概念,它涉及到多个线程之间如何协调对共享资源的访问,以确保程序的正确性和效率。竞态条件和锁是线程同步中两个关键的概念,它们之间有着紧密的联系和区别。

1.1定义

  • 当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步 措施,就会遇到线程同步问题。这种情况下,程序最终的结果依赖于线程执行的具体时序, 导致了竞态条件。
  • 竞态条件(race condition)是一种特定的线程同步问题,指的是两个或者以上进 程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。它会导致程 序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机。为了 确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件。

1.2锁

锁是一种同步机制,用于在并发环境中对共享资源进行互斥访问,确保同一时间只有一个线程能够访问共享资源,从而避免竞态条件和数据不一致的问题。常见的锁包括互斥锁(Mutex)、读写锁(RWMutex)等。

1.2.1锁的作用
  1. 互斥访问:通过锁机制,可以确保在任意时刻只有一个线程能够访问共享资源,从而避免多个线程同时修改资源导致的冲突和数据不一致。
  2. 保护临界区:在并发编程中,将访问共享资源的代码段称为临界区。通过加锁,可以保护临界区内的代码不被多个线程同时执行,从而确保数据的一致性和完整性。
1.2.2锁的使用
  1. 加锁:在访问共享资源之前,线程需要获取锁。如果锁已被其他线程持有,则当前线程需要等待直到锁被释放。
  2. 访问资源:在成功获取锁之后,线程可以安全地访问共享资源。
  3. 释放锁:在访问完共享资源后,线程需要释放锁,以便其他线程可以获取锁并访问资源。

1.3竞态条件和锁的关系

竞态条件是并发编程中需要避免的问题,而锁是解决竞态条件的一种有效手段。通过加锁机制,可以确保对共享资源的访问是有序的、互斥的,从而避免竞态条件的发生。然而,锁也会带来一定的性能开销,包括锁的获取和释放、线程的调度等。因此,在设计并发程序时,需要权衡竞态条件的风险和锁的开销,选择合适的同步机制来平衡正确性和性能。

2.竞态案例

下面的程序没有合理控制线程的并发访问,可能会引发竞态条件。

cpp 复制代码
//
// Created by root on 2024/9/19.
//
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREAD_COUNT 20000

void *add_thread(void *argv){
    int *num=(int *)argv;
    (*num)++;
    return (void *)0;
}

int main(int argc,char *argv[]){
    pthread_t pid[THREAD_COUNT];
    int num=0;
    for (size_t i = 0; i <THREAD_COUNT ; ++i) {
        pthread_create(pid+i,NULL,add_thread,&num);
    }

    for (int i = 0; i < THREAD_COUNT; ++i) {
        pthread_join(pid[i],NULL);
    }

    printf("%d\n",num);
    return 0;
}

可以看到 20000 个线程对 num 的累加结果是不确定的,没有达到我们的预期值 20000。 这是因为线程之间出现了竞争,不同线程对于 num 的累加操作可能重叠,这就会导致多次 累加操作可能只生效一次。

3.如何避免竞态条件

3.1方法

  1. 避免多线程写入一个地址:其可以通过逻辑上组织业务逻辑实现。
  2. 给资源加锁 :使同一时间操作特定资源的线程只有一个。想解决竞争问题,我们需要互斥锁------mutex

3.2锁机制

锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。 包括上面的互斥锁在内,常见的锁机制共有三种:

  1. 互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
  2. 读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问 是互斥的。
  3. 自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间 非常短的场景,一般是 Linux 内核使用。

4.互斥锁

4.1pthread_mutex_t

4.1.1定义

pthread_mutex_t 是一个定义在头文件中的联合体类型的别名, 其声明如下。

cpp 复制代码
typedef union
{ 
 struct __pthread_mutex_s __data; 
 char __size[__SIZEOF_PTHREAD_MUTEX_T];
 long int __align; 
} pthread_mutex_t; 

​ pthread_mutex_t 用作线程之间的互斥锁。互斥锁是一种同步机制,用来控制对共享 资源的访问。在任何时刻,最多只能有一个线程持有特定的互斥锁。如果一个线程试图获 取一个已经被其他线程持有的锁,那么请求锁的线程将被阻塞,直到锁被释放

4.2Mutex的作用

  • 保护共享数据,避免同时被多个线程访问导致的数据不一致问题。
  • 实现线程间的同步,确保线程之间对共享资源的访问按照预定的顺序进行。

4.3流程

  1. 初始化(pthread_mutex_init):创建互斥锁并初始化。
  2. 锁定(pthread_mutex_lock):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。
  3. 尝试锁定(pthread_mutex_trylock):尝试获取互斥锁。如果锁已被持有,立即返回而不是阻塞。
  4. 解锁(pthread_mutex_unlock):释放互斥锁,使其可被其他线程获取。
  5. 销毁(pthread_mutex_destroy):清理互斥锁资源。

4.4相关函数

4.4.1pthread_mutex_lock
cpp 复制代码
#include <pthread.h>
/**
* @brief 获取锁,如果此时锁被占则阻塞
* 
* @param mutex 锁
* @return int 获取锁结果
*/
int pthread_mutex_lock(pthread_mutex_t *mutex);

​ 该函数用于锁定指定的互斥锁。如果互斥锁已经被其他线程锁定,调用此函数的线程 将会被阻塞,直到互斥锁变为可用状态。这意味着如果另一个线程持有锁,当前线程将等 待直到锁被释放。

​ 成功时返回 0;失败时返回错误码。

4.4.2pthread_mutex_trylock
cpp 复制代码
/**
* @brief 非阻塞式获取锁,如果锁此时被占则返回 EBUSY
* 
* @param mutex 锁
* @return int 获取锁结果
*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);

​ 该函数尝试锁定指定的互斥锁。与 pthread_mutex_lock 不同,如果互斥锁已经被其 他线程锁定,pthread_mutex_trylock 不会阻塞调用线程,而是立即返回一个错误码 (EBUSY)。

​ 如果成功锁定互斥锁,则返回 0;如果互斥锁已被其他线程锁定,返回 EBUSY;其他 错误情况返回不同的错误码。

4.4.3pthread_mutex_unlock
cpp 复制代码
/**
* @brief 释放锁
* 
* @param mutex 锁
* @return int 释放锁结果
*/
int pthread_mutex_unlock(pthread_mutex_t *mutex);

​ 该函数用于解锁指定的互斥锁。调用线程必须是当前持有互斥锁的线程;否则,解锁 操作可能会失败。

​ 成功时返回 0;失败时返回错误码。

4.4.4pthread_mutex_init

pthread_mutex_init 是 POSIX 线程(也称为 pthreads)库中用于初始化互斥锁(mutex)的函数。互斥锁是用于同步线程的工具,它们允许多个线程以受控的方式访问共享资源,以避免数据竞争和其他并发问题。

cpp 复制代码
#include <pthread.h>  
  
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex 指向要初始化的互斥锁对象的指针。这是一个类型为 pthread_mutex_t 的变量,通常在你定义它的时候就被声明为全局或静态变量,以便多个线程可以访问它。

  • attr 是一个指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性。大多数情况下,如果你不需要特殊的互斥锁属性,可以将此参数设置为 NULL,此时互斥锁将使用默认属性进行初始化。

  • 成功时,pthread_mutex_init 返回 0

  • 出错时,返回错误码。这些错误码可以包括但不限于 EAGAIN(资源暂时不可用,但这不是 pthread_mutex_init 的典型错误)、EINVAL(参数无效,例如 mutex 是一个无效的地址),或 ENOMEM(内存不足,无法初始化互斥锁)。

4.5初始化互斥锁

​ PTHREAD_MUTEX_INITIALIZER 是 POSIX 线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被锁 定和解锁,而不需要在程序运行时显式调用初始化函数。

​ 当我们使用 PTHREAD_MUTEX_INITIALIZER 初始化互斥锁时,实际上是将互斥锁设置 为默认属性和未锁定状态。这种初始化方式适用于简单的同步问题,我们可以通过以下代 码初始化互斥锁。

cpp 复制代码
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

4.6将 mutex 加入线程

4.6.1案例测试

​ 为了保证计算结果的正确性,很显然,我们应阻塞式获取互斥锁,应调用的是 pthread_mutex_lock 函数。共享变量修改完成后,应该释放锁。

cpp 复制代码
//
// Created by root on 2024/9/19.
//
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREAD_COUNT 20000

//初始化互斥锁
static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

void *add_thread(void *argv){
    int *num=(int *)argv;
    //加锁,同一时间只有一个线程可以访问
    pthread_mutex_lock(&mutex);
    (*num)++;
    //解锁
    pthread_mutex_unlock(&mutex);
    return (void *)0;
}

int main(int argc,char *argv[]){
    pthread_t pid[THREAD_COUNT];
    int num=0;
    for (size_t i = 0; i <THREAD_COUNT ; ++i) {
        pthread_create(pid+i,NULL,add_thread,&num);
    }

    for (int i = 0; i < THREAD_COUNT; ++i) {
        pthread_join(pid[i],NULL);
    }

    printf("%d\n",num);
    return 0;
}
4.6.2注意

​ 上述代码中,互斥锁 counter_mutex 并未被显式销毁 ,但这通常不会引起 资源泄露问题。上述程序在所有线程执行完毕后直接结束,进程结束时,操作系统会回收该进程的所有资源,包括内存、打开的文件描述符和互斥锁等。因此即便没有显式销毁互斥锁也不 会有问题。

​ 在某些情况下,确实需要显式销毁互斥锁资源。如果互斥锁是动态分配 的(使用 pthread_mutex_init 函数初始化),或者互斥锁会被跨多个函数或文件使用 ,不再需要时必须显式销毁它。但对于静态初始化,并且在程序结束时不再被使用的互斥锁(上述程 序中的 counter_mutex),显式销毁不是必需的。

相关推荐
编程版小新2 分钟前
C++初阶:STL详解(四)——vector迭代器失效问题
开发语言·c++·迭代器·vector·迭代器失效
人类群星闪耀时3 分钟前
服务器管理:从零开始的服务器安装与配置指南
运维·服务器
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
gopher95111 小时前
linux驱动开发-中断子系统
linux·运维·驱动开发
AlexMercer10121 小时前
【C++】二、数据类型 (同C)
c语言·开发语言·数据结构·c++·笔记·算法
码哝小鱼1 小时前
firewalld封禁IP或IP段
linux·网络
鼠鼠龙年发大财1 小时前
【x**3专享】安装SSH、XFTP、XShell、ARM Linux
linux·arm开发·ssh
nfgo1 小时前
快速体验Linux发行版:DistroSea详解与操作指南
linux·ubuntu·centos
河南宽信李工1503806 16862 小时前
测绘航空摄影专项资质在洛阳市的获取流程
服务器
小灰灰爱代码2 小时前
C++——求3个数中最大的数(分别考虑整数、双精度数、长整数的情况),用函数模板来实现。
开发语言·c++·算法