新知识
1.哈希表
2.如果没有阻塞或者等待,主线程会直接运行下去,并不会等待子线程执行结束。这里是靠pthread_join
使主线程等待两个子线程结束。也可以通过sleap
3.在用户没有设定线程间的调度策略时,系统默认采取基于时间片轮转
的调度策略。所以当NKEYS=100000时,会出现两个线程的多次轮转,也才有可能导致key的丢失。当我把NKEYS改成10的时候,就算不用put里也不加锁,也并没有丢失任何key,说明了这一点。执行顺序参考了李亚超兄弟
4.多线程可能影响并行速度,所以可能并不一定多线程就是好的
5. locks有助于正确地共享数据
; locks可以限制并行加速
6. parallelism
(并行性)
7. 可以参考Lecture9
实验过程
在本作业中,您将使用a hash table
(哈希表)研究threads and locks
的并行编程。您应该在具有多个核心的真实计算机(不是xv6,也不是qemu)上完成这项作业。
在自己的操作系统上使用下面命令
$ gcc -g -O2 ph.c -pthread
$ ./a.out 2
2指定在哈希表上执行put和get操作的线程数。结果如下:
每个线程分两个阶段(phases
)运行。在第一个阶段,每个线程将NKEYS/nthread keys
放入the hash table
。在第二阶段,每个线程从散列表中获取NKEYS。print语句告诉您每个阶段为每个线程花费了多长时间。底部的completion time告诉您应用程序的总运行时间。在上面的输出中,应用程序的完成时间约为30.5秒。每个线程计算大约30.5秒(put ~0.0 + get ~30.5)。
要查看使用两个线程是否提高了性能,让我们与单个线程进行比较$ ./a.out 1
:
这就尴尬了,homework页面里的两个线程任务7.7秒,一个线程也要7.0秒,自然是提高了近一倍的速度。但是我这里两个线程30.5,一个线程只要14.0,那岂不是还更慢些了?尴尬。。。
好吧,原来是我的虚拟机设置的是单核的,改成双核的再试一次:
两个重点:1)完成时间与2个线程大致相同,但是双线程get的线程数是单线程的两倍,即双线程遍历了两次哈希表
,我们正在实现良好的并行性。2)两个线程的输出表明丢失了许多keys。在您的运行中,可能会丢失或多或少的键。如果使用一个线程
运行,就不会丢失任何键
。
我发现我的线程的执行顺序好像跟别人的不一样。而且不改代码,连续运行两遍,执行顺序也不一样?执行顺序到底是什么样的?
答:一开始,应该是在主线程里,然后主线程因为pthread_join
而要等线程0和线程1结束。然后线程0与线程1都开始put,因为__sync_fetch_and_add
需要等所有线程完成put操作,所以先完成put操作的子线程会print出put的时间然后阻塞等另一个线程完成put操作。然后再通过get来遍历哈希表
。所以顺序不一样应该是正常的,看哪边先put完,跟机器应该也有关系
为什么有两个或多个线程丢失键,而一个线程时却不会?识别可能导致两个线程丢失键的事件序列
。
可能线程1在线程0执行完insert
函数中的e->next = n;
后执行了insert操作,此时新增的两个Entry都指向n,并且线程1的Entry作为链表头
放入了bucket
,然后线程0的Entry也会放入,这样就覆盖了线程1的那个,就是丢失了一个key。(尽管线程1的key1与线程0的key2不相等,但当key1%NBUCKET == key2%NBUCKET == i
时,就有可能同时想插入bucket[i]
中)
为了避免这个事件序列,在put
和get
中插入lock
和unlock
语句,使得丢失的键数总是0。相关的pthread调用是:
pthread_mutex_t lock; // declare a lock
pthread_mutex_init(&lock, NULL); // initialize the lock
pthread_mutex_lock(&lock); // acquire lock
pthread_mutex_unlock(&lock); // release lock
修改如下:
static void put(int key, int value)
{
pthread_mutex_lock(&lock); // acquire lock
int i = key % NBUCKET;
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&lock); // release lock
}
static struct entry* get(int key)
{
pthread_mutex_lock(&lock); // acquire lock
struct entry *e = 0;
for (e = table[key % NBUCKET]; e != 0; e = e->next) {
if (e->key == key) break;
}
pthread_mutex_unlock(&lock); // release lock
return e;
}
首先用一个线程测试代码,然后用两个线程测试它。它是正确的吗?(也就是说,你已经把丢失的keys清除了吗?)双线程版本比单线程版本快吗?
确实没有再miss keys了,但是双线程时间变成34.5秒,比单线程13秒的两倍还多。说明双线程并不比单线程版本快,所以有时候多线程并不总是好的。
修改代码,以便在保持正确性的同时并行运行get
操作。(提示:在这个应用程序中,为了保证正确性,locks
在get
中是必需的吗?)
答:get只是遍历哈希表,并不会改变哈希表,所以加不加锁都不会导致key丢失。所以单纯只讨论key会不会丢失时get里不用加locks
修改代码,使某些put
操作并行运行,同时保持正确性。(提示:每个bucket(桶?)都有a lock
吗?)
不懂什么意思。。。按照逻辑来看,为了防止两个线程同时往同一个bucket里insert才加的锁,所以应该确实时每个bucket都有a lock。
修改如下:
static void put(int key, int value)
{
pthread_mutex_lock(&lock); // acquire lock
int i = key % NBUCKET; //i为bucket的索引值
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&lock); // release lock
}
static struct entry* get(int key)
{
struct entry *e = 0;
for (e = table[key % NBUCKET]; e != 0; e = e->next) {
if (e->key == key) break;
}
return e;
}
最后一次运行: