mutex
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t * mutex,
const pthread_mutexattr_t * attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
返回值:成功返回0,失败返回错误号。
pthread_mutex_init
函数对Mutex做初始化,参数attr
设定Mutex的属性,如果attr
为NULL
则表示缺省属性。用pthread_mutex_init
函数初始化的Mutex可以用pthread_mutex_destroy
销毁。如果Mutex变量是静态分配的(全局变量或static
变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER
来初始化,相当于用pthread_mutex_init
初始化并且attr
参数为NULL
。Mutex的加锁和解锁操作可以用下列函数:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号。
一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
Condition Variable
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。Condition Variable用pthread_cond_t
类型的变量表示,可以这样初始化和销毁:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:成功返回0,失败返回错误号。
和Mutex的初始化和销毁类似,pthread_cond_init
函数初始化一个Condition Variable,attr
参数为NULL
则表示缺省属性,pthread_cond_destroy
函数销毁一个Condition Variable。如果Condition Variable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER
初始化,相当于用pthread_cond_init
函数初始化并且attr
参数为NULL
。Condition Variable的操作可以用下列函数:
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *abstime);
int pthread_cond_wait(pthread_cond_t *cond,
pthread_mutex_t *mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误号。
可见,一个Condition Variable总是和一个Mutex搭配使用的。一个线程可以调用pthread_cond_wait
在一个Condition Variable上阻塞等待,这个函数做以下三步操作:
- 释放Mutex
- 阻塞等待
- 当被唤醒时,重新获得Mutex并返回
pthread_cond_timedwait
函数还有一个额外的参数可以设定等待超时,如果到达了abstime
所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT
。一个线程可以调用pthread_cond_signal
唤醒在某个Condition Variable上等待的另一个线程,也可以调用pthread_cond_broadcast
唤醒在这个Condition Variable上等待的所有线程。
#include <pthread.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <stdbool.h>
bool flag = false;
pthread_cond_t cnd = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static void func1()
{
printf("enter func1\n");
pthread_mutex_lock(&mtx);
while (!flag)
{
printf("pthread_cond_wait before\n");
pthread_cond_wait(&cnd, &mtx);
//执行到在这里的时候
//1、自动先释放mutex,那么signal的线程才可以抢占mutex,才能够修改条件变量(flag)
//2、阻塞等待
//3、当被唤醒时,重新获取mutex并返回。
// (注意这里,当另外一个线程signal时,并不是立即执行后面的语句,而是要等另外一个线程释放了mutex,
// 这里重新获取了mutex才会继续执行后续的代码)
printf("pthread_cond_wait after\n");
}
pthread_mutex_unlock(&mtx);
printf("exit func1\n");
}
static void func2()
{
printf("enter func2\n");
pthread_mutex_lock(&mtx);
flag = true;
pthread_cond_signal(&cnd);
printf("pthread_cond_signal after\n");
sleep(2);
pthread_mutex_unlock(&mtx);
printf("exit func2\n");
}
int main(void)
{
printf("__STDC__=%d __STDC_VERSION__=%d\n", __STDC__, __STDC_VERSION__);
pthread_t id;
pthread_create(&id, NULL, func1, NULL);
sleep(2);
pthread_create(&id, NULL, func2, NULL);
while (1)
{
usleep(1000);
}
return 0;
}
输出结果为:
__STDC__=1 __STDC_VERSION__=201112
enter func1
pthread_cond_wait before
enter func2
pthread_cond_signal after
exit func2
pthread_cond_wait after
exit func1
Semaphore
Mutex变量是非0即1的,可看作一种资源的可用数量,初始化时Mutex是1,表示有一个可用资源,加锁时获得该资源,将Mutex减到0,表示不再有可用资源,解锁时释放该资源,将Mutex重新加到1,表示又有了一个可用资源。
信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
semaphore变量的类型为sem_t,sem_init()初始化一个semaphore变量,value参数表示可用资源的数量,pshared参数为0表示信号量用于同一进程的线程间同步,本节只介绍这种情况。在用完semaphore变量之后应该调用sem_destroy()释放与semaphore相关的资源。
调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。
#include <pthread.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <stdbool.h>
#include <semaphore.h>
#define QUEUE_SIZE (10)
int queue[QUEUE_SIZE];
sem_t empty; //距离空了还剩余的个数
sem_t full; //距离满了还剩余的个数
static void take()
{
int t = 0;
while (1)
{
sem_wait(&empty); //每一次取的时候判断是否为空
queue[t] = 0;
printf("take queue[%d]\n", t);
t = (t + 1) % QUEUE_SIZE; //取的索引加1
sem_post(&full); //取走了一个后,距离满了得个数需要加1
sleep(rand() % 5);
}
}
static void store()
{
int s = 0;
while (1)
{
sem_wait(&full); //每一次存的时候判断是否满了
queue[s] = rand() % 1000 + 1;
printf("store queue[%d]=%d\n", s, queue[s]);
s = (s + 1) % QUEUE_SIZE;
sem_post(&empty);
sleep(rand() % 5);
}
}
int main(void)
{
printf("__STDC__=%d __STDC_VERSION__=%d\n", __STDC__, __STDC_VERSION__);
sem_init(&empty, 0, QUEUE_SIZE);
sem_init(&full, 0, 0);
pthread_t id;
pthread_create(&id, NULL, take, NULL);
sleep(2);
pthread_create(&id, NULL, store, NULL);
while (1)
{
usleep(1000);
}
return 0;
}
rwlock
pthread读写锁把对共享资源的访问者分为读者和写者,读者只对共享资源进行读访问,写者只对共享资源进行写操作。在互斥机制,读者和写者都需要独立独占互斥量以独占共享资源,在读写锁机制下,允许同时有多个读者读访问共享资源,只有写者才需要独占资源。相比互斥机制,读写机制由于允许多个读者同时读访问共享资源,进一步提高了多线程的并发度。
int pthread_rwlock_init(pthread_rwlock_t * rwlock, const pthread_rwlockattr_t * attr);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
自旋锁
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);
由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问,由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?
通常有两种处理方式:
- 一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);
- 还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。
自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。由于这个原因,操作系统的内核经常使用自旋锁。
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。