目录
前言
实现
信号量(Semaphore)
计数型信号量
二值信号量
信号量的原语操作
无名信号量的操作函数
例子
互斥锁(mutex)
互斥锁的操作函数
例子
自旋锁 (Spinlock)
自旋锁与互斥锁的区别
自旋锁的操作函数
例子
前言
线程同步是为了对共享资源的访问进行保护,确保数据的一致性,由于进程中会有多个线程的存在,每个线程对共享资源的并发访问就会出现数据的一致性问题。
实现
实现线程同步的机制有很多,比较常用的有:信号量、互斥锁、自旋锁、条件变量、读写锁等等,这里仅探讨linux下的线程同步机制。
信号量(Semaphore)
信号量是一种非常常见的进程同步机制,主要用于多线程或多进程环境中,以控制对共享资源的访问。如果是按照命名区分可以分为有名信号量和无名信号量,本章主要使用无名信号量,因为无名信号量常用于线程同步,它们没有全局名称,只在进程内部有效,使用起来简单且高效。
如果按照信号量值分类可以分成计数型信号量与二值信号量。
计数型信号量
- 计数信号量的值可以是一个非负整数。
- 它用于控制多个相同资源的访问数量。
- 当一个进程(或线程)试图获取资源时,它会检查信号量的值是否大于零。如果大于零,则信号量减一,进程继续执行;否则,进程会进入等待状态,直到信号量的值大于零。
- 当一个进程释放资源时,它会将信号量的值加一,并唤醒等待队列中的一个进程(如果有的话)。
二值信号量
- 二元信号量的值只有0和1两种状态,类似于互斥锁(Mutex)。
- 当信号量为1时,表示资源是可用的;当信号量为0时,表示资源不可用。
- 一个进程获取资源时,将信号量设置为0;当进程释放资源时,将信号量设置为1。
- 二元信号量主要用于确保一次只有一个进程访问某个资源。
信号量的原语操作
-
P操作(Proberen):也叫wait或down操作,表示请求资源。
- 对于计数信号量,P操作会将信号量的值减一,如果结果为负,进程将进入等待状态。
- 对于二元信号量,如果信号量值为0,进程将进入等待状态;如果为1,则设置为0,继续执行。
-
V操作(Verhogen):也称为
signal
或up
操作,表示释放资源。- 对于计数信号量,V操作会将信号量的值加一,如果有进程在等待,将唤醒一个等待的进程。
- 对于二元信号量,将信号量设置为1,并唤醒一个等待的进程(如果有的话)。
无名信号量的操作函数
1、sem_init:初始化一个无名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);sem_t sem;
if (sem_init(&sem, 0, 1) != 0) {perror("sem_init");exit(EXIT_FAILURE);
}
-
参数:
sem
:指向信号量对象的指针。pshared
:指定信号量是否在进程间共享。0
表示用于线程间同步,非0
表示用于进程间同步。value
:信号量的初始值。
-
返回值:
- 成功返回
0
,失败返回-1
并设置errno
。
- 成功返回
2、sem_destroy:销毁一个无名信号量。
int sem_destroy(sem_t *sem);if (sem_destroy(&sem) != 0) {perror("sem_destroy");exit(EXIT_FAILURE);
}
-
参数:
sem
:指向要销毁的信号量对象的指针。
-
返回值:
- 成功返回
0
,失败返回-1
并设置errno
。
- 成功返回
3、sem_wait:等待信号量,将信号量的值减1。如果信号量的值为0,则阻塞直到信号量的值大于0。
int sem_wait(sem_t *sem);if (sem_wait(&sem) != 0) {perror("sem_wait");exit(EXIT_FAILURE);
}
-
参数:
sem
:指向信号量对象的指针。
-
返回值:
- 成功返回
0
,失败返回-1
并设置errno
。
- 成功返回
4、sem_post:释放信号量,将信号量的值加1。如果有其他线程正在阻塞等待该信号量,则唤醒其中一个线程。
int sem_post(sem_t *sem);if (sem_post(&sem) != 0) {perror("sem_post");exit(EXIT_FAILURE);
}
-
参数:
sem
:指向信号量对象的指针。
-
返回值:
- 成功返回
0
,失败返回-1
并设置errno
。
- 成功返回
5、sem_trywait:尝试等待信号量。如果信号量的值大于0,将信号量的值减1并立即返回。如果信号量的值为0,则立即返回错误而不阻塞。
int sem_trywait(sem_t *sem);if (sem_trywait(&sem) != 0) {if (errno == EAGAIN) {printf("信号量当前不可用\n");} else {perror("sem_trywait");exit(EXIT_FAILURE);}
}
-
参数:
sem
:指向信号量对象的指针。
-
返回值:
- 成功返回
0
,失败返回-1
并设置errno
。
- 成功返回
6、sem_timedwait:在指定的时间内等待信号量。如果在指定时间内信号量的值变为正数,则将其减1并返回。如果超过指定时间,返回错误。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; // 等待5秒if (sem_timedwait(&sem, &ts) != 0) {if (errno == ETIMEDOUT) {printf("等待超时\n");} else {perror("sem_timedwait");exit(EXIT_FAILURE);}
}
-
参数:
sem
:指向信号量对象的指针。abs_timeout
:指定的绝对超时时间。
-
返回值:
- 成功返回
0
,失败返回-1
并设置errno
。
- 成功返回
例子
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>sem_t sem_g, sem_p; // 定义两个信号量
char ch = 'a';void *pthread_g(void *arg) // 改变字符 ch 的值
{while (1){sem_wait(&sem_g); //第一次为0阻塞ch++;sleep(1);sem_post(&sem_p); //加1}
}void *pthread_p(void *arg) // 打印字符 ch 的值
{while (1){sem_wait(&sem_p); //非0,减一执行printf("%c", ch);fflush(stdout);sem_post(&sem_g); //为0,加一}
}int main()
{pthread_t tid1, tid2;sem_init(&sem_g, 0, 0); // 初始化信号量sem_init(&sem_p, 0, 1); //sem_p先获得1,先执行pthread_create(&tid1, NULL, pthread_g, NULL);pthread_create(&tid2, NULL, pthread_p, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
互斥锁(mutex)
互斥锁又叫互斥量,在对共享资源进行访问的时候可以进行上锁,访问结束后可以解锁。当有一个线程对互斥锁进行上锁后,其他线程也想对互斥锁进行上锁就会被阻塞,直到对互斥锁上锁的线程释放互斥锁为止。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行上锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
简单说,互斥量就是为了确保同一时间只有一个线程能够访问一个共享资源。
互斥锁的操作函数
1、pthread_mutex_init:初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);pthread_mutex_t mutex;
if (pthread_mutex_init(&mutex, NULL) != 0) {perror("pthread_mutex_init");exit(EXIT_FAILURE);
}
-
参数:
mutex
:指向互斥锁对象的指针。attr
:互斥锁属性。可以是NULL
,表示使用默认属性。
-
返回值:
- 成功返回
0
,失败返回错误代码。
- 成功返回
2、pthread_mutex_destroy:销毁一个互斥锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);if (pthread_mutex_destroy(&mutex) != 0) {perror("pthread_mutex_destroy");exit(EXIT_FAILURE);
}
-
参数:
mutex
:指向要销毁的互斥锁对象的指针。
-
返回值:
- 成功返回
0
,失败返回错误代码。
- 成功返回
3、pthread_mutex_lock:锁定一个互斥锁。如果互斥锁已经被锁定,则阻塞直到互斥锁可用。
int pthread_mutex_lock(pthread_mutex_t *mutex);if (pthread_mutex_lock(&mutex) != 0) {perror("pthread_mutex_lock");exit(EXIT_FAILURE);
}
-
参数:
mutex
:指向要锁定的互斥锁对象的指针。
-
返回值:
- 成功返回
0
,失败返回错误代码。
- 成功返回
4、pthread_mutex_trylock:尝试锁定一个互斥锁。如果互斥锁已经被锁定,则立即返回错误而不阻塞。
int pthread_mutex_trylock(pthread_mutex_t *mutex);if (pthread_mutex_trylock(&mutex) != 0) {if (errno == EBUSY) {printf("互斥锁当前已被锁定\n");} else {perror("pthread_mutex_trylock");exit(EXIT_FAILURE);}
}
-
参数:
mutex
:指向要锁定的互斥锁对象的指针。
-
返回值:
- 成功返回
0
,失败返回错误代码。
- 成功返回
5、pthread_mutex_unlock:解锁一个互斥锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);if (pthread_mutex_unlock(&mutex) != 0) {perror("pthread_mutex_unlock");exit(EXIT_FAILURE);
}
-
参数:
mutex
:指向要解锁的互斥锁对象的指针。
-
返回值:
- 成功返回
0
,失败返回错误代码。
- 成功返回
例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>#define NUM_ITERATIONS 1000000int counter = 0;
pthread_mutex_t mutex;void* increment(void* arg) {for (int i = 0; i < NUM_ITERATIONS; i++) {pthread_mutex_lock(&mutex); // 加锁counter++;pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}void* decrement(void* arg) {for (int i = 0; i < NUM_ITERATIONS; i++) {pthread_mutex_lock(&mutex); // 加锁counter--;pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}int main() {pthread_t thread1, thread2;// 初始化互斥锁if (pthread_mutex_init(&mutex, NULL) != 0) {perror("pthread_mutex_init");exit(EXIT_FAILURE);}// 创建线程if (pthread_create(&thread1, NULL, increment, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}if (pthread_create(&thread2, NULL, decrement, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}// 等待线程完成if (pthread_join(thread1, NULL) != 0) {perror("pthread_join");exit(EXIT_FAILURE);}if (pthread_join(thread2, NULL) != 0) {perror("pthread_join");exit(EXIT_FAILURE);}// 销毁互斥锁if (pthread_mutex_destroy(&mutex) != 0) {perror("pthread_mutex_destroy");exit(EXIT_FAILURE);}// 输出结果printf("Final counter value: %d\n", counter);return 0;
}
自旋锁 (Spinlock)
自旋锁(spinlock)是一种用于多线程编程的同步机制,用于保护共享资源,使同一时间只有一个线程可以访问该资源。与互斥锁不同,自旋锁在等待锁时不会使线程休眠,而是不断地检查锁的状态,直到获得锁。这种机制在短时间内需要高频率访问共享资源的情况下比较有效。
自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋), 所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。 试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁。
自旋锁与互斥锁的区别
特性 | 自旋锁(Spinlock) | 互斥锁(Mutex) |
等待机制 | 忙等待,循环检查锁状态 | 睡眠等待,线程被挂起 |
CPU开销 | 高(忙等待消耗CPU) | 低(等待时线程睡眠) |
上下文切换 | 不会发生 | 可能发生 |
适用场景 | 锁持有时间短,多处理器系统 | 锁持有时间长,各类系统 |
复杂度 | 简单 | 复杂(需要操作系统调度支持) |
死锁检测 | 一般没有自动检测机制 | 可以有自动检测机制 |
自旋锁的操作函数
1、spin_lock_init:初始化一个自旋锁。
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);static pthread_spinlock_t spin;//定义自旋锁
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
- 参数:
lock
:指向pthread_spinlock_t
类型的指针,用于存储初始化后的自旋锁。pshared
:指定锁的共享性质,可以是PTHREAD_PROCESS_PRIVATE
或PTHREAD_PROCESS_SHARED
。
- 返回值:
- 成功初始化返回 0,失败返回错误码。
2、pthread_spin_destroy:用于销毁自旋锁。
int pthread_spin_destroy(pthread_spinlock_t *lock);static pthread_spinlock_t spin;
/* 销毁自旋锁 */pthread_spin_destroy(&spin);
- 参数:
lock
:指向pthread_spinlock_t
类型的指针,指定要销毁的自旋锁。
- 返回值:
- 成功销毁返回 0,失败返回错误码。
3、pthread_spin_lock:用于获取自旋锁,如果自旋锁已经被其他线程持有,则当前线程会一直自旋等待直到获取锁为止。
int pthread_spin_lock(pthread_spinlock_t *lock);static pthread_spinlock_t spin;//定义自旋锁
pthread_spin_lock(&spin); //自旋锁上锁
- 参数:
lock
:指向pthread_spinlock_t
类型的指针,指定要获取的自旋锁。
- 返回值:
- 成功获取锁返回 0,失败返回错误码。
4、pthread_spin_trylock:用于尝试获取自旋锁,如果自旋锁已经被其他线程持有,则立即返回失败,不会进入自旋等待。
int pthread_spin_trylock(pthread_spinlock_t *lock);
- 参数:
lock
:指向pthread_spinlock_t
类型的指针,指定要尝试获取的自旋锁。
- 返回值:
- 成功获取锁返回 0,失败返回
EBUSY
(锁已经被持有)或其他错误码。
- 成功获取锁返回 0,失败返回
5、pthread_spin_unlock:用于释放自旋锁,允许其他线程继续获取该自旋锁。
int pthread_spin_unlock(pthread_spinlock_t *lock);pthread_spin_unlock(&spin);//自旋锁解锁
- 参数:
lock
:指向pthread_spinlock_t
类型的指针,指定要释放的自旋锁。
- 返回值:
- 成功释放锁返回 0,失败返回错误码。
例子
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // 提供 sleep 函数pthread_spinlock_t lock;
int shared_variable = 0;void* thread_func(void* arg) {for (int i = 0; i < 5; ++i) {pthread_spin_lock(&lock);shared_variable++;printf("Thread %ld: shared_variable = %d\n", (long)pthread_self(), shared_variable);pthread_spin_unlock(&lock);sleep(1); // 模拟耗时操作}return NULL;
}int main() {pthread_t thread1, thread2;// 初始化自旋锁pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);// 创建线程pthread_create(&thread1, NULL, thread_func, NULL);pthread_create(&thread2, NULL, thread_func, NULL);// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 销毁自旋锁pthread_spin_destroy(&lock);return 0;
}