每个线程都有自己的栈区和寄存器(CPU中的资源由内核管理)
保护共享数据(多个线程共同访问的资源也就是内存)
同一个时间点多个线程有的写有的读
例子:有个银行卡里面有200,分别绑定我对象的微信和我的支付宝,在同一时间点去消费,我不可能取出400块钱,肯定有个先后,他去完我才能取钱.
线程同步,线性同步,排队访问.
多线程共同访问资源,
分时复用CPU时间片,从物理内存读到的加载到CPU寄存器中,有一二三级缓存.,时间片用完了,
比如说数数数到1 2 3 4 5,数到5的时候没来得及写会物理内存,时间片用完了,5还是存到了CPU寄存器中没来得。物理内存中的数是4;所以下次数数还是从4开始,4 5 6 7 ;时间片用完了下次从7数。
加锁调用同步函数,其他线程阻塞这把锁上,当此线程时间片用完后,由于其他线程被阻塞在锁上,所以此线程又抢到了时间片。
例子:在车站上厕所,进去得锁门,外面的人得等着。
1.互斥锁
只能被一个线程使用,
pthread_mutex_t mutex;// 初始化互斥锁/* restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址,其他指针是不行的*/int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);// 释放互斥锁资源int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex: 互斥锁变量的地址
attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
2.条件变量
2.1条件变量函数
严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:
- 假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
 - 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
 
一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t,这样就可以定义一个条件变量类型的变量了:
pthread_cond_t cond;//初始化int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t * restrict attr);//销毁释放资源int pthread_cond_destroy(pthread_cond_t *cond);
参数:
- cond:条件变量
 - atr:条件变量属性,一般使用默认属性,指定 为NULL;
 
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:
- 在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,避免了死锁
 - 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区。
 
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示struct timespec {time_t tv_sec; /* Seconds */long tv_nsec; /* Nanoseconds [0 .. 999999999] */};// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
/* 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞*/int pthread_cond_signal(pthread_cond_t *cond);// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞int pthread_cond_broadcast(pthread_cond_t *cond);
调用上面两个函数中的任意一个,都可以换线被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程。
2.2生产者消费者模型
5个生产者,5个消费者,一个任务队列
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <pthread.h>// 链表的节点struct Node{int number;struct Node* next;};// 定义条件变量, 控制消费者线程pthread_cond_t cond;// 互斥锁变量pthread_mutex_t mutex;// 指向头结点的指针struct Node * head = NULL;// 生产者的回调函数void* producer(void* arg){// 一直生产while(1){pthread_mutex_lock(&mutex);// 创建一个链表的新节点struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));// 节点初始化pnew->number = rand() % 1000;// 节点的连接, 添加到链表的头部, 新节点就新的头结点pnew->next = head;// head指针前移head = pnew;printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());pthread_mutex_unlock(&mutex);// 生产了任务, 通知消费者消费pthread_cond_broadcast(&cond);// 生产慢一点sleep(rand() % 3);}return NULL;}// 消费者的回调函数void* consumer(void* arg){while(1){pthread_mutex_lock(&mutex);/*不能用if(head==NULL) 有bug为什么在第7行使用if 有bug:当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);也就是阻塞在代码的第9行当生产者生产了1个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程- 有一个消费者线程通过 pthread_cond_wait()加锁成功, 其余没有加锁成功的线程继续阻塞- 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁- 没有加锁成功的线程解除阻塞继续抢这把锁, 另外一个子线程加锁成功- 但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误解决方案:- 需要循环的对链表是否为空进行判断, 需要将if 该成 while*/// 一直消费, 删除链表中的一个节点while(head == NULL){// 任务队列, 也就是链表中已经没有节点可以消费了// 消费者线程需要阻塞// 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开// 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁// 这函数会自动将线程拥有的锁解开pthread_cond_wait(&cond, &mutex);// 当消费者线程解除阻塞之后, 会自动将这把锁锁上// 这时候当前这个线程又重新拥有了这把互斥锁}// 取出链表的头结点, 将其删除struct Node* pnode = head;printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());head = pnode->next;free(pnode);pthread_mutex_unlock(&mutex);sleep(rand() % 3);}return NULL;}int main(){// 初始化条件变量pthread_cond_init(&cond, NULL);pthread_mutex_init(&mutex, NULL);// 创建5个生产者, 5个消费者pthread_t ptid[5];pthread_t ctid[5];for(int i=0; i<5; ++i){pthread_create(&ptid[i], NULL, producer, NULL);}for(int i=0; i<5; ++i){pthread_create(&ctid[i], NULL, consumer, NULL);}// 释放资源for(int i=0; i<5; ++i){// 阻塞等待子线程退出pthread_join(ptid[i], NULL);}for(int i=0; i<5; ++i){pthread_join(ctid[i], NULL);}// 销毁条件变量pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);return 0;}
信号量
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t 对应的头文件为 
#include <semaphore.h>sem_t sem;
#include <semaphore.h>// 初始化信号量/信号灯int sem_init(sem_t *sem, int pshared, unsigned int value);// 资源释放, 线程销毁之后调用这个函数即可// 参数 sem 就是 sem_init() 的第一个参数int sem_destroy(sem_t *sem);
参数
- sem:信号量变量地址pshared:
 - pshared:
- 0:线程同步
 - 非 0:进程同步
 
 - value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
 
// 参数 sem 就是 sem_init() 的第一个参数// 函数被调用sem中的资源就会被消耗1个, 资源数-1int sem_wait(sem_t *sem);
当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。
// 参数 sem 就是 sem_init() 的第一个参数// 函数被调用sem中的资源就会被消耗1个, 资源数-1int sem_trywait(sem_t *sem);
当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示struct timespec {time_t tv_sec; /* Seconds */long tv_nsec; /* Nanoseconds [0 .. 999999999] */};// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,//在阻塞abs_timeout对应的时长之后,解除阻塞。// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。
// 调用该函数给sem中的资源数+1int sem_post(sem_t *sem);
调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中// sval是一个传出参数int sem_getvalue(sem_t *sem, int *sval);
通过这个函数可以查看 sem 中现在拥有的资源个数,通过第二个参数 sval 将数据传出,也就是说第二个参数的作用和返回值是一样的。
生产消费者模型
由于生产者和消费者是两类线程,并且在还没有生成之前是不能进行消费的,在使用信号量处理这类问题的时候可以定义两个信号量,分别用于记录生产者和消费者线程拥有的总资源数。
// 生产者线程sem_t psem;// 消费者线程sem_t csem;// 信号量初始化sem_init(&psem, 0, 5); // 5个生产者可以同时生产sem_init(&csem, 0, 0); // 消费者线程没有资源, 因此不能消费// 生产者线程// 在生产之前, 从信号量中取出一个资源sem_wait(&psem);// 生产者商品代码, 有商品了, 放到任务队列/*..................*/// 通知消费者消费,给消费者信号量添加资源,让消费者解除阻塞sem_post(&csem);////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 消费者线程// 消费者需要等待生产, 默认启动之后应该阻塞sem_wait(&csem);// 开始消费/*..................*/// 消费完成, 通过生产者生产,给生产者信号量添加资源sem_post(&psem);
通过上面的代码可以知道,初始化信号量的时候没有消费者分配资源,消费者线程启动之后由于没有资源自然就被阻塞了,等生产者生产出产品之后,再给消费者分配资源,这样二者就可以配合着完成生产和消费流程了。
信号量的使用
场景描述:使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。
1. 总资源数为1
如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。
