一、互斥锁(同步)
在多任务操作系统中,同时运行的多个工作可能都须要应用同一种资源。这个过程有点相似于,公司部门里,我在应用着打印机打印货色的同时(还没有打印完),他人刚好也在此刻应用打印机打印货色,如果不做任何解决的话,打印进去的货色必定是错乱的。
在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简略的加锁的办法来管制对共享资源的拜访,互斥锁只有两种状态, 即上锁 (lock) 和解锁(unlock)。
【互斥锁的特点】:
- 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或 pthread 函数库)保障了如果一个线程锁定了一个互斥量,没有其余线程在同一时间能够胜利锁定这个互斥量;
- 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其余线程能够锁定这个互斥量;
- 非忙碌期待:如果一个线程曾经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何 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);
【Demo】(阻塞模式):
// 应用互斥量解决多线程抢占资源的问题 #include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>#include <string.h> char* buf[5]; // 字符指针数组 全局变量 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){//2. 初始化互斥量, 默认属性 pthread_mutex_init(&mutex, NULL); //1. 启动一个线程 向数组中存储内容 pthread_t tid, tid2; pthread_create(&tid, NULL, task, (void *)"zhangfei"); pthread_create(&tid2, NULL, task, (void *)"guanyu"); //2. 主线程过程期待, 并且打印最终的后果 pthread_join(tid, NULL); pthread_join(tid2, NULL); //5. 销毁互斥量 pthread_mutex_destroy(&mutex); int i = 0; printf("字符指针数组中的内容是:"); for(i = 0; i < pos; ++i) {printf("%s", buf[i]); } printf("n"); return 0;}
【Demo】(非阻塞模式):
#include <stdio.h>#include <pthread.h>#include <time.h>#include <string.h> int main (void){int err; struct timespec tout; struct tm *tmp; char buf[64]; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock (&lock); printf ("mutex is lockedn"); clock_gettime (CLOCK_REALTIME, &tout); tmp = localtime (&tout.tv_sec); strftime (buf, sizeof (buf), "%r", tmp); printf ("current time is %sn", buf); tout.tv_sec += 10; err = pthread_mutex_timedlock (&lock, &tout); clock_gettime (CLOCK_REALTIME, &tout); tmp = localtime (&tout.tv_sec); strftime (buf, sizeof (buf), "%r", tmp); printf ("the time is now %sn", buf); if (err == 0) printf ("mutex locked againn"); else printf ("can`t lock mutex again:%sn", strerror (err)); return 0;}
**C/C++ Linux 后盾服务器架构师学习地址:https://ke.qq.com/course/4177…
(材料包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)**
二、条件变量(同步)
与互斥锁不同,条件变量是用来期待而不是用来上锁的。条件变量用来主动阻塞一个线程,直 到某非凡状况产生为止。通常条件变量和互斥锁同时应用。
条件变量使咱们能够睡眠期待某种条件呈现。条件变量是利用线程间共享的全局变量进行同步 的一种机制,次要包含两个动作:
一个线程期待 ” 条件变量的条件成立 ” 而挂起;
另一个线程使“条件成立”(给出条件成立信号)。
【原理】:
条件的检测是在互斥锁的爱护下进行的。线程在扭转条件状态之前必须首先锁住互斥量。如果一个条件为假,一个线程主动阻塞,并开释期待状态扭转的互斥锁。如果另一个线程扭转了条件,它发信号给关联的条件变量,唤醒一个或多个期待它的线程,从新取得互斥锁,从新评估条件。如果两过程共享可读写的内存,条件变量 能够被用来实现这两过程间的线程同步。
【条件变量的操作流程如下】:
- 初始化:init()或者 pthread_cond_tcond=PTHREAD_COND_INITIALIER;属性置为 NULL;
- 期待条件成立:pthread_wait,pthread_timewait.wait()开释锁, 并阻塞期待条件变量为真 timewait()设置等待时间, 仍未 signal, 返回 ETIMEOUT(加锁保障只有一个线程 wait);
- 激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有期待线程)
- 革除条件变量:destroy; 无线程期待, 否则返回 EBUSY 革除条件变量:destroy; 无线程期待, 否则返回 EBUSY
#include <pthread.h>// 初始化条件变量 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);// 阻塞期待 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);// 超时期待 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex, const timespec *abstime);// 解除所有线程的阻塞 int pthread_cond_destroy(pthread_cond_t *cond);// 至多唤醒一个期待该条件的线程 int pthread_cond_signal(pthread_cond_t *cond);// 唤醒期待该条件的所有线程 int pthread_cond_broadcast(pthread_cond_t *cond);
1、线程的条件变量实例 1
Jack 开着一辆出租车来到一个站点停车,看见没人就走了。过段时间,Susan 来到站点筹备乘车,然而没有来,于是就等着。过了一会 Mike 开着车来到了这个站点,Sunsan 就上了 Mike 的车走了。如图所示:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; void *traveler_arrive(void *name) {char *p = (char *)name; printf ("Travelr: %s need a taxi now!n", p); // 加锁,把信号量退出队列,开释信号量 pthread_mutex_lock(&taximutex); 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); // 给线程或者条件发信号,肯定要在扭转条件状态后再给线程发信号 pthread_cond_signal(&taxicond); pthread_exit(NULL); } int main (int argc, char **argv) {char *name; pthread_t thread; pthread_attr_t threadattr; // 线程属性 pthread_attr_init(&threadattr); // 线程属性初始化 // 创立三个线程 name = "Jack"; pthread_create(&thread, &threadattr, taxi_arrive, (void *)name); sleep(1); name = "Susan"; pthread_create(&thread, &threadattr, traveler_arrive, (void *)name); sleep(1); name = "Mike"; pthread_create(&thread, &threadattr, taxi_arrive, (void *)name); sleep(1); return 0; }
2、线程的条件变量实例 2
Jack 开着一辆出租车来到一个站点停车,看见没人就等着。过段时间,Susan 来到站点筹备乘车看见了 Jack 的出租车,于是就下来了。过了一会 Mike 开着车来到了这个站点,看见没人救等着。如图所示:
#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){char *name; pthread_t thread; pthread_attr_t threadattr; pthread_attr_init(&threadattr); name = "Jack"; pthread_create(&thread, &threadattr, taxi_arrive, name); sleep(1); name = "Susan"; pthread_create(&thread, &threadattr, traveler_arrive, name); sleep(3); name = "Mike"; pthread_create(&thread, &threadattr, taxi_arrive, name); sleep(4); return 0;}
**C/C++ Linux 后盾服务器架构师学习地址:https://ke.qq.com/course/4177…
(材料包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)**
3、虚伪唤醒(spurious wakeup)
虚伪唤醒 (spurious wakeup) 在采纳条件期待时:
while(条件不满足){condition_wait(cond, mutex); } // 而不是: If(条件不满足){Condition_wait(cond,mutex); }
这是因为可能会存在虚伪唤醒”spurious wakeup”的状况。
也就是说,即便没有线程调用 condition_signal, 原先调用 condition_wait 的函数也可能会返回。此时线程被唤醒了,然而条件并不满足,这个时候如果不对条件进行查看而往下执行,就可能会导致后续的解决呈现谬误。
虚伪唤醒在 linux 的多处理器零碎中 / 在程序接管到信号时可能回产生。在 Windows 零碎和 JAVA 虚拟机上也存在。在零碎设计时应该能够防止虚伪唤醒,然而这会影响条件变量的执行效率,而既然通过 while 循环就能防止虚伪唤醒造成的谬误,因而程序的逻辑就变成了 while 循环的状况。
四、读写锁(同步)
读写锁与互斥量相似,不过读写锁容许更改的并行性,也叫共享互斥锁。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程能够对其加锁。读写锁能够有 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);
【Demo】:
// 一个应用读写锁来实现 4 个线程读写一段数据是实例。// 在此示例程序中,共创立了 4 个线程,// 其中两个线程用来写入数据,两个线程用来读取数据 #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 == %dn", num); pthread_rwlock_unlock(&rwlock); sleep(1); }} // 读操作,其余线程容许读操作,却不容许写操作 void *fun2(void *arg){while(1) {pthread_rwlock_rdlock(&rwlock); printf("read num second == %dn", num); pthread_rwlock_unlock(&rwlock); sleep(2); }} // 写操作,其它线程都不容许读或写操作 void *fun3(void *arg){while(1) {pthread_rwlock_wrlock(&rwlock); num++; printf("write thread firstn"); pthread_rwlock_unlock(&rwlock); sleep(2); }} // 写操作,其它线程都不容许读或写操作 void *fun4(void *arg){while(1) {pthread_rwlock_wrlock(&rwlock); num++; printf("write thread secondn"); pthread_rwlock_unlock(&rwlock); sleep(1); } } int main() { pthread_t ptd1, ptd2, ptd3, ptd4; pthread_rwlock_init(&rwlock, NULL);// 初始化一个读写锁 // 创立线程 pthread_create(&ptd1, NULL, fun1, NULL); pthread_create(&ptd2, NULL, fun2, NULL); pthread_create(&ptd3, NULL, fun3, NULL); pthread_create(&ptd4, NULL, fun4, NULL); // 期待线程完结,回收其资源 pthread_join(ptd1, NULL); pthread_join(ptd2, NULL); pthread_join(ptd3, NULL); pthread_join(ptd4, NULL); pthread_rwlock_destroy(&rwlock);// 销毁读写锁 return 0; }
五、自旋锁(同步)
自旋锁与互斥量性能一样,惟一一点不同的就是互斥量阻塞后休眠让出 cpu,而自旋锁阻塞后不会让出 cpu,会始终忙期待,直到失去锁。
自旋锁在用户态应用的比拟少,在内核应用的比拟多!自旋锁的应用场景:锁的持有工夫比拟短,或者说小于 2 次上下文切换的工夫。
自旋锁在用户态的函数接口和互斥量一样,把 pthread_mutex_xxx()中 mutex 换成 spin,如:pthread_spin_init()。
六、信号量(同步与互斥)
信号量宽泛用于过程或线程间的同步和互斥,信号量实质上是一个非负的整数计数器,它被用来管制对公共资源的拜访。
编程时可依据操作信号量值的后果判断是否对公共资源具备拜访的权限,当信号量值大于 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); fflush(stdout); 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;}
**C/C++ Linux 后盾服务器架构师学习地址:https://ke.qq.com/course/4177…
(材料包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)**
【信号量用于互斥】:
// 信号量用于互斥实例 #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); fflush(stdout); str++; sleep(1); } printf("n"); 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;}