线程同步---竞态条件和锁
1.竞态条件
线程同步是并发编程中的一个重要概念,它涉及到多个线程之间如何协调对共享资源的访问,以确保程序的正确性和效率。竞态条件和锁是线程同步中两个关键的概念,它们之间有着紧密的联系和区别。
1.1定义
- 当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步 措施,就会遇到线程同步问题。这种情况下,程序最终的结果依赖于线程执行的具体时序, 导致了竞态条件。
- 竞态条件(race condition)是一种特定的线程同步问题,指的是两个或者以上进 程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。它会导致程 序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机。为了 确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件。
1.2锁
锁是一种同步机制,用于在并发环境中对共享资源进行互斥访问,确保同一时间只有一个线程能够访问共享资源,从而避免竞态条件和数据不一致的问题。常见的锁包括互斥锁(Mutex)、读写锁(RWMutex)等。
1.2.1锁的作用
- 互斥访问:通过锁机制,可以确保在任意时刻只有一个线程能够访问共享资源,从而避免多个线程同时修改资源导致的冲突和数据不一致。
- 保护临界区:在并发编程中,将访问共享资源的代码段称为临界区。通过加锁,可以保护临界区内的代码不被多个线程同时执行,从而确保数据的一致性和完整性。
1.2.2锁的使用
- 加锁:在访问共享资源之前,线程需要获取锁。如果锁已被其他线程持有,则当前线程需要等待直到锁被释放。
- 访问资源:在成功获取锁之后,线程可以安全地访问共享资源。
- 释放锁:在访问完共享资源后,线程需要释放锁,以便其他线程可以获取锁并访问资源。
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方法
- 避免多线程写入一个地址:其可以通过逻辑上组织业务逻辑实现。
- 给资源加锁 :使同一时间操作特定资源的线程只有一个。想解决竞争问题,我们需要互斥锁------mutex
3.2锁机制
锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。 包括上面的互斥锁在内,常见的锁机制共有三种:
- 互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
- 读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问 是互斥的。
- 自旋锁(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流程
- 初始化(pthread_mutex_init):创建互斥锁并初始化。
- 锁定(pthread_mutex_lock):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。
- 尝试锁定(pthread_mutex_trylock):尝试获取互斥锁。如果锁已被持有,立即返回而不是阻塞。
- 解锁(pthread_mutex_unlock):释放互斥锁,使其可被其他线程获取。
- 销毁(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),显式销毁不是必需的。