互斥锁
加锁 -> 阻塞(睡眠等待sleep)-> 解锁。
阻塞时会进行上下文切换,CPU可进行其他工作。
函数原型:
#include
#include
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
// 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
// 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用例程:
//使用互斥量解决多线程抢占资源的问题
#include
#include
#include
#include
#include
char* buf[10]; //字符指针数组 全局变量
int pos; //用于指定上面数组的下标
//1.定义互斥量
pthread_mutex_t mutex;
void *task(void *p)
{
//3.使用互斥量进行加锁
pthread_mutex_lock(&mutex);
buf[pos] = (char *)p;
sleep(1);
pos++;
//4.使用互斥量进行解锁
pthread_mutex_unlock(&mutex);
}
int main(void)
{
//初始化互斥量, 默认属性
pthread_mutex_init(&mutex, NULL);
//启动两个线程 轮流向数组中存储内容
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, task, (void *)"hello");
pthread_create(&tid2, NULL, task, (void *)"world");
//主线程进程等待,并且打印最终的结果
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁互斥量
pthread_mutex_destroy(&mutex);
return 0;
}
自旋锁
加锁 -> 阻塞(循环检测等待running)-> 解锁
自旋锁与互斥量功能一样,唯一不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待直到获得锁,减少了线程从睡眠到唤醒的资源消耗。
自旋锁在用户态使用的比较少,在内核使用的比较多,使用场景:资源的锁被持有的时间短或者说小于2次上下文切换的时间,而又不希望在线程的唤醒上花费太多资源的情况。
自旋锁在用户态的函数接口和互斥量一样,把pthread_mutex_xxx()中mutex换成spin,如:pthread_spin_init()。
读写锁
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因(允许多个线程读但只允许一个线程写)。
适合资源的读操作远多于写操作的情况。
1)多个读者可以同时进行读
2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
函数原型:
#include
// 初始化读写锁
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_trywrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
使用例程:
// 一个使用读写锁来实现 3 个线程读写一段数据是实例。
// 在此示例程序中,共创建了 3 个线程,
// 其中一个线程用来写入数据,两个线程用来读取数据
#include
#include
#include
pthread_rwlock_t rwlock; //读写锁
int num = 1;
//读操作,其他线程允许读操作,却不允许写操作
void *fun1(void *arg)
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
printf("read num first == %d\n", num);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
//读操作,其他线程允许读操作,却不允许写操作
void *fun2(void *arg)
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
printf("read num second == %d\n", num);
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
}
//写操作,其它线程都不允许读或写操作
void *fun3(void *arg)
{
while(1)
{
pthread_rwlock_wrlock(&rwlock);
num++;
printf("write thread first\n");
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
}
int main()
{
pthread_t ptd1, ptd2, ptd3;
pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁
//创建线程
pthread_create(&ptd1, NULL, fun1, NULL);
pthread_create(&ptd2, NULL, fun2, NULL);
pthread_create(&ptd3, NULL, fun3, NULL);
//等待线程结束,回收其资源
pthread_join(ptd1, NULL);
pthread_join(ptd2, NULL);
pthread_join(ptd3, NULL);
pthread_rwlock_destroy(&rwlock);//销毁读写锁
return 0;
}
条件变量
条件变量用来自动阻塞一个线程,直到某特殊情况发生,通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
1)一个线程因 “条件不成立” 而挂起等待
2)另一个线程使 “条件成立”, 并发出信号
函数原型:
#include
#include
#include
#include
int travelercount = 0;
pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER;
void *traveler_arrive(void *name)
{
char *p = (char *)name;
pthread_mutex_lock(&taximutex);
printf ("traveler: %s need a taxi now!\n", p);
travelercount++;
pthread_cond_wait(&taxicond, &taximutex);
pthread_mutex_unlock(&taximutex);
printf ("traveler: %s now got a taxi!\n", p);
pthread_exit(NULL);
}
void *taxi_arrive(void *name)
{
char *p = (char *)name;
printf ("Taxi: %s arrives.\n", p);
for(;;)
{
if(travelercount)
{
pthread_cond_signal(&taxicond);
travelercount--;
break;
}
}
pthread_exit(NULL);
}
int main (int argc, char **argv)
{
pthread_t thread;
pthread_attr_t threadattr;
pthread_attr_init(&threadattr);
pthread_create(&thread, &threadattr, taxi_arrive,"taxi1");
sleep(1);
pthread_create(&thread, &threadattr, traveler_arrive,"traveler");
sleep(3);
pthread_create(&thread, &threadattr, taxi_arrive,"taxi2");
sleep(4);
return 0;
}
信号量
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
信号量适合用于上锁时间长的情况。
当数量等于1 时称为二值信号量,当数量大于1时称为计数信号量。
当信号量值大于 0 时,则可以访问,否则将阻塞进入等待队列。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
函数原型:
#include
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 信号量 P 操作(减 1)
int sem_wait(sem_t *sem);
// 以非阻塞的方式来对信号量进行减 1 操作
int sem_trywait(sem_t *sem);
// 信号量 V 操作(加 1)
int sem_post(sem_t *sem);
// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
// 销毁信号量
int sem_destroy(sem_t *sem);
同步例程:
// 信号量用于同步实例
#include
#include
#include
#include
sem_t sem_g,sem_p; //定义两个信号量
char ch = 'a';
void *pthread_g(void *arg) //此线程改变字符ch的值
{
while(1)
{
sem_wait(&sem_g);
ch++;
sleep(1);
sem_post(&sem_p);
}
}
void *pthread_p(void *arg) //此线程打印ch的值
{
while(1)
{
sem_wait(&sem_p);
printf("%c",ch);
sem_post(&sem_g);
}
}
int main(int argc, char *argv[])
{
pthread_t tid1,tid2;
sem_init(&sem_g, 0, 0); // 初始化信号量为0
sem_init(&sem_p, 0, 1); // 初始化信号量为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;
}
互斥例程:
// 信号量用于互斥实例
#include
#include
#include
#include
sem_t sem; //信号量
void printer(char *str)
{
sem_wait(&sem);//减一,p操作
while(*str) // 输出字符串(如果不用互斥,此处可能会被其他线程入侵)
{
putchar(*str);
str++;
sleep(1);
}
sem_post(&sem);//加一,v操作
}
void *thread_fun1(void *arg)
{
char *str1 = "hello";
printer(str1);
}
void *thread_fun2(void *arg)
{
char *str2 = "world";
printer(str2);
}
int main(void)
{
pthread_t tid1, tid2;
sem_init(&sem, 0, 1); //初始化信号量,初始值为 1
//创建 2 个线程
pthread_create(&tid1, NULL, thread_fun1, NULL);
pthread_create(&tid2, NULL, thread_fun2, NULL);
//等待线程结束,回收其资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&sem); //销毁信号量
return 0;
}
避免死锁:
1.使用嵌套的锁时必须保证以相同的顺序获取锁,最好记录下锁的顺序以便其他人也能照此顺序使用。
2.防止发生饥饿,要试问该情况若不发生,是否一直等待下去?
3.不要重复请求同一个锁
4.加锁方案应该应当力求设计的简单
锁使用场景:
互斥体包含计数信号量和互斥锁。除非互斥锁某个条件约束妨碍你使用,否则优先考虑使用互斥锁而不是信号量,一般只有很底层的代码需要用到信号量。
自旋锁:中断上下文加锁、低开销加锁、短期加锁
互斥体:长期加锁、持有锁需要睡眠