每个线程都有自己的栈区和寄存器(CPU中的资源由内核管理)
保护共享数据(多个线程共同访问的资源也就是内存)
同一个时间点多个线程有的写有的读

例子:有个银行卡里面有200,分别绑定我对象的微信和我的支付宝,在同一时间点去消费,我不可能取出400块钱,肯定有个先后,他去完我才能取钱.

线程同步,线性同步,排队访问.

多线程共同访问资源,
分时复用CPU时间片,从物理内存读到的加载到CPU寄存器中,有一二三级缓存.,时间片用完了,
比如说数数数到1 2 3 4 5,数到5的时候没来得及写会物理内存,时间片用完了,5还是存到了CPU寄存器中没来得。物理内存中的数是4;所以下次数数还是从4开始,4 5 6 7 ;时间片用完了下次从7数。

加锁调用同步函数,其他线程阻塞这把锁上,当此线程时间片用完后,由于其他线程被阻塞在锁上,所以此线程又抢到了时间片。
例子:在车站上厕所,进去得锁门,外面的人得等着。

1.互斥锁

只能被一个线程使用,

  1. pthread_mutex_t mutex;
  2. // 初始化互斥锁
  3. /* restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址,
  4. 其他指针是不行的*/
  5. int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
  6. // 释放互斥锁资源
  7. int pthread_mutex_destroy(pthread_mutex_t *mutex);

mutex: 互斥锁变量的地址
attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL

2.条件变量

2.1条件变量函数

严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:

  • 假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
  • 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。

一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t,这样就可以定义一个条件变量类型的变量了:

  1. pthread_cond_t cond;
  2. //初始化
  3. int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t * restrict attr);
  4. //销毁释放资源
  5. int pthread_cond_destroy(pthread_cond_t *cond);

