一个进程中可以有多个线程,这些线程共享进程的资源,但当多个线程访问同一个资源时,在并不能保证操作是原子的情况下,就会产生冲突而使数据最终的结果不准确,像上次我们提到的将一个数进行加1操作需要三步:将数据从内存中取出;将数据加1;再将数据放回内存中,当多个线程并发执行这一操作时,有可能一个线程刚将数据从内存中取出就被临时切换出去了,这时别的线程进行加1操作,而当被切出去的线程重新回来继续执行加1操作时,这个数据已经改变了但是它拿到的还是原来的值,再进行加1放回内存时,结果就和我们所期望达到的不一样了。
再有一个栗子就是接下来要谈到的生产者与消费者,生产者生产产品,消费者消费产品,它们之间会访问到一个共同的临界资源那就是产品,既然是临界资源,那就不得不提到互斥了,当生产者正在生产产品的时候消费者不能中途打断取走产品,因此二者之间是有一个互斥的关系,需要有一个互斥量来维护,下面可以举个栗子:
可以用链表来作为二者“交易”的场所,生产者将生产出来的产品放入链表,而消费者从链表中取走所需要的产品:
首先要创建出链表的结点并有初始化;
生产者要向链表中放入数据,实现一个函数push_node,用头插的方式插入数据;而消费者要从链表中取出数据,实现一个函数pop_node,用尾取的方式取走数据;
因为二者的互斥关系,也为了观察方便两个线程函数都需要打印出数据会对输出设备产生访问冲突,需要加入互斥锁:
在上面的程序栗子中,生产者每隔一秒向链表头部插入一个随机数据,而消费者每隔两秒从链表的尾部取走数据,运行程序可得如下结果:
从上面的结果中不难看出,消费者取走的是链表尾部的结点数据,也就是按照“先进先出(FIFO)”的模式;而如果将生产者与消费者之间的时间颠倒一下,也就是生产者每隔一秒生产数据,而消费者每隔两秒取一个数据,则结果就会如下:
因此并不难知道,虽然加入了互斥量解决了线程之间访问临界资源的冲突问题,但似乎还并不是那么高效,也就是如果生产者生产速度比消费者消费产品的速度要快,则会有产品剩余;如果消费者比生产者生产产品的速度要快,则消费者会拿不到准确有效的数据;
因此,最好有一种解决办法,可以让消费者跟着生产者的步伐,生产者每生产出了一件产品,就通知消费者过来取数据,这样就提高了效率实现了线程之间的顺序性,也就是同步,可以加入条件变量的控制:
之所以将pop_node的结果判断放在consumer线程函数的一个while循环里,是因为有可能多次获取链尾数据都为无效值,则消费者就需要一直等待;
运行程序:
观察结果就如预期的那样,生产者和消费者之间有一个有序的过程,可以提高效率;
===========================================================================================
上面提到的栗子中,只有一个生产者和一个消费者,它们之间是同步与互斥的关系,那如果是多个生产者和多个消费者呢?
当一件产品由一个生产者生产时,其他的生产者就不能插手干预;同样,当一件产品被一个消费者给消费了时,别的消费者就不能再消费这件产品了;
因此,当有多个生产者和多个消费者时,生产者和消费者之间的关系是同步与互斥的,而生产者和生产者之间以及消费者和消费者之间的关系是互斥的:
栗子时间:
创建出多个生产者和多个消费者,生成者之间执行同一份线程函数,其实就是对同一个链表进行操作,而多个消费者之间也是执行同一个线程函数,从同一个链表中取函数,只是因为它们之间加入了互斥锁,因此彼此之间的操作都是互斥的,保证了操作的原子性,既不会有数据丢失,也不会有数据重复,运行程序:
从执行结果可以看出每一份数据都是独立有效的,并且生产者和消费者交互对链表进行操作,完成了线程之间的同步与互斥。
===========================================================================================
总结:
- 线程之间共享同一个进程的资源,因此也会对临界资源的访问产生冲突,需要加入互斥量来保证每个线程对临界资源操作的原子性;
- 线程间的互斥保证了对临界资源操作的原子性,但线程间的同步可以提高效率,使线程间的并发执行变得有序;
- 互斥锁和条件变量可以完成线程间的同步与互斥,使用互斥锁时需要申请,申请不到则线程挂起等待,使用完成释放锁,再重新竞争锁资源;使用条件变量时调用wait挂起等待的一方需要另一方来使其满足条件将其唤醒。
《完》