30.1
问题描述:
我们的第一个问题集中在 main-two-cvs-while.c(有效的解决方案)上。 首先,研究代码。 你认为你了解当你运行程序时会发生什么吗?
解答:
我们首先查看main-two-cvs-while.c的主要函数:
void do_fill(int value) {
// ensure empty before usage
ensure(buffer[fill_ptr] == EMPTY, "error: tried to fill a non-empty buffer");
buffer[fill_ptr] = value;
fill_ptr = (fill_ptr + 1) % max;
num_full++;
}
int do_get() {
int tmp = buffer[use_ptr];
ensure(tmp != EMPTY, "error: tried to get an empty buffer");
buffer[use_ptr] = EMPTY;
use_ptr = (use_ptr + 1) % max;
num_full--;
return tmp;
}
void *producer(void *arg) {
int id = (int) arg;
// make sure each producer produces unique values
int base = id * loops;
int i;
for (i = 0; i < loops; i++) { p0;
Mutex_lock(&m); p1;
while (num_full == max) { p2;
Cond_wait(&empty, &m); p3;
}
do_fill(base + i); p4;
Cond_signal(&fill); p5;
Mutex_unlock(&m); p6;
}
return NULL;
}
void *consumer(void *arg) {
int id = (int) arg;
int tmp = 0;
int consumed_count = 0;
while (tmp != END_OF_STREAM) { c0;
Mutex_lock(&m); c1;
while (num_full == 0) { c2;
Cond_wait(&fill, &m); c3;
}
tmp = do_get(); c4;
Cond_signal(&empty); c5;
Mutex_unlock(&m); c6;
consumed_count++;
}
// return consumer_count-1 because END_OF_STREAM does not count
return (void *) (long long) (consumed_count - 1);
}
可以发现,就是书上254页的生产者/消费者线程函数。
这里,生产者生产loop个数据。生成过程如下:首先获取一把锁,然后如果缓冲区已满,则等待消费者发出的消费信号再往下执行。否则,就往缓冲区中放置生产的数据。然后发出一个信号,通知消费者消费。最后解锁。
消费者消费若干个数据。消费过程如下:首先获取一把锁,然后如果缓冲区中没有数据,则等待一个数据信号。否则,取出缓冲区中的数据。并发出一个信号。然后释放锁,并让自己的消费计数器+1。
这个代码目的是让生产者生产数据到缓冲区中(如果缓冲区未满),消费者从缓冲区中取数据。程序运行时,希望生产者会在仓库满时等待,并且一旦仓库有空间,则立即解除等待放置数据。消费者会在仓库为空的时候等待,并且仓库一旦有数据,则立刻解除等待消费数据。
30.2
问题描述:
指定一个生产者和一个消费者运行,并让生产者产生一些元素。 缓冲区大小从 1 开始,然后增加。随着缓冲区大小增加,程序运行结果如何改变? 当使用不同的缓冲区大小(例如 -m 10),生产者生产不同的产品数量(例如 -l 100), 修改消费者的睡眠字符串(例如 -C 0,0,0,0,0,0,1),full_num 的值如何变化?
解答:
首先对参数选项进行解释:
- -p x表示指定生产者数量为x
- -c x表示指定消费者数量为x
- -m x表示指定buffer大小为x
- -l x表示生产多少
- -v跟踪运行的过程
而在下面的运行结果中,NF表示num_full,缓冲区中数据的数据量。中间是仓库的情况。- - -表示该仓库位没有数据。后面P0列表示生成者0执行到哪一行代码。对应第一题中每一行代码后面的注释。
首先假设生产者生产总数为3,缓冲区大小为1时,输入命令./main-two-cvs-while -l 3 -m 1 -p 1 -c 1 -v
:
缓冲区大小为2时:
缓冲区大小为3时:
可以看到,程序的运行结果基本不变,m的增加,只是让生成者和消费者每次放置数据的位置和消费数据的有所改变。但消费者还是能够在仓库为空时等待,一旦不为空则开始消费。生成者放置数据没有异常,消费者消费数据也没有异常。最终消费者成功地消费了3个数据。NF(full_num,缓冲区数据的数量)的值也是0,1,0,1的交替
而当当将buffer的大小设置为10(即-m 10),生产的总数设置为100(即-l 100),同时设置消费者的睡眠情况为-C 0,0,0,0,0,0,1(也就是在c6处进入睡眠)时,输入命令./main-two-cvs-while -p 1 -c 1 -m 10 -l 100 -C 0,0,0,0,0,0,1 -v
:
可以看到,在这个设置下,消费者会等待仓库满的时候才开始消费。一旦消费一个,生产者就会立刻生成一个让仓库重新变成满。直到最后生成者不生产了,消费者才开始把仓库清空。
30.4
问题描述:
我们来看一些 timings。 对于一个生产者,三个消费者,大小为 1 的共享缓冲区以及每个消费者在 c3 点暂停一秒,您认需要执行多长时间? (./main-two-cvs-while -p 1 -c 3 -m 1 -C 0,0,0,1,0,0,0:0,0,0,1,0,0,0:0,0,0,1,0,0,0 -l 10 -v -t)
解答:
输入命令./main-two-cvs-while -p 1 -c 3 -m 1 -C 0,0,0,1,0,0,0:0,0,0,1,0,0,0:0,0,0,1,0,0,0 -l 10 -v -t
查看结果
下面是消费者先执行的情况:
由题目我们知道,设置了3个消费者,每个消费者在c3的时候进入睡眠,睡眠的时长为1秒,以上面运行出的结果为例,对花费的总时长进行分析,本次运行率先运行的是消费者线程,由于buffer中没有可以取的数据,所以三者在c2的时候进入等待fill信号的状态,之后生产者线程进行数据的填充(“生产”),当buffer中有数据的时候,消费者就可以继续运行了,但每次到达c3都要睡眠1秒钟,可以看到整个过程中,3个消费者总共到达c3的次数为13次,所以总睡眠时长为13秒,由于程序运行本身花费时间相比较13秒来说非常的小,所以程序总共花费时间近似为13秒。
查看结果:
可以看到结果与分析一致,程序总花费时间近似13秒。
如果生产者者线程先执行,那么睡眠时间为 12s,那么程序运行时间应该也近似12秒。
30.8
问题描述:
现在让我们看一下 main-one-cv-while.c。您是否可以假设只有一个生产者, 一个消费者和一个大小为 1 的缓冲区,配置一个睡眠字符串,让代码运行出现问题
解答:
这种生产者/消费者方案存在的问题在于“只使用了一个条件变量”,
但是当消费者数量为1时,消费者每次只能唤醒生产者,避免了这个问题,所以当生产者和消费者的数量都为1时,不会出现问题。
30.9
问题描述:
现在将消费者数量更改为两个。 为生产者消费者配置睡眠字符串,从而使代码运行出现问题。
解答:
可以发现就是书中255页的冲突问题。所以即使不设置睡眠字符串,代码运行也可能出现问题。如:生产者生产后,缓冲区满了,唤醒了两个正在睡眠的消费者中的一个,然后进入睡眠(Mutex_lock),消费者消费后,唤醒另一个消费者,进入睡眠(Mutex_lock),新的消费者线程被唤醒,发现缓冲区为空,进入睡眠(Cond_wait),此时三个线程都进入睡眠,因此程序出现错误。
输入命令./main-one-cv-while -p 1 -c 2 -m 1 -P 0,0,0,0,0,0,1 -l 2 -v -t
查看运行情况:
可以看到,最后三者都进入睡眠
30.10
问题描述:
现在查看 main-two-cvs-if.c。 您是否可以配置一些参数让代码运行出现问题? 再次考虑只有一个消费者的情况,然后再考虑有一个以上消费者的情况。
解答:
由30.8可以得知,一个生产者一个消费者不会出现问题。
两个消费者的情况下,会出现生产者生产完成时,消费者 c1 还没有进入临界区,消费者 c2 在 Cond_wait 处等待,生产者唤醒一个消费者,c1 抢先执行,执行完后缓冲区为空,c2 开始执行,发现缓冲区为空,do_get 执行发生错误。
运行命令./main-two-cvs-if -p 1 -P 0,1,1,1,0,0,1 -c 2 -m 1 -C 0,0,0,1,0,0,1:0,0,0,1,0,0,1 -l 4 -v
查看结果:
可以看到当一个消费者将buffer中的数据消费之后,另一个消费者再次尝试获取数据,将会发现无数据可取,导致断言触发。
30.11
问题描述:
最后查看 main-two-cvs-while-extra-unlock.c。在向缓冲区添加或取出元素时释放锁时会出现什么问题? 给定睡眠字符串来引起这类问题的发生? 会造成什么不好的结果?
解答:
首先查看一下代码:
void *producer(void *arg) {
int id = (int) arg;
// make sure each producer produces unique values
int base = id * loops;
int i;
for (i = 0; i < loops; i++) { p0;
Mutex_lock(&m); p1;
while (num_full == max) { p2;
Cond_wait(&empty, &m); p3;
}
Mutex_unlock(&m);
do_fill(base + i); p4;
Mutex_lock(&m);
Cond_signal(&fill); p5;
Mutex_unlock(&m); p6;
}
return NULL;
}
void *consumer(void *arg) {
int id = (int) arg;
int tmp = 0;
int consumed_count = 0;
while (tmp != END_OF_STREAM) { c0;
Mutex_lock(&m); c1;
while (num_full == 0) { c2;
Cond_wait(&fill, &m); c3;
}
Mutex_unlock(&m);
tmp = do_get(); c4;
Mutex_lock(&m);
Cond_signal(&empty); c5;
Mutex_unlock(&m); c6;
consumed_count++;
}
可以看到,do_get 和 do_fill 分别在消费者和生产者的锁外面,锁相当于没有加,可能导致别的线程插入进来运行,导致该线程再次get()的时候结果无数据可取,导致断言触发!