C Linux 下常用锁介绍
目录
1、互斥锁
(1)基本概念
(2)相关 API 函数
(3)使用 Demo
(4)注意事项
2、自旋锁
(1)基本概念
(2)相关 API 函数
(3)使用 Demo
(4)注意事项
3、文件共享锁
(1)基本概述
(2)相关 API 函数
(3)使用 Demo
1)flock
2)fcntl
4、读写锁
(1)基本概述
(2)相关 API 函数
(3)使用 Demo
在多线程和多进程编程中,锁机制是确保数据一致性和避免竞态条件的关键工具。通过合理使用锁,可以有效管理对共享资源的访问,防止多个线程或进程同时修改同一资源,从而保证程序的正确性和稳定性。选择合适的锁类型(如互斥锁、自旋锁、读写锁等)和策略,能够优化性能并减少资源浪费。
作者日常中最常用的,用户态的锁机制主要分为两类:变量锁和文件锁。
对于变量加锁,互斥锁是最常用的方式,但相比自旋锁,它的效率较低;而自旋锁虽然效率高,但由于占用大量 CPU 资源,只适合短时间内的加锁操作。读写锁则适用于读多写少的场景。
对于文件加锁,共享锁可以避免在读取文件过程中内容发生改变,同时也可以添加互斥锁以确保文件操作的安全性。
总的来说,选择合适的锁类型需要根据具体场景来决定,以达到最优的性能和安全性。
1、互斥锁
(1)基本概念
互斥锁是一种同步机制,用于确保在同一时刻只有一个线程能够访问共享资源。当一个线程持有互斥锁时,其他试图获取该锁的线程将被阻塞,直到锁被释放。
如果一个线程试图获取已经锁定的互斥锁时,该线程会被内核挂起(即路径阻塞),释放 CPU 时间片资源。直到另一个拥有此互斥锁的线程释放它,操作系统会将这个处于等待状态的线程唤醒,并让它继续执行。在锁的获取过程中会涉及到线程上下文的切换调度。
互斥锁适用于长时间锁定的场景,由于使用互斥锁时系统将阻塞等待线程,因此非常适合那种可能持有锁一段时间的操作(比如执行一些 I/O 任务)。对于这种情况来说,减少线程在无锁等待过程中的 CPU 占用是很关键。
(2)相关 API 函数
#include <pthread.h>
# 静态初始化:可以在声明互斥锁的同时进行初始化,使用 PTHREAD_MUTEX_INITIALIZER 宏定义。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
# 动态初始化:通过 pthread_mutex_init 函数进行初始化,可以灵活设置互斥锁的属性。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
# 销毁:当不再需要互斥锁时,应使用 pthread_mutex_destroy 函数销毁它,以释放系统资源。
pthread_mutex_destroy(&mutex);
# 加锁:在访问共享资源之前,需要对互斥锁进行加锁操作,使用 pthread_mutex_lock 函数。如果锁已经被其他线程持有,当前线程将会阻塞,直到锁被释放。
pthread_mutex_lock(&mutex);
# 解锁:访问完共享资源后,需要对互斥锁进行解锁操作,使用 pthread_mutex_unlock 函数,以便其他线程能够继续访问共享资源。
pthread_mutex_unlock(&mutex);
(3)使用 Demo
两个线程对共享资源 shared_variable 进行循环累加,可以看出该共享资源被两个线程依次访问,该共享资源的访问过程是线程安全的。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 定义共享变量
int shared_variable = 0;
// 定义互斥锁
pthread_mutex_t mutex;
// 线程函数
void* thread_function(void* arg) {
int id = *((int*)arg);
for (int i = 0; i < 5; ++i) {
// 加锁
pthread_mutex_lock(&mutex);
// 访问和修改共享变量
printf("Thread %d: shared_variable = %d\n", id, shared_variable);
shared_variable++;
printf("Thread %d: shared_variable after increment = %d\n", id, shared_variable);
// 解锁
pthread_mutex_unlock(&mutex);
// 模拟一些工作
sleep(1);
}
return NULL;
}
int main() {
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建线程
pthread_t thread1, thread2;
int thread1_id = 1;
int thread2_id = 2;
pthread_create(&thread1, NULL, thread_function, &thread1_id);
pthread_create(&thread2, NULL, thread_function, &thread2_id);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("Final value of shared_variable = %d\n", shared_variable);
return 0;
}
(4)注意事项
- 死锁:在使用互斥锁时,需要特别注意避免死锁。死锁是指两个或多个线程互相等待对方释放锁,从而导致程序无法继续执行。在设计多线程程序时,应避免在一个线程中持有多个锁,或者确保加锁顺序一致。
- 锁的粒度:锁的粒度指的是锁保护的资源范围。锁的粒度过大会导致线程竞争严重,降低程序的并发性能;锁的粒度过小则会增加锁操作的开销。因此,在设计多线程程序时,需要权衡锁的粒度,选择合适的锁保护范围。
- 性能影响:互斥锁的使用会带来一定的性能开销,特别是在高并发环境下。为了减少锁的性能影响,可以考虑使用读写锁(pthread_rwlock_t)或自旋锁(pthread_spinlock_t)等更高效的同步机制。
2、自旋锁
(1)基本概念
自旋锁是一种使线程在无法获取锁时一直循环直至获得所需的锁。与互斥锁不同,它不引起线程切换,如果锁可得,则直接使用;否则,线程就会“自旋”(即在循环中等待)直到条件改变。
该锁的优点就是低延迟,自旋等待比通过系统调用进行阻塞和调度切换要快得多,因为在高并发情况下上下文切换本身的开销可能超过实际执行的时间,而自旋锁避免了线程上下文切换调度带来的损耗。
但是该锁只适用于短时间锁定的场景,因为在“自旋”获取锁的过程中,该线程不会释放 CPU 资源,这在单线程或者系统负载较重时是不利的。而且当高优先级的进程需要等待一个低优先级进程释放其正在使用的资源时(即自旋锁),可能会导致性能瓶颈。
(2)相关 API 函数
#include <pthread.h>
# 参数 lock 是要初始化的自旋锁的指针,pshared 表示自旋锁的共享属性。
# 如果 pshared 设为 PTHREAD_PROCESS_SHARED,则自旋锁可以在多个进程的线程之间共享;
# 如果 pshared 设为 PTHREAD_PROCESS_PRIVATE,则自旋锁只能在初始化它的进程内的线程之间使用。
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
# 用于销毁一个已经初始化的自旋锁并释放相关资源。
# 在调用此函数后,如果没有重新初始化自旋锁,任何对其的访问都是未定义的行为。
int pthread_spin_destroy(pthread_spinlock_t *lock);
# 获取指定的自旋锁。如果自旋锁当前没有被其他线程持有,调用该函数的线程将获得自旋锁;
# 否则,线程会进入自旋状态,不断检查锁是否可用,直到获得锁为止。
# 注意,如果调用该函数的线程已经持有了该自旋锁,结果是不确定的。
int pthread_spin_lock(pthread_spinlock_t *lock);
# 尝试获取指定的自旋锁,如果锁当前可用,则立即获得锁并返回 0;
# 如果锁不可用,则不进入自旋状态,直接返回错误码 EBUSY,表示锁当前被其他线程持有。
int pthread_spin_trylock(pthread_spinlock_t *lock);
# 释放指定的自旋锁。只有持有自旋锁的线程才能调用此函数来释放锁。
int pthread_spin_unlock(pthread_spinlock_t *lock);
(3)使用 Demo
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 定义一个全局的自旋锁
pthread_spinlock_t spinlock;
// 共享资源
int shared_resource = 0;
// 线程函数
void* thread_func(void* arg) {
int id = *((int*)arg);
int i = 0;
for (i = 0; i < 5; ++i) {
// 尝试获取自旋锁
pthread_spin_lock(&spinlock);
// 临界区开始
printf("Thread %d is in critical section with i %d\n", id, i + 1);
shared_resource++;
printf("Shared resource value: %d\n", shared_resource);
// 模拟一些工作
sleep(1);
// 临界区结束
// 释放自旋锁
pthread_spin_unlock(&spinlock);
// 非临界区
printf("Thread %d is in non-critical section\n", id);
sleep(1);
}
return NULL;
}
int main() {
// 初始化自旋锁
if (pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE) != 0) {
fprintf(stderr, "Failed to initialize spinlock\n");
return EXIT_FAILURE;
}
// 创建两个线程
pthread_t threads[2];
int thread_ids[2] = {1, 2};
int i = 0;
for (i = 0; i < 2; ++i) {
if (pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]) != 0) {
fprintf(stderr, "Failed to create thread %d\n", i + 1);
return EXIT_FAILURE;
}
}
// 等待所有线程完成
for (i = 0; i < 2; ++i) {
pthread_join(threads[i], NULL);
}
// 销毁自旋锁
pthread_spin_destroy(&spinlock);
printf("Final value of shared resource: %d\n", shared_resource);
return EXIT_SUCCESS;
}
(4)注意事项
- C 标准支持:旧的 C 标准可能并不支持 pthread_spinlock_t;
- 避免长时间持有:如果锁持有时间较长,自旋锁会浪费大量的CPU资源。在这种情况下,可能需要考虑使用其他类型的锁机制。
- 高竞争环境:在高竞争环境下,多个线程可能会频繁地尝试获取同一个自旋锁,导致性能下降。在这种情况下,可能需要优化锁的使用方式或考虑使用更高级的同步机制。
- 死锁问题:虽然自旋锁本身不会导致死锁,但在使用时仍需注意避免因编程错误导致的死锁情况
3、文件共享锁
(1)基本概述
共享锁是一种文件锁定机制,主要用于确保在读取文件的同时,其他进程不会对文件进行更改,从而确保数据的一致性。共享锁又称为读锁。
当一个进程请求共享锁时,操作系统会检查是否有其他进程持有独占锁或写锁。如果没有,系统将授予共享锁。此时,其他进程仍然可以请求并获得共享锁,但不能获取独占锁或写锁。当所有共享锁释放后,请求独占锁或写锁的进程才能获得锁。
共享锁主要用于:
- 多进程文件读取:多个进程需要同时读取文件,但需要确保在读取期间,文件内容不会被修改。
- 读者 - 写者问题解决方案:在文件读写操作中实现读者 - 写者问题的解决方案,即允许多个读者同时读取文件,但写者必须独占访问权。
- 保护关键资源:避免多个进程同时修改配置文件等关键资源。
(2)相关 API 函数
/*
参数:
fd:要加锁或解锁的文件的描述符。
operation:指定加锁或解锁的类型,可取以下值之一:
LOCK_SH:建立共享锁,允许多个进程对文件进行读取操作,但不允许写入。
LOCK_EX:建立互斥锁,只允许一个进程对文件进行读取或写入操作,其他进程对该文件的访问将被阻塞。
LOCK_UN:释放之前对文件的锁定。
LOCK_NB:非阻塞模式,如果无法立即获得锁,则函数不会阻塞,而是立即返回。
返回值:
成功时返回0,失败时返回-1,并设置相应的错误码。
*/
int flock(int fd, int operation)
/*
参数:
fd:文件描述符。
cmd:指定要执行的命令,常用的命令包括:
F_DUPFD:复制文件描述符。
F_GETFD:获取文件描述符标志。
F_SETFD:设置文件描述符标志。
F_GETFL:获取文件状态标志。
F_SETFL:设置文件状态标志。
F_GETLK:获取文件锁定的状态。
F_SETLK:设置文件锁定的状态。
F_SETLKW:设置文件锁定的状态,如果无法锁定则等待。
arg:可选参数,根据cmd的不同而不同。
#ifndef F_RDLCK
/* For posix fcntl() and `l_type' field of a `struct flock' for lockf(). */
# define F_RDLCK 0 /* Read lock. */
# define F_WRLCK 1 /* Write lock. */
# define F_UNLCK 2 /* Remove lock. */
#endif
返回值:
成功时返回新的文件描述符(对于F_DUPFD命令)或其他相应的值,失败时返回-1,并设置相应的错误码。
*/
int fcntl(int fd, int cmd, ... /* arg */)
(3)使用 Demo
1)flock
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("example.txt", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
exit(-1);
}
// 共享锁
if (flock(fd, LOCK_EX) == -1) {
perror("flock");
close(fd);
exit(-1);
}
printf("File locked for writing.
");
// Perform file operations here
if (flock(fd, LOCK_UN) == -1) {
perror("flock");
close(fd);
exit(-1);
}
printf("File unlocked.
");
close(fd);
return 0;
}
2)fcntl
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("example.txt", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
_exit(-1);
}
struct flock lock;
lock.l_type = F_RDLCK; // 设置为读锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; // 锁定整个文件
lock.l_pid = getpid();
// 加锁
if (fcntl(fd, F_SETLKW, &lock) == -1) {
perror("fcntl");
close(fd);
_exit(-1);
}
printf("File locked for writing.\n");
// Perform file operations here
// 解锁
lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("fcntl");
close(fd);
_exit(1);
}
printf("File unlocked.\n");
close(fd);
return 0;
}
4、读写锁
(1)基本概述
读写锁是一种高级同步原语,与互斥锁(mutex)和条件变量(condition variable)不同。它允许多个线程同时读取共享资源,但在写入共享资源时,必须确保没有其他线程在读取或写入。读写锁有三种状态:
- 读模式(读锁):多个线程可以同时持有读锁,只要没有线程持有写锁。
- 写模式(写锁):只有一个线程可以持有写锁,此时不允许任何线程持有读锁或写锁。
- 无锁状态:资源没有被任何线程锁定。
读写锁非常适用于读多写少的场景,例如:
- 数据缓存:多个线程频繁读取缓存数据,写入较少。
- 配置文件读取:配置文件在程序运行时需要频繁读取,但修改很少。
- 数据库连接池:多个线程读取连接池状态,偶尔有线程更新连接池配置。
(2)相关 API 函数
#include <pthread.h>
pthread_rwlock_t rwlock;
/*
初始化一个读写锁对象。
rwlock: 指向要初始化的读写锁对象的指针。
attr: 指向读写锁属性对象的指针,如果为NULL,则使用默认属性。
*/
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
/*
销毁一个读写锁对象。
*/
pthread_rwlock_destroy(&rwlock);
/*
获取读锁。如果写锁未被持有,则当前线程可以获取读锁并继续执行;否则,线程会被阻塞直到写锁释放。
*/
pthread_rwlock_rdlock(&rwlock);
/*
获取写锁。如果没有任何读锁或写锁被持有,则当前线程可以获取写锁并继续执行;否则,线程会被阻塞直到所有读锁和写锁释放。
*/
pthread_rwlock_wrlock(&rwlock);
/*
释放读写锁。如果当前线程持有读锁,则释放读锁;如果当前线程持有写锁,则释放写锁。
*/
pthread_rwlock_unlock(&rwlock);
(3)使用 Demo
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 共享资源
int shared_data = 0;
// 读写锁
pthread_rwlock_t rwlock;
// 读线程函数
void* reader(void* arg) {
int thread_id = *((int*)arg);
int i = 0;
for (i = 0; i < 5; ++i) {
pthread_rwlock_rdlock(&rwlock);
printf("Reader %d: Read shared_data = %d\n", thread_id, shared_data);
pthread_rwlock_unlock(&rwlock);
sleep(1); // 模拟读操作耗时
}
return NULL;
}
// 写线程函数
void* writer(void* arg) {
int thread_id = *((int*)arg);
int i = 0;
for (i = 0; i < 5; ++i) {
pthread_rwlock_wrlock(&rwlock);
shared_data++;
printf("Writer %d: Wrote shared_data = %d\n", thread_id, shared_data);
pthread_rwlock_unlock(&rwlock);
sleep(2); // 模拟写操作耗时
}
return NULL;
}
int main() {
// 初始化读写锁
if (pthread_rwlock_init(&rwlock, NULL) != 0) {
fprintf(stderr, "Failed to initialize rwlock\n");
return EXIT_FAILURE;
}
// 创建线程ID数组
int reader_ids[3];
int writer_ids[2];
// 创建读者线程
pthread_t readers[3];
int i = 0;
for (i = 0; i < 3; ++i) {
reader_ids[i] = i + 1;
if (pthread_create(&readers[i], NULL, reader, &reader_ids[i]) != 0) {
fprintf(stderr, "Failed to create reader thread %d\n", i + 1);
return EXIT_FAILURE;
}
}
// 创建写者线程
pthread_t writers[2];
for (i = 0; i < 2; ++i) {
writer_ids[i] = i + 1;
if (pthread_create(&writers[i], NULL, writer, &writer_ids[i]) != 0) {
fprintf(stderr, "Failed to create writer thread %d\n", i + 1);
return EXIT_FAILURE;
}
}
// 等待所有线程完成
for (i = 0; i < 3; ++i) {
pthread_join(readers[i], NULL);
}
for (i = 0; i < 2; ++i) {
pthread_join(writers[i], NULL);
}
// 销毁读写锁
if (pthread_rwlock_destroy(&rwlock) != 0) {
fprintf(stderr, "Failed to destroy rwlock\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}