Linux之锁
引言
线程间同步与互斥是并发编程中非常重要的概念,主要用于解决多个线程同时访问共享资源时可能引发的数据不一致或数据损坏问题。
并发是指计算机系统中同时处理多个任务的能力。这里的“同时”并不总是意味着所有任务在确切的同一时刻执行,而是指通过快速切换任务或利用多核处理器的能力,在宏观上给人一种所有任务都在同一时间运行的印象。
1.线程同步
线程同步是指在并发环境下,控制不同线程按照一定的顺序执行以保证程序的正确性。同步可以确保当一个线程对共享资源进行操作时,其他线程不会对该资源进行修改,从而避免了竞争条件(race condition)。同步机制有助于协调线程间的活动,使得它们能够有序地执行而不会导致逻辑错误或数据损坏。
常见的同步方法包括:
- .锁(Locks):互斥锁是最基本的同步工具之一,用于确保任何时刻只有一个线程可以访问共享资源。
- .信号量(Semaphores): 允许多个线程同时访问一个资源,但限制最大并发数。
- 条件变量(Condition Variables): 用于实现线程之间的简单通信,允许一个线程等待另一个线程的通知后才继续执行。
- .读写锁(Read-Write Locks): 对于读多写少的情况特别有用,允许多个线程同时读取数据,但在写入时需要独占访问。
2.互斥
互斥(Mutual Exclusion, Mutex)是一种特殊的同步方式,旨在防止多个线程同时访问同一个共享资源。互斥通常通过使用锁来实现。互斥锁可以有两种状态:锁定和解锁。当一个线程获取到互斥锁并进入临界区(即访问共享资源的代码段),其他试图获取同一互斥锁的线程将被阻塞,直到第一个线程释放该锁。
互斥的关键在于它保证了在同一时间只有一个线程能够进入临界区,这有效地避免了竞态条件的发生。然而,不正确的使用互斥锁可能会导致死锁等问题,其中两个或更多的线程都在等待对方释放锁而无法继续执行。
互斥的核心思想是:在并发执行的多个线程中,互斥锁定某个资源,使得其他线程不能同时操作该资源,避免数据的不一致性
总结
(1)线程同关注的是如何让多个线程有序地执行,,确保线程协作,以达到预期的行为;而互斥则是用来保护共享资源免受并发访问的影响(避免多个线程同时访问共享资源,保证资源访问的排他性)。
(2)使用适当的同步和互斥机制可以帮助我们构建高效且无错误的并发程序,但同时也需要注意避免潜在的问题如死锁、饿死等。正确设计和测试这些机制对于开发可靠的多线程应用程序至关重要。
常见的锁机制
锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。包括上面的互斥锁在内,常见的锁机制共有三种:
(1)互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
(2)读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问是互斥的。
(3)自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景,一般是Linux内核使用。
1.互斥锁
-
pthread_mutex_t
pthread_mutex_t 是一个定义在头文件<pthreadtypes.h>中的联合体类型的别名,其声明如下。
typedef union
{
struct __pthread_mutex_s __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
pthread_mutex_t用作线程之间的互斥锁。互斥锁是一种同步机制,用来控制对共享资源的访问。在任何时刻,最多只能有一个线程持有特定的互斥锁。如果一个线程试图获取一个已经被其他线程持有的锁,那么请求锁的线程将被阻塞,直到锁被释放。
1.用途
保护共享数据,避免同时被多个线程访问导致的数据不一致问题。实现线程间的同步,确保线程之间对共享资源的访问按照预定的顺序进行
2.操作
初始化(pthread_mutex_init):创建互斥锁并初始化。
锁定(pthread_mutex_lock):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。
尝试锁定(pthread_mutex_trylock):尝试获取互斥锁。如果锁已被持有,立即返回而不是阻塞。
解锁(pthread_mutex_unlock):释放互斥锁,使其可被其他线程获取。
销毁(pthread_mutex_destroy):清理互斥锁资源。
3.互斥锁操作函数
互斥锁相关的操作函数是POSIX的一部分,默认情况下,当前的Ubuntu系统没有这些函数的手册页。使用指令安装POSIX标准的手册页。
(1)pthread_mutex_lock
该函数用于锁定指定的互斥锁。如果互斥锁已经被其他线程锁定,调用此函数的线程将会被阻塞,直到互斥锁变为可用状态。这意味着如果另一个线程持有锁,当前线程将等待直到锁被释放。
成功时返回0;失败时返回错误码。
(2)pthread_mutex_trylock
该函数尝试锁定指定的互斥锁。与pthread_mutex_lock不同,如果互斥锁已经被其他线程锁定,pthread_mutex_trylock不会阻塞调用线程,而是立即返回一个错误码(EBUSY)。
如果成功锁定互斥锁,则返回0;如果互斥锁已被其他线程锁定,返回EBUSY;其他错误情况返回不同的错误码。
(3)pthread_mutex_unlock
该函数用于解锁指定的互斥锁。调用线程必须是当前持有互斥锁的线程;否则,解锁操作可能会失败。
成功时返回0;失败时返回错误码。
初始化互斥锁
PTHREAD_MUTEX_INITIALIZER是POSIX线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被锁定和解锁,而不需要在程序运行时显式调用初始化函数。
当我们使用PTHREAD_MUTEX_INITIALIZER初始化互斥锁时,实际上是将互斥锁设置为默认属性和未锁定状态。这种初始化方式适用于简单的同步问题,我们可以通过以下代码初始化互斥锁。
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
将mutex加入我们刚刚的程序
为了保证计算结果的正确性,很显然,我们应阻塞式获取互斥锁,应调用的是pthread_mutex_lock函数。共享变量修改完成后,应该释放锁。
示例说明:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define THREAD_COUNT 20000
/*
PTHREAD_MUTEX_INITIALIZER是POSIX线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。
这个宏为互斥锁提供了一个初始状态,使其准备好被锁定和解锁,而不需要在程序运行时显式调用初始化函数。
*/
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;/*** @brief 对传入值累加1** @param argv 传入指针* @return void* 无返回值*/
/*
该函数 `add_thread` 是一个线程函数,用于对指针指向的整数值进行加锁、累加和解锁操作。功能如下:
1. 接收一个 `void*` 类型参数并转换为 `int*`;
2. 使用互斥锁(`pthread_mutex_lock`)保护临界区;
3. 对指针指向的值加1;
4. 操作完成后释放锁(`pthread_mutex_unlock`);
5. 返回 NULL 指针作为线程退出状态。
*/
void *add_thread(void *argv)
{int *p = argv;// 累加之前加锁,此时其他获取该锁的线程都会被阻塞pthread_mutex_lock(&counter_mutex);(*p)++;// 累加之后释放锁pthread_mutex_unlock(&counter_mutex);return (void *)0;
}int main()
{/*这段代码定义了一个`pthread_t`类型的数组`pid`,大小为`THREAD_COUNT`。 功能:**用于存储多个线程的ID**,每个元素代表一个线程的唯一标识符。*/ pthread_t pid[THREAD_COUNT];int num = 0;/*
这段代码的功能是:**循环创建 `THREAD_COUNT` 个线程,每个线程都执行 `add_thread` 函数,并将 `&num` 作为参数传递给这些线程。**
- `pthread_create`:用于创建一个新线程。
- `pid + i`:指向线程ID数组的第 `i` 个位置。
- `NULL`:表示使用默认线程属性。
- `add_thread`:线程执行的函数。
- `&num`:传递给线程函数的参数。*/// 用20000个线程对num作累加for (int 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
(4)注意
上述代码中,互斥锁counter_mutex并未被显式销毁,但这通常不会引起资源泄露问题。上述程序在所有线程执行完毕后直接结束,进程结束时,操作系统会回收该进程的所有资源,包括内存、打开的文件描述符和互斥锁等。因此即便没有显式销毁互斥锁也不会有问题。
在某些情况下,确实需要显式销毁互斥锁资源。如果互斥锁是动态分配的(使用pthread_mutex_init函数初始化),或者互斥锁会被跨多个函数或文件使用,不再需要时必须显式销毁它。但对于静态初始化,并且在程序结束时不再被使用的互斥锁(上述程序中的counter_mutex),显式销毁不是必需的。
2.读写锁
定义:
读写锁是一种特殊的锁,可以提高并发程序中对于共享资源的读操作效率。读写锁分为读锁和写锁两种,读锁可被多个线程同时持有,但是写锁只能被单个线程持有。当某个线程占用写锁时,其他线程无法持有读锁或写锁,这样可以保证数据的一致性。读写锁适用于多读少写的场景,因为它可以允许多个线程同时读取共享资源,而不会因为读操作的阻塞而影响程序性能。
工作原理:
读操作:在读写锁的控制下,多个线程可以同时获得读锁。这些线程可以并发地读取共享资源,但它们的存在阻止了写锁的授予。
写操作:如果至少有一个读操作持有读锁,写操作就无法获得写锁。写操作将会阻塞,直到所有的读锁都被释放。
相关调用
1.pthread_rwlock_t
声明如下。
typedef union
{
struct __pthread_rwlock_arch_t __data;
char __size[__SIZEOF_PTHREAD_RWLOCK_T];
long int __align;
} pthread_rwlock_t;
2.pthread_rwlock_init()
/**
* @brief 为rwlock指向的读写锁分配所有需要的资源,并将锁初始化为未锁定状态。读写锁的属性由attr参数指定,如果attr为NULL,则使用默认属性。当锁的属性为默认时,可以通过宏PTHREAD_RWLOCK_INITIALIZER初始化,即
* pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 效果和调用当前方法并为attr传入NULL是一样的
*
* @param rwlock 读写锁
* @param attr 读写锁的属性
* @return int 成功则返回0,否则返回错误码
*/
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
3.pthread_rwlock_destroy()
#include <pthread.h>
/**
* @brief 销毁rwlock指向的读写锁对象,并释放它使用的所有资源。当任何线程持有锁的时候销毁锁,或尝试销毁一个未初始化的锁,结果是未定义的。
*
* @param rwlock
* @return int
*/
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
4.pthread_rwlock_rdlock()
/**
* @brief 应用一个读锁到rwlock指向的读写锁上,并使调用线程获得读锁。如果写线程持有锁,调用线程无法获得读锁,它会阻塞直至获得锁。
*
* @param rwlock 读写锁
* @return int 成功返回0,失败返回错误码
*/
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
5.pthread_rwlock_wrlock()
/**
* @brief 应用一个写锁到rwlock指向的读写锁上,并使调用线程获得写锁。只要任意线程持有读写锁,则调用线程无法获得写锁,它将阻塞直至获得写锁。
*
* @param rwlock 读写锁
* @return int 成功返回0,失败返回错误码
*/
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
6.pthread_rwlock_unlock()
/**
* @brief 释放调用线程锁持有的rwlock指向的读写锁。
*
* @param rwlock 读写锁
* @return int 成功返回0.失败返回错误码
*/
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
示例代码:
模拟一个共享变量的读写过程:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define NUM_THREADS 3
#define NUM_ITERATIONS 10int shared_data = 0; // 共享数据
pthread_rwlock_t rwlock; // 读写锁void *reader(void *arg)
{for (int i = 0; i < NUM_ITERATIONS; ++i) {pthread_rwlock_rdlock(&rwlock);printf("Reader %ld: read shared_data = %d\n", (long int) arg, shared_data);pthread_rwlock_unlock(&rwlock);usleep(rand() % 100000); // 模拟读操作耗时}pthread_exit(NULL);
}void *writer(void *arg)
{for (int i = 0; i < NUM_ITERATIONS; ++i) {pthread_rwlock_wrlock(&rwlock);shared_data++;printf("Writer %ld: write shared_data = %d\n", (long int) arg, shared_data);pthread_rwlock_unlock(&rwlock);usleep(rand() % 100000); // 模拟写操作耗时}pthread_exit(NULL);
}int main() {/*这段代码定义了一个`pthread_t`类型的数组`threads`,大小为`NUM_THREADS`。 功能:**用于存储多个线程的标识符(ID)**,便于后续对这些线程进行操作(如等待、取消等)。*/pthread_t threads[NUM_THREADS];pthread_rwlock_init(&rwlock, NULL);// 创建读线程和写线程for (intptr_t i = 0; i < NUM_THREADS; ++i) {if (i == 0) {pthread_create(&threads[i], NULL, writer, (void *) i);} else {pthread_create(&threads[i], NULL, reader, (void *) i);}}// 等待线程结束for (int i = 0; i < NUM_THREADS; ++i) {pthread_join(threads[i], NULL);}pthread_rwlock_destroy(&rwlock);return 0;
}
3.自旋锁
在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。自旋锁相对于其他锁的优点是它们在锁被占用时会持续检查锁的状态(即“自旋”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免了线程上下文切换的开销。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用。