-
互斥锁
加锁 -> 阻塞(睡眠等待sleep)-> 解锁。
阻塞时会进行上下文切换,CPU可进行其他工作。
函数原型:#include <pthread.h> #include <time.h> // 初始化一个互斥锁。 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 <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> 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 <pthread.h> // 初始化读写锁 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 <stdio.h> #include <unistd.h> #include <pthread.h> 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 <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> 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 <semaphore.h> // 初始化信号量 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 <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); 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 <stdio.h> #include <pthread.h> #include <unistd.h> #include <semaphore.h> 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.加锁方案应该应当力求设计的简单
锁使用场景:
互斥体包含计数信号量和互斥锁。除非互斥锁某个条件约束妨碍你使用,否则优先考虑使用互斥锁而不是信号量,一般只有很底层的代码需要用到信号量。
自旋锁:中断上下文加锁、低开销加锁、短期加锁
互斥体:长期加锁、持有锁需要睡眠