参数:

  • cond:条件变量
  • atr:条件变量属性,一般使用默认属性,指定 为NULL;
  1. // 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
  2. int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:

  • 在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,避免了死锁
  • 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区。
  1. // 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
  2. struct timespec {
  3. time_t tv_sec; /* Seconds */
  4. long tv_nsec; /* Nanoseconds [0 .. 999999999] */
  5. };
  6. // 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
  7. int pthread_cond_timedwait(pthread_cond_t *restrict cond,
  8. pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
  1. /* 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞*/
  2. int pthread_cond_signal(pthread_cond_t *cond);
  3. // 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
  4. int pthread_cond_broadcast(pthread_cond_t *cond);

调用上面两个函数中的任意一个,都可以换线被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程。

2.2生产者消费者模型

5个生产者,5个消费者,一个任务队列

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. #include <pthread.h>
  6. // 链表的节点
  7. struct Node
  8. {
  9. int number;
  10. struct Node* next;
  11. };
  12. // 定义条件变量, 控制消费者线程
  13. pthread_cond_t cond;
  14. // 互斥锁变量
  15. pthread_mutex_t mutex;
  16. // 指向头结点的指针
  17. struct Node * head = NULL;
  18. // 生产者的回调函数
  19. void* producer(void* arg)
  20. {
  21. // 一直生产
  22. while(1)
  23. {
  24. pthread_mutex_lock(&mutex);
  25. // 创建一个链表的新节点
  26. struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
  27. // 节点初始化
  28. pnew->number = rand() % 1000;
  29. // 节点的连接, 添加到链表的头部, 新节点就新的头结点
  30. pnew->next = head;
  31. // head指针前移
  32. head = pnew;
  33. printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
  34. pthread_mutex_unlock(&mutex);
  35. // 生产了任务, 通知消费者消费
  36. pthread_cond_broadcast(&cond);
  37. // 生产慢一点
  38. sleep(rand() % 3);
  39. }
  40. return NULL;
  41. }
  42. // 消费者的回调函数
  43. void* consumer(void* arg)
  44. {
  45. while(1)
  46. {
  47. pthread_mutex_lock(&mutex);
  48. /*不能用if(head==NULL) 有bug为什么在第7行使用if 有bug:
  49. 当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);
  50. 也就是阻塞在代码的第9行
  51. 当生产者生产了1个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程
  52. - 有一个消费者线程通过 pthread_cond_wait()加锁成功, 其余没有加锁成功的线程继续阻塞
  53. - 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁
  54. - 没有加锁成功的线程解除阻塞继续抢这把锁, 另外一个子线程加锁成功
  55. - 但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误
  56. 解决方案:
  57. - 需要循环的对链表是否为空进行判断, 需要将if 该成 while
  58. */
  59. // 一直消费, 删除链表中的一个节点
  60. while(head == NULL)
  61. {
  62. // 任务队列, 也就是链表中已经没有节点可以消费了
  63. // 消费者线程需要阻塞
  64. // 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
  65. // 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
  66. // 这函数会自动将线程拥有的锁解开
  67. pthread_cond_wait(&cond, &mutex);
  68. // 当消费者线程解除阻塞之后, 会自动将这把锁锁上
  69. // 这时候当前这个线程又重新拥有了这把互斥锁
  70. }
  71. // 取出链表的头结点, 将其删除
  72. struct Node* pnode = head;
  73. printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
  74. head = pnode->next;
  75. free(pnode);
  76. pthread_mutex_unlock(&mutex);
  77. sleep(rand() % 3);
  78. }
  79. return NULL;
  80. }
  81. int main()
  82. {
  83. // 初始化条件变量
  84. pthread_cond_init(&cond, NULL);
  85. pthread_mutex_init(&mutex, NULL);
  86. // 创建5个生产者, 5个消费者
  87. pthread_t ptid[5];
  88. pthread_t ctid[5];
  89. for(int i=0; i<5; ++i)
  90. {
  91. pthread_create(&ptid[i], NULL, producer, NULL);
  92. }
  93. for(int i=0; i<5; ++i)
  94. {
  95. pthread_create(&ctid[i], NULL, consumer, NULL);
  96. }
  97. // 释放资源
  98. for(int i=0; i<5; ++i)
  99. {
  100. // 阻塞等待子线程退出
  101. pthread_join(ptid[i], NULL);
  102. }
  103. for(int i=0; i<5; ++i)
  104. {
  105. pthread_join(ctid[i], NULL);
  106. }
  107. // 销毁条件变量
  108. pthread_cond_destroy(&cond);
  109. pthread_mutex_destroy(&mutex);
  110. return 0;
  111. }

信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t 对应的头文件为

  1. #include <semaphore.h>
  2. sem_t sem;
  1. #include <semaphore.h>
  2. // 初始化信号量/信号灯
  3. int sem_init(sem_t *sem, int pshared, unsigned int value);
  4. // 资源释放, 线程销毁之后调用这个函数即可
  5. // 参数 sem 就是 sem_init() 的第一个参数
  6. int sem_destroy(sem_t *sem);

参数

  • sem:信号量变量地址pshared:
  • pshared:
    • 0:线程同步
    • 非 0:进程同步
  • value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
  1. // 参数 sem 就是 sem_init() 的第一个参数
  2. // 函数被调用sem中的资源就会被消耗1个, 资源数-1
  3. int sem_wait(sem_t *sem);

当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。

  1. // 参数 sem 就是 sem_init() 的第一个参数
  2. // 函数被调用sem中的资源就会被消耗1个, 资源数-1
  3. int sem_trywait(sem_t *sem);

当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。

  1. // 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
  2. struct timespec {
  3. time_t tv_sec; /* Seconds */
  4. long tv_nsec; /* Nanoseconds [0 .. 999999999] */
  5. };
  6. // 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,
  7. //在阻塞abs_timeout对应的时长之后,解除阻塞。
  8. // abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
  9. int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。

  1. // 调用该函数给sem中的资源数+1
  2. int sem_post(sem_t *sem);

调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。

  1. // 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
  2. // sval是一个传出参数
  3. int sem_getvalue(sem_t *sem, int *sval);

通过这个函数可以查看 sem 中现在拥有的资源个数,通过第二个参数 sval 将数据传出,也就是说第二个参数的作用和返回值是一样的。

生产消费者模型

由于生产者和消费者是两类线程,并且在还没有生成之前是不能进行消费的,在使用信号量处理这类问题的时候可以定义两个信号量,分别用于记录生产者和消费者线程拥有的总资源数。

  1. // 生产者线程
  2. sem_t psem;
  3. // 消费者线程
  4. sem_t csem;
  5. // 信号量初始化
  6. sem_init(&psem, 0, 5); // 5个生产者可以同时生产
  7. sem_init(&csem, 0, 0); // 消费者线程没有资源, 因此不能消费
  8. // 生产者线程
  9. // 在生产之前, 从信号量中取出一个资源
  10. sem_wait(&psem);
  11. // 生产者商品代码, 有商品了, 放到任务队列
  12. /*......
  13. ......
  14. ......*/
  15. // 通知消费者消费,给消费者信号量添加资源,让消费者解除阻塞
  16. sem_post(&csem);
  17. ////////////////////////////////////////////////////////
  18. ////////////////////////////////////////////////////////
  19. // 消费者线程
  20. // 消费者需要等待生产, 默认启动之后应该阻塞
  21. sem_wait(&csem);
  22. // 开始消费
  23. /*......
  24. ......
  25. ......*/
  26. // 消费完成, 通过生产者生产,给生产者信号量添加资源
  27. sem_post(&psem);

通过上面的代码可以知道,初始化信号量的时候没有消费者分配资源,消费者线程启动之后由于没有资源自然就被阻塞了,等生产者生产出产品之后,再给消费者分配资源,这样二者就可以配合着完成生产和消费流程了。

信号量的使用

场景描述:使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。

1. 总资源数为1

如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。