第三次作业
第26章
本章题目代码包含了一个模拟器x86.py。x86.py作为模拟器,运行包括loop.s在内的汇编代码,一些重要的参数如下:
- -p:指定程序
- -t:线程数
- -i:指定每执行i个指令产生一次中断
- -M:追踪内存
- -R:追踪特定寄存器值
- -C:追踪条件码的值
- -r:中断随机
1.题1
题目中提到的loop.s程序如下。
.main
.top
sub $1,%dx
test $0,%dx
jgte .top
halt
这段程序实现了一个简单循环的单线程。dx寄存器中的值为循环计数,每次循环dx的值-1,与0比较,如果大于等于0跳转并继续进行循环,dx值为0进入循环,即最后一次循环中dx=-1,循环结束。
给定的参数为:./x86.y -p loop.s -t 1 -i 100 -R dx,这表示执行loop.s这个单线程,100条指令产生一个中断(一个单进程,不会发生线程切换),追踪寄存器dx。循环一次为3条指令,因此每三条指令,dx的值-1,当dx值为-1时,循环结束。运行程序,得到结果如下:
dx Thread 0
?
? 1000 sub $1,%dx
? 1001 test $0,%dx
? 1002 jgte .top
? 1003 halt
只执行了4条指令就结束了,dx的初始值应该为0,进入循环后减为-1,直接退出了循环。使用-c标志检查,dx的值符合预期。
dx Thread 0
0
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 1003 halt
2.题2
同样运行loop.s,此次的参数为:./x86.py -p loop.s -t 2 -i 100 -a dx=3,dx=3 -R dx ,即dx被初始化为3,运行2个同样的线程。多个线程运行时,中断时可能会发生调度。但由于dx初始化为3,一个线程的执行只需要10条指令,而中断产生间隔为100个指令,因此两个线程将分别运行,各执行13条指令(3次循环,每次3条指令,最后一次退出循环执行4条指令)后结束,且运行情况相同。使用-c标志进行验证:
dx Thread 0 Thread 1
3
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
1 1000 sub $1,%dx
1 1001 test $0,%dx
1 1002 jgte .top
0 1000 sub $1,%dx
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 1003 halt
3 ----- Halt;Switch ----- ----- Halt;Switch -----
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
1 1000 sub $1,%dx
1 1001 test $0,%dx
1 1002 jgte .top
0 1000 sub $1,%dx
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 1003 halt
这两个线程的同时存在并不会影响计算,这段代码并不存在竞态条件,没有使用共享的数据,寄存器中的值在线程切换时会保存及恢复,不会影响运行。
3.题3
本题中指定的参数为:./x86.py -p loop.s -t 2 -i 3 -r -a dx=3,dx=3 -R dx,中断间隔为3且随机。但由于loop.s中的代码并不存在竞态条件,所以中断间隔并不会影响两个线程运行的情况。使用c标志进行验证:
dx Thread 0 Thread 1
3
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
3 ------ Interrupt ------ ------ Interrupt ------
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
1 1001 test $0,%dx
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1002 jgte .top
0 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1001 test $0,%dx
1 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
0 1000 sub $1,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1001 test $0,%dx
-1 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
0 1001 test $0,%dx
0 1002 jgte .top
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1003 halt
0 ----- Halt;Switch ----- ----- Halt;Switch -----
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1002 jgte .top
-1 1003 halt
可以看到虽然发生了中断,但每个线程各自的运行情况和题2中是相同的,即正常进行循环。使用不同的种子,并修改中断间隔,都不会影响每个线程的运行情况。
4.题4
本题中需要运行的是looping-race-nolock.s:
.main
.top
# critical section
mov 2000, %ax # get 'value' at address 2000
add $1, %ax # increment it
mov %ax, 2000 # store it back
# see if we're still looping
sub $1, %bx
test $0, %bx
jgt .top
halt
这段程序将地址2000的值取出放在ax寄存器中,加1后存回地址2000处,并循环进行这个操作,bx仍然用于循环计数。
运行这段程序,题中所给参数为:./x86.py -p looping-race-nolock.s -t 1 -M 2000,单线程运行这段程序,设内存地址2000的值为x,随着循环(每6条指令)x的值递增,直到循环结束,最终程序实现的是x=原x+dx寄存器的值的运算。使用-c标志进行验证:
2000 Thread 0
0
0 1000 mov 2000, %ax
0 1001 add $1, %ax
1 1002 mov %ax, 2000
1 1003 sub $1, %bx
1 1004 test $0, %bx
1 1005 jgt .top
1 1006 halt
地址2000处的值为0,dx值为1,因此只循环了一次,在mov指令将值从ax寄存器存入地址2000后,最终的值为1。
这段代码实际是有竞态条件的,因为如果两个同样的该线程运行,就会对地址2000(x)处的共享数据进行操作。两个线程运行的正确的效果应该为最终x=原x+2dx的值,但同时进入临界区将导致错误发生。
#临界区
mov 2000, %ax # get 'value' at address 2000
add $1, %ax # increment it
mov %ax, 2000 # store it back
可能发生的错误如下:
线程1 | 线程2 | 地址2000处的数据 | ax寄存器 |
---|---|---|---|
mov 2000, %ax add $1, %ax | x | x+1 | |
mov 2000, %ax add $1, %ax mov %ax, 2000 | x+1 | x+1 | |
mov %ax, 2000 | x+1 | x+1 |
在两个线程各自的一次循环中,发生了调度,本应在两个分别的循环实现x=x+2,产生了错误的结果x=x+1,这就是此处竞态条件可能导致的错误。
第28章
本章同样使用x86.py模拟运行一些程序,观察运行情况和运行结果。
1.题1
flag.s如下:
.var flag
.var count
.main
.top
.acquire
mov flag, %ax # get flag
test $0, %ax # if we get 0 back: lock is free!
jne .acquire # if not, try again
mov $1, flag # store 1 into flag
# critical section
mov count, %ax # get the value at the address
add $1, %ax # increment it
mov %ax, count # store it back
# release lock
mov $0, flag # clear the flag now
# see if we're still looping
sub $1, %bx
test $0, %bx
jgt .top
halt
该段代码每次循环中将变量count转移到ax寄存器中,加1后再保存该值。使用了flag变量实现锁,每当对count变量运算时,先确定flag=0,如果不为0则循环等待,flag=0则将flag置1,对count进行操作,加1保存后再将flag置0。尝试通过flag变量实现锁,对临界区代码进行保护。但这个实现是有问题的,会出现类似如下的错误:
线程1 | 线程2 |
---|---|
mov flag, %ax test $0, %ax | |
mov flag, %ax test $0, %ax mov $1, flag | |
mov $1, flag … | |
… |
按照以上顺序执行时,就会产生两个线程都将flag设置为1,都能够进入临界区的情况,因此这种实现锁的方式不正确。
2.题2
使用默认值,指定2个线程运行:./x86 -p flag.s -t 2,运行情况如下:
Thread 0 Thread 1
1000 mov flag, %ax
1001 test $0, %ax
1002 jne .acquire
1003 mov $1, flag
1004 mov count, %ax
1005 add $1, %ax
1006 mov %ax, count
1007 mov $0, flag
1008 sub $1, %bx
1009 test $0, %bx
1010 jgt .top
1011 halt
----- Halt;Switch ----- ----- Halt;Switch -----
1000 mov flag, %ax
1001 test $0, %ax
1002 jne .acquire
1003 mov $1, flag
1004 mov count, %ax
1005 add $1, %ax
1006 mov %ax, count
1007 mov $0, flag
1008 sub $1, %bx
1009 test $0, %bx
1010 jgt .top
1011 halt
在默认情况下,中断间隔为50,线程运行没有发生中断,两个线程先后运行,不会产生错误,最终标志flag会在线程2最后一次循环被设置为0,而count=count+2*bx值。使用./x86 -p flag.s -t 2 -M count -R bx -c进行验证:
count bx Thread 0 Thread 1
0 0
0 0 1000 mov flag, %ax
0 0 1001 test $0, %ax
0 0 1002 jne .acquire
0 0 1003 mov $1, flag
0 0 1004 mov count, %ax
0 0 1005 add $1, %ax
1 0 1006 mov %ax, count
1 0 1007 mov $0, flag
1 -1 1008 sub $1, %bx
1 -1 1009 test $0, %bx
1 -1 1010 jgt .top
1 -1 1011 halt
1 0 ----- Halt;Switch ----- ----- Halt;Switch -----
1 0 1000 mov flag, %ax
1 0 1001 test $0, %ax
1 0 1002 jne .acquire
1 0 1003 mov $1, flag
1 0 1004 mov count, %ax
1 0 1005 add $1, %ax
2 0 1006 mov %ax, count
2 0 1007 mov $0, flag
2 -1 1008 sub $1, %bx
2 -1 1009 test $0, %bx
2 -1 1010 jgt .top
2 -1 1011 halt
count值为0,bx值为0,每个线程只有一次count+1,最终count值为2。两个线程运行中没有被中断。
3.题3
将bx值设置为2,运行两个线程,则每个线程实现count=count+2,最终count+=4。在题1中已分析过线程执行的代码存在竞态条件,但由于50条指令才发生中断,此处两个线程仍然是先后运行的,不会产生错误,最终count=4。使用./x86 -p flag.s -t 2 -a bx=2,bx=2 -M count -c运行程序进行验证:
0
0 1000 mov flag, %ax
0 1001 test $0, %ax
0 1002 jne .acquire
0 1003 mov $1, flag
0 1004 mov count, %ax
0 1005 add $1, %ax
1 1006 mov %ax, count
1 1007 mov $0, flag
1 1008 sub $1, %bx
1 1009 test $0, %bx
1 1010 jgt .top
1 1000 mov flag, %ax
1 1001 test $0, %ax
1 1002 jne .acquire
1 1003 mov $1, flag
1 1004 mov count, %ax
1 1005 add $1, %ax
2 1006 mov %ax, count
2 1007 mov $0, flag
2 1008 sub $1, %bx
2 1009 test $0, %bx
2 1010 jgt .top
2 1011 halt
2 ----- Halt;Switch ----- ----- Halt;Switch -----
2 1000 mov flag, %ax
2 1001 test $0, %ax
2 1002 jne .acquire
2 1003 mov $1, flag
2 1004 mov count, %ax
2 1005 add $1, %ax
3 1006 mov %ax, count
3 1007 mov $0, flag
3 1008 sub $1, %bx
3 1009 test $0, %bx
3 1010 jgt .top
3 1000 mov flag, %ax
3 1001 test $0, %ax
3 1002 jne .acquire
3 1003 mov $1, flag
3 1004 mov count, %ax
3 1005 add $1, %ax
4 1006 mov %ax, count
4 1007 mov $0, flag
4 1008 sub $1, %bx
4 1009 test $0, %bx
4 1010 jgt .top
4 1011 halt
4.题4
将bx设置为高值,即线程会执行很多条指令,再更改中断频率,中断时在一些特定的位置发生线程切换,就会产生在题1指出的可能发生的问题,两线程本应实现count=count+2bx,但运行结果将可能为count<count+2bx,使用不同的bx值和中断频率测试,结果如下:
bx | 中断间隔 | 结果(count) |
---|---|---|
1000 | 20 | 1779 |
1000 | 30 | 1802 |
1000 | 40 | 1776 |
1000 | 50 | 1985 |
1000 | 60 | 1904 |
1000 | 70 | 1746 |
1000 | 80 | 1800 |
1000 | 90 | 1802 |
1000 | 100 | 1810 |
按照产生错误的情况来看,应该是中断间隔越小,越多次线程切换,越可能产生错误。但实际上有一些中断间隔下,中断可能总是在设置标志flag的位置发生,就会产生不好的结果,有的中断间隔下,中断总是不发生在设置flag处,结果可能就较好。因此中断间隔越小,结果越不好的这种趋势并不明显。
5.题5
test-and-set.s中的程序如下:
.var mutex
.var count
.main
.top
.acquire
mov $1, %ax
xchg %ax, mutex # atomic swap of 1 and mutex
test $0, %ax # if we get 0 back: lock is free!
jne .acquire # if not, try again
# critical section
mov count, %ax # get the value at the address
add $1, %ax # increment it
mov %ax, count # store it back
# release lock
mov $0, mutex
# see if we're still looping
sub $1, %bx
test $0, %bx
jgt .top
halt
利用xchg指令实现锁,上锁时将ax寄存器设为1,使用xchg指令交换mutex和ax寄存器的值,如ax值为0则成功获取了锁。释放锁时,直接将mutex置0。
6.题6
运行两个线程,更改中断间隔,运行test-and-set.s,运算结果如下:
bx | 中断间隔 | 结果(count) |
---|---|---|
1000 | 10 | 2000 |
1000 | 20 | 2000 |
1000 | 30 | 2000 |
1000 | 40 | 2000 |
1000 | 50 | 2000 |
1000 | 60 | 2000 |
1000 | 70 | 2000 |
1000 | 80 | 2000 |
1000 | 90 | 2000 |
1000 | 100 | 2000 |
无论中断间隔多大,代码总是能按照预期工作,因为xchg实现的锁能够提供互斥,两线程不会同时进入临界区。这种锁的实现下,有时CPU利用率不高,因为如果锁被另一个线程占用,当前线程就需要循环等待,直到调度另一个线程且另一个线程释放锁,才能获取锁并继续运行。循环等待的时间就是CPU利用率不高的原因。完成一次完整的循环计算只需要6条指令,包含获取释放锁在内则需要11条指令,取比值,只有55%的时间CPU用于完成关键的运算。考虑到获取锁还需要等待,完成一次循环计算还需要更多条指令,将预期一次就可以获取锁完成一次计算的11条指令与实际完成一次计算的指令取比值,也可以量化CPU的利用率。
7.题7
在第一个线程获取锁之后,第二个线程应该无法获取锁,因此不停尝试获取锁,不能进入临界区。运行两个线程,设置bx=1,让一个线程先获取锁,另一个线程应该在不断尝试获取锁,因此设置线程运行序列为00(线程0获取锁)111111110000001111111111110000,./x86.py -p test-and-set.s -t 2 -a bx=1,bx=1 -P 00111111110000001111111111110000 -M mutex -c运行程序,结果是正确的,线程1自旋等待线程0释放锁。
mutex Thread 0 Thread 1
0
0 1000 mov $1, %ax
1 1001 xchg %ax, mutex
1 ------ Interrupt ------ ------ Interrupt ------
1 1000 mov $1, %ax
1 1001 xchg %ax, mutex
1 1002 test $0, %ax
1 1003 jne .acquire
1 1000 mov $1, %ax
1 1001 xchg %ax, mutex
1 1002 test $0, %ax
1 1003 jne .acquire
1 ------ Interrupt ------ ------ Interrupt ------
1 1002 test $0, %ax
1 1003 jne .acquire
1 1004 mov count, %ax
1 1005 add $1, %ax
1 1006 mov %ax, count
0 1007 mov $0, mutex
0 ------ Interrupt ------ ------ Interrupt ------
0 1000 mov $1, %ax
1 1001 xchg %ax, mutex
1 1002 test $0, %ax
1 1003 jne .acquire
1 1004 mov count, %ax
1 1005 add $1, %ax
1 1006 mov %ax, count
0 1007 mov $0, mutex
0 1008 sub $1, %bx
0 1009 test $0, %bx
0 1010 jgt .top
0 1011 halt
0 ----- Halt;Switch ----- ----- Halt;Switch -----
0 1008 sub $1, %bx
0 1009 test $0, %bx
0 1010 jgt .top
0 1011 halt
第30章
本章题目提供的程序可以观察一些使用条件变量的代码解决的生产者消费者问题的情况,一些重要的参数如下:
- -l:生产者生产数
- -m:缓冲区大小
- -p/-c:生产者和消费者数量
- -P/-C:生产者/消费者休眠情况
1.题1
main-two-cvs-while.c程序中是使用条件变量实现的生产者/消费者问题的解决方法。使用了两个条件变量发出正确的信号唤醒线程。
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t fill = PTHREAD_COND_INITIALIZER;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
缓冲区有不止一个位置,对应的fill和get的实现通过fill_ptr和use_ptr确定执行fill或get的生产者和消费者。
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;
}
生产者的部分如下,loops为次数,每次生产时,先获取锁,根据缓冲区是否为空决定是否需要休眠等待,为空则调用do_fill生产值,使用fill唤醒消费者,并释放锁。
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;
}
消费者的部分如下,消费者会一直消费至到消费到END_OF_STREAM,消费过程为上锁,根据缓冲区是否有数据,并决定是否休眠,有数据则获取数据,唤醒消费者并释放锁。
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);
}
2.题2
根据题中要求,只有一个生产者和一个消费者,缓冲区初始大小为1,在不休眠和消费者休眠释放锁时休眠的情况下,增加缓冲区大小,增加生产数。生产者消费者的生产和消费的过程应该不受缓冲区大小和生产数量的影响,增大缓冲区和生产数,可以看到两部分代码交替执行,每次生产与消费的过程都基本相同,生产和消费交替进行,num_full在0,1,2之间变化。加入休眠,./main-two-cvs-while -l 100 -m 10 -p 1 -c 1 -v -C 0,0,0,0,0,0,1运行程序,起初生产者一直运行直到缓冲区填满,缓冲区填满后看到消费者和生产者的代码按以下顺序交替循环执行,num_full随着消费和生产在9和10之间变化,最后消费者连续消费至0:
9 [ 90 91 92 93 94 95 96 97 f— u 89 ] p3
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] p4
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] p5
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] p6
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] p0
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] p1
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] p2
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] c0
10 [ 90 91 92 93 94 95 96 97 98 * 89 ] c1
9 [u 90 91 92 93 94 95 96 97 98 f— ] c4
9 [u 90 91 92 93 94 95 96 97 98 f— ] c5
9 [u 90 91 92 93 94 95 96 97 98 f— ] c6
3.题4
题中所给参数为:./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
一个生产者,三个消费者,缓冲区大小为1,每个消费者都在c3休眠。生产者生产10次,如果每个消费者都在c3休眠一秒,则所需时间为10s,而有时c3休眠后在while循环中,由于num_full=0再次进入c3,又休眠1s,因此最终所需要的时间应大于10s。
多次运行程序,所用时间不同,而且发现生产者总是唤醒某个特定消费者,导致总是一个消费者消费了大部分的数据。其中一次运行结果如下:
NF P0 C0 C1 C2
0 [*--- ] p0
0 [*--- ] c0
0 [*--- ] c0
0 [*--- ] c0
0 [*--- ] p1
1 [* 0 ] p4
1 [* 0 ] p5
1 [* 0 ] p6
1 [* 0 ] c1
1 [* 0 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c1
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] p1
1 [* 1 ] p4
1 [* 1 ] p5
1 [* 1 ] p6
1 [* 1 ] c1
1 [* 1 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 2 ] p4
1 [* 2 ] p5
1 [* 2 ] p6
1 [* 2 ] c1
1 [* 2 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 3 ] p4
1 [* 3 ] p5
1 [* 3 ] p6
1 [* 3 ] c1
1 [* 3 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 4 ] p4
1 [* 4 ] p5
1 [* 4 ] p6
1 [* 4 ] c1
1 [* 4 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 5 ] p4
1 [* 5 ] p5
1 [* 5 ] p6
1 [* 5 ] c1
1 [* 5 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 6 ] p4
1 [* 6 ] p5
1 [* 6 ] p6
1 [* 6 ] c1
1 [* 6 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 7 ] p4
1 [* 7 ] p5
1 [* 7 ] p6
1 [* 7 ] c1
1 [* 7 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 8 ] p4
1 [* 8 ] p5
1 [* 8 ] p6
1 [* 8 ] c1
1 [* 8 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 9 ] p4
1 [* 9 ] p5
1 [* 9 ] p6
1 [* 9 ] c1
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
1 [*EOS ] [main: added end-of-stream marker]
1 [*EOS ] c3
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c1
0 [*--- ] c2
1 [*EOS ] [main: added end-of-stream marker]
1 [*EOS ] c3
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
1 [*EOS ] [main: added end-of-stream marker]
1 [*EOS ] c3
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
Consumer consumption:
C0 -> 0
C1 -> 10
C2 -> 0
Total time: 12.06 seconds
4.题8
main-one-cv-while.c中do_get和do_fill与main-two-cv-while.c中的相同,消费者与生产者的部分只使用了一个条件变量。
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
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(&cv, &m); p3;
}
do_fill(base + i); p4;
Cond_signal(&cv); 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(&cv, &m); c3;
}
tmp = do_get(); c4;
Cond_signal(&cv); c5;
Mutex_unlock(&m); c6;
consumed_count++;
}
在对缓冲区进行写和读取的部分有锁进行保护,而条件变量的使用保证了缓冲区有数据才会读取,无数据才会写入,当只有一个生产者和一个消费者时,这段代码是可以正确运行的,不会出现错误。
5.题9
main-one-cv-while.c的代码中消费者与生产者只使用了一个条件变量来进行等待,当有多个消费者时,就会产生问题。有两个消费者,一个生产者时,假设生产者先运行,然后唤醒一个消费者并进入睡眠,消费者消费值后需要唤醒一个线程,如果此时唤醒的是另一个消费者,另一个消费者醒来后无值可消费,就会睡眠,造成了三个线程都进入睡眠的情况。以上是书中给出的出错情况,由于程序添加了调度,难以复现这种错误。所以考虑另一种错误,让生产者先生产填满缓冲区后进入睡眠,一个消费者消费该值,然后唤醒另一个消费者,两个消费者都没有值可消费,睡眠等待。 ./main-one-cv-while -p 1 -c 2 -m 1 -P 0,0,0,0,0,0,1 -l 3 -v -t,运行后,生产者完成一次生产就休眠,最后一次时,C1消费后唤醒了C2,两个消费者都进入休眠,,将无法再唤醒。
NF P0 C0 C1
0 [*--- ] p0
0 [*--- ] c0
0 [*--- ] c0
0 [*--- ] p1
1 [* 0 ] p4
1 [* 0 ] p5
1 [* 0 ] p6
1 [* 0 ] c1
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c1
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] p0
0 [*--- ] p1
1 [* 1 ] p4
1 [* 1 ] p5
1 [* 1 ] p6
1 [* 1 ] c3
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c2
0 [*--- ] c0
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] p0
0 [*--- ] p1
1 [* 2 ] p4
1 [* 2 ] p5
1 [* 2 ] p6
1 [* 2 ] c3
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] c1
0 [*--- ] c2
1 [*EOS ] [main: added end-of-stream marker]
1 [*EOS ] c3
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c2
6.题10
在main-two-cv-if.c中,判断缓冲区是否为空使用if语句
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;
if (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;
if (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);
}
当只有一个生产者和一个消费者时,如果消费者判断缓冲区为空,休眠等待,生产者生产后唤醒消费者,消费者继续执行c4进行消费,不会产生问题。当有一个生产者和两个消费者时就会产生问题,消费者休眠后,生产者生产唤醒了该消费者,如果没来得及唤醒并上锁,另一个消费者就进入消费了该值,这个消费者唤醒后执行c4就会发生错误。只要消费者c1在c0处,而生产者刚完成生产,就可能产生这种问题,不需要特别设定睡眠,./main-two-cvs-if -m 1 -c 2 -p 1 -l 10 -v 运行程序,一次该错误发生的情况如下:
NF P0 C0 C1
0 [*--- ] p0
0 [*--- ] c0
0 [*--- ] c0
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] p1
1 [* 0 ] p4
1 [* 0 ] p5
1 [* 0 ] p6
1 [* 0 ] c3
1 [* 0 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] p1
0 [*--- ] c0 //消费者C1已经在c0处
1 [* 1 ] p4
1 [* 1 ] p5
1 [* 1 ] p6 //生产结束,唤醒c0,但是消费者C1先运行了
1 [* 1 ] c1
1 [* 1 ] p0
0 [*--- ] c4 //C1抢先消费了值
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3 //C0准备消费,将发生错误
0 [*--- ] c0
error: tried to get an empty buffer
7.题11
在main-cvs-while-extra-unlock.c中,条件变量处都添加了锁,但缓冲区相关的do_fill和do_get处没有加锁,消费者生产者都可能同时访问,会产生错误。
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++;
}
// return consumer_count-1 because END_OF_STREAM does not count
return (void *) (long long) (consumed_count - 1);
}
具体而言,如果两个生产者都在执行do_fill,对同一个缓冲区进行操作,对buf[fill_ptr]赋值value,还没有进行fill_ptr+1,此时另一个生产者调度运行,对同一个buf[fill_ptr]赋值value,就导致对同一个缓冲区两次赋值,第一次的赋值被覆盖掉而没有被消费。题目中给出的程序总是单步完成p4,调度没有在单条指令执行时发生,使用指定睡眠也没有出现生产者执行p4时调度到另一个生产者执行p4,似乎不会出现这个错误。
第31章
1.题1
根据要求,需要完成fork-join.c中的代码。该段代码是将信号量作为条件变量使用的实例,父进程创建子线程后,等待子线程的完成。只需要使用信号量,让父进程在创建完子进程后调用sem_wait等待,子线程执行完成后调用sem_post唤醒父进程。信号量的初始值应该设为0,如果父线程先运行,则将信号量-1,睡眠等待,子线程运行结束后将信号量加为0,父线程运行;如果子线程先运行,父线程运行时信号量已被加为1,就可以直接运行。补全fork-join.c中的代码:
sem_t s;
void *child(void *arg) {
printf("child\n");
// use semaphore here
sem_post(&s);
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t p;
printf("parent: begin\n");
// init semaphore here
sem_init(&s,0,0);
Pthread_create(&p, NULL, child, NULL);
// use semaphore here
sem_wait(&s);
printf("parent: end\n");
return 0;
}
运行结果正常,父线程等待子线程运行结束后继续执行。
parent: begin
child
parent: end
2.题2
第二题需要完成的是聚集问题:有两个线程,每个线程的代码都要进入聚集部分,一个线程在另一个线程进入该段代码之前不能结束。
void *child_1(void *arg) {
printf("child 1: before\n");
// what goes here?
printf("child 1: after\n");
return NULL;
}
void *child_2(void *arg) {
printf("child 2: before\n");
// what goes here?
printf("child 2: after\n");
return NULL;
}
在before与after之间就是两个线程聚集的部分,因此正确的解决这个问题后,应该在两个子线程都输出before后才输出after。可以使用两个信号量来实现,此处的聚集本质上是一种相互的等待,两个线程都需要等到对方进入聚集处,才可以继续执行。使用信号量s1和s2,child1调用sem_post将s1信号量+1,然后调用sem_wait等待child2,表示自己已进入聚集处,child2同理。两个信号量都用作了条件变量,与上题相同,信号量应该初始化为0。
sem_t s1, s2;
void *child_1(void *arg) {
printf("child 1: before\n");
sem_post(&s1);
sem_wait(&s2);
printf("child 1: after\n");
return NULL;
}
void *child_2(void *arg) {
printf("child 2: before\n");
sem_post(&s2);
sem_wait(&s1);
printf("child 2: after\n");
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t p1, p2;
printf("parent: begin\n");
// init semaphores here
sem_init(&s1,0,0);
sem_init(&s2,0,0);
Pthread_create(&p1, NULL, child_1, NULL);
Pthread_create(&p2, NULL, child_2, NULL);
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("parent: end\n");
return 0;
}
运行程序,输出正确。
parent: begin
child 2: before
child 1: before
child 1: after
child 2: after
parent: end
3.题4
本题需要使用信号量完成读者写者锁。在reader-writer.c中定义了读者与写者,读者和写者分别进行loops次读和写,写者每次将value值+1,可以通过参数指定读者与写者的数量。而读者与写者对数据的访问需要使用的锁即rwlock_t,就是需要实现的锁吗,需要完成该锁读者写者锁的获取以及相应的释放。
int loops;
int value = 0;
rwlock_t lock;
void *reader(void *arg) {
int i;
for (i = 0; i < loops; i++) {
rwlock_acquire_readlock(&lock);
printf("read %d\n", value);
rwlock_release_readlock(&lock);
}
return NULL;
}
void *writer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
rwlock_acquire_writelock(&lock);
value++;
printf("write %d\n", value);
rwlock_release_writelock(&lock);
}
return NULL;
}
int main(int argc, char *argv[]) {
assert(argc == 4);
int num_readers = atoi(argv[1]);
int num_writers = atoi(argv[2]);
loops = atoi(argv[3]);
pthread_t pr[num_readers], pw[num_writers];
rwlock_init(&lock);
printf("begin\n");
int i;
for (i = 0; i < num_readers; i++)
Pthread_create(&pr[i], NULL, reader, NULL);
for (i = 0; i < num_writers; i++)
Pthread_create(&pw[i], NULL, writer, NULL);
for (i = 0; i < num_readers; i++)
Pthread_join(pr[i], NULL);
for (i = 0; i < num_writers; i++)
Pthread_join(pw[i], NULL);
printf("end: value %d\n", value);
return 0;
}
首先对rwlock_t结构进行定义,该结构中需要包含一个基本的二值信号量锁,还需要一个写者锁,保证只有一个写者可以进行写,最后还需要一个整型变量记录读者数量,第一个读者会获取写锁,其他读者可以读,只有所有读者都结束,读者数为0才可以有写者获取锁。
typedef struct __rwlock_t {
sem_t lock;
sem_t writelock;
int readers;
} rwlock_t;
由于该问题中,信号量做锁使用,将信号量初始化为1。读者数初始化为0。
void rwlock_init(rwlock_t *rw) {
sem_init(&rw->lock,0,1);
sem_init(&rw->writelock,0,1);
rw->readers=0;
}
同一时刻可以有多个读者读取数据,因此获取读者锁中,只有第一个读者需要同时获取写者锁,其他读者只需要将读者数+1,释放读者锁同理。
void rwlock_acquire_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->readers++;
if(rw->readers==0) sem_wait(&rw->writelock);
sem_post(&rw->lock);
}
void rwlock_release_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->readers--;
if(rw->readers==0) sem_post(&rw->writelock);
sem_post(&rw->lock);
}
写者锁的获取与释放只需要调用sem_wait和sem_post。
void rwlock_acquire_writelock(rwlock_t *rw) {
sem_wait(&rw->writelock);
}
void rwlock_release_writelock(rwlock_t *rw) {
sem_post(&rw->writelock);
}
运行程序,设置2个读者,2个写者,loops=2,先有一个读者进行了读,此后2个写者进行了写,最后一个读者再次读,运行情况是正常的。
y@ubuntu:~/workspace/threads-sema$ ./reader-writer 2 2 2
begin
read 0
read 0
write 1
write 2
write 3
write 4
read 4
read 4
end: value 4
由于写者总是需要等待所有读者都结束才可以获取写锁,当读者过多时,写者就可能会饿死。运行一个有100个读者,1个写者情况,可以看到一直是读者进行读,直到最后写者才获取写锁进行写,这种情况写者就很可能饿死。
4.题5
本题需要解决读者写者锁中写者可能饿死的问题。写者可能饿死,是因为读者数量不受限制,同一时刻可以有多个读者进行读,而只要有读者,写者就不能获取写锁。为了解决这个问题,可以再增加一个信号量实现一个锁writerlock,一旦有写者准备进行写操作,尝试获取该锁,获取该锁后可以使新的读者不能进行读,直到写者完成写。这样读者的数量就受到了限制,写者能够保证在等待当前数量的读者读取数据后,可以进行写操作,而不需要因为一直有新加入的读者而等待至饿死。
typedef struct __rwlock_t {
sem_t lock;
sem_t writelock;
sem_t writerlock;
int readers;
} rwlock_t;
void rwlock_init(rwlock_t *rw) {
sem_init(&rw->lock,0,1);
sem_init(&rw->writelock,0,1);
sem_init(&rw->writerlock,0,1);
rw->readers=0;
}
void rwlock_acquire_readlock(rwlock_t *rw) {
sem_wait(&rw->writerlock);
sem_wait(&rw->lock);
rw->readers++;
if(rw->readers==0) sem_wait(&rw->writelock);
sem_post(&rw->lock);
sem_post(&rw->writerlock);
}
void rwlock_release_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->readers--;
if(rw->readers==0) sem_post(&rw->writelock);
sem_post(&rw->lock);
}
void rwlock_acquire_writelock(rwlock_t *rw) {
sem_wait(&rw->writerlock);
sem_wait(&rw->writelock);
}
void rwlock_release_writelock(rwlock_t *rw) {
sem_post(&rw->writelock);
sem_post(&rw->writerlock);
}
运行以上程序,运行一个有100个读者,1个写者的情况,loop=3,与上一题中同样的情况相比,明显饥饿问题得到了缓解,写的操作不再总是在最后才进行。多次运行,有的情况下写操作还是很靠后,这可能是因为在写者获取写者锁writerlock阻止读者读之前,已经有很多读者在进行读操作了。
5.题6
本题要求实现一种不会产生饥饿的锁,任何尝试获取该锁的线程都最终可以获取该锁而不会被饿死。线程无法获取锁而饿死的原因是当线程等待时,可能有其他线程先获取了锁,不断有其他的线程先抢锁,因此当前线程一直无法拿到锁。如果可以保证当前线程抢锁时,没有其他线程不停插队抢锁,就可以保证当前线程总是可以拿到锁。
具体如何实现没有新的线程插队抢锁,参考书中所提供的资料Little Book of Semaphores中解决该问题的方法,即Morris’s solution。该方法划分出了三个room,三个信号量。一段时间内尝试获取锁的线程先进入room1等待,room1中的线程又会进入room2,并停留在room2中,当room1中不再有线程时,room2到room3的门打开,room1到room2的门关闭,进入room3执行的线程只有每次一个,而room2中等待运行的线程数是确定的,不会有任何一个线程饿死。room2中的线程都执行完毕后,再打开room1的门,让线程进入room1,重复上述过程。使线程能够按批次获取锁并执行,就实现了没有饥饿问题的锁。其中room1和room2中的线程数量是通过变量进行计数的,具体的实现如下:
typedef __ns_mutex_t {
sem_t lock;
sem_t door1;
sem_t door2;
int room1;
int room2;
} ns_mutex_t;
void ns_mutex_init(ns_mutex_t *m) {
sem_init(&m->lock,1);
sem_init(&m->door1,1);
sem_init(&m->door2,0);
room1=0;
room2=0;
}
void ns_mutex_acquire(ns_mutex_t *m) {
sem_wait(&m->lock);
m->room1++;
sem_post(&m->lock);
//进入room2
sem_wait(&m->door1);
m->room2++;
sem_wait(&m->lock);
m->room1--;
if(m->room1){ //还有线程进入room1
sem_post(&m->lock); //room1room2可进
sem_post(&m->door1);
}
else{
sem_post(&m->lock); //room1空,可进
sem_post(&m->door2); //room3可进
}
sem_wait(&m->door2);
m->room2--;
}
void ns_mutex_release(ns_mutex_t *m) {
if(m->room2){
sem_post(&m->door2);
}
else{
sem_post(&m->door1);
}
}
仿照其他题目中的程序完成main及worker线程:
int loops;
ns_mutex_t lock;
void *worker(void* arg) {
int i;
for (i = 0; i < loops; i++) {
ns_mutex_acquire(&lock);
ns_mutex_release(&lock);
}
return NULL;
}
int main(int argc, char *argv[]) {
assert(argc == 3);
int num_workers= atoi(argv[1]);
loops = atoi(argv[2]);
pthread_t pr[num_workers];
ns_mutex_init(&lock);
int i;
printf("parent: begin\n");
for (i = 0; i < num_workers; i++)
Pthread_create(&pr[i], NULL,worker, NULL);
for (i = 0; i < num_workers; i++)
Pthread_join(pr[i], NULL);
printf("parent: end\n");
return 0;
}
该锁是可以正常工作的,但是最后的实现没有检测是否保证了没有饥饿的情况产生,仅仅是根据参考资料实现了该锁,对这个不会产生饥饿问题的锁的理解还不够深刻,还需要继续学习。