6.S081 lab7 Multithreading

1. 任务

  任务主要有3个:将所给代码补充完整,简单实现用户级的多线程;使用多线程+锁体验程序速度提升;实现barrier函数(多线程同步机制)。

1.5 补充

  在barrier函数的实现中,用到了之前没用过的pthread函数,在此记录一下函数语义。


int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t *cond_attr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, 
    pthread_mutex_t *mutex, const struct timespec *abstime); 
int pthread_cond_broadcast(pthread_cond_t *cond);     
int pthread_cond_signal(pthread_cond_t *cond);

  pthread_cond_t是一个条件变量,和mutex类似,也需要初始化和销毁(attr变量一般填0就好?),一般把cond和mutex合称为条件锁。pthread_cond_wait调用时要求调用者持有锁mutex。一般的调用伪代码是这样:

acquire(&mutex);
while(wait_condition == false){  //这里的while也可以改为if
	pthread_cond_wait(&cond, &mutex);
}
//the following is critical section.
......
//end of critical section
release(&mutex);

  上面程序的语义是:检验条件是否为真(比如典型的生产者消费者模型中,消费者需要检验此时是否有消费品),若否,则调用pthread_cont_wait陷入阻塞,该函数同时会释放锁mutex。这时,需要另一个线程调用signal或者broadcast函数将其唤醒。

acquire(&mutex);
//the following is critical section.
.... //这其中的代码使得等待条件为真,比如一个生产者生产了一个消费品
//end of critical section
pthread_cond_signal(&cond, &mutex); //pthread_cond_broadcast也可以
release(&mutex);

  因为前一个线程在阻塞时会释放掉锁,因此该线程能顺利进入关键区,然后使得等待条件为真,之后调用signal函数唤醒等待线程,再释放掉该锁。此时前一个线程便从阻塞态恢复,然后退出while循环,进入关键区。
  详细解释一下一些容易困惑的地方(反正我当时挺困惑的)

  • 对于同一个条件变量cond,要求只对应唯一一把锁。也就是说,同一个cond,可以多个线程调用pthread_cond_wait(&cond, &mutex),但是这个mutex必须是同一个,否则是未定义行为
  • signal和broadcast的区别:正如前面说到的,可以多个线程阻塞在同一个cond上,这会形成一个阻塞队列,signal的语义是把该cond对应的阻塞队列中某一个线程唤醒,语义上并未指定是哪个。而broadcast语义是将该cond对应的阻塞队列全部变为可运行态。
  • 并不是说调用了signal或者broadcast之后被阻塞线程马上就可以运行,因为wait函数的语义要求被阻塞线程返回后需要持有该mutex。因此还需要后一个线程释放了mutex之后,被wait函数阻塞的线程才能返回。所以调用broadcast函数并不意味着所有线程都能够从wait函数中返回,只有唯一一个抢到了mutex锁的函数才能返回。但这并不意味着signal和broadcast函数能够划等号,因为broadcast使得队列中所有线程都不被cond阻塞了。具体地说,调用broadcast后,虽然只有一个线程能从wait函数中返回,但当该线程释放mutex时,一个新的线程就能从wait函数中返回了(而如果之前调用的是signal函数,意味着需要再调用一次signal才能有新的线程从wait中返回,即使现在没有线程占用mutex锁)。
  • signal或者broadcast函数不能提前调用,就是说现在阻塞队列中没有线程,调用了signal或者broadcast函数,是没有作用的。此后一个新线程调用了wait之后仍会被cond阻塞,除非再次有其它线程调用signal或者broadcast函数。

1.8 关于timedwait函数

  timedwait函数与wait基本上差不多,除了多一个时间参数timespec。有两个相关的数据结构。

#include <sys/time.h>
struct timeval
{
__time_t tv_sec;        /* Seconds. */
__suseconds_t tv_usec;  /* Microseconds. */
};

struct timespec {
time_t tv_sec; // seconds
long tv_nsec; // and nanoseconds
};
int gettimeofday(struct timeval *tv, NULL); //第二个参数传NULL就好了

  这两个数据结构表达的是从1970年1月1日0点开始经过的时间,一个精确到微秒,一个精确到纳秒。gettimeofday可以获取现在的经过时间,数据放在传入的timeval中。一个简单的使用范例:

int u;
struct timeval now;
struct timespec sepc;
gettimeofday(&now, NULL);
sepc.tv_sec = now.tv_sec + 5;
sepc.tv_nsec = now.tv_usec*1000;
u = pthread_cond_timedwait( &g_cond, &g_mutex,&sepc);

  表示该进程被阻塞至多5秒,如果5秒后仍没有被signal或者broadcast唤醒,则自动退出,返回值为ETIMEDOUT(在errno.h中定义)。在文档里对阻塞时linux信号的处理的描述,一种可能是自动处理信号,就好像信号没发生一样,或者信号会唤醒阻塞在cond上的进程,并返回EINTR。不过我在Ubuntu上测试wait和timedwait发现效果应该是前者。
  我觉得差不多条件锁相关的函数的语义就解释清楚了,下面可以开始看lab了。

2 用户级多线程实现

 这个部分挺有趣的,没想到100多行代码就可以实现一个简单的用户级线程切换。

  // thread_create 函数的补充代码
  *(uint64*)t->stack = (uint64)(t->stack + STACK_SIZE);  // 设置sp 
  *((uint64*)t + 1) = (uint64)func;    // 设置 ra

想法是用线程栈底来保存寄存器,在初始化线程时只需要在栈底设置好sp和ra即可。然后在scheduler的调度中传入线程的栈底指针。

// thread_schedule 函数的补充代码
thread_switch((uint64)t->stack, (uint64)next_thread->stack);

最后thread_switch的代码和xv6的上下文切换代码基本一致

		sd sp, (a0)
		sd ra, 8(a0)
        sd s0, 16(a0)
        sd s1, 24(a0)
        sd s2, 32(a0)
        sd s3, 40(a0)
        sd s4, 48(a0)
        sd s5, 56(a0)
        sd s6, 64(a0)
        sd s7, 72(a0)
        sd s8, 80(a0)
        sd s9, 88(a0)
        sd s10, 96(a0)
        sd s11, 104(a0)

        ld sp, (a1)
		ld ra, 8(a1)
        ld s0, 16(a1)
        ld s1, 24(a1)
        ld s2, 32(a1)
        ld s3, 40(a1)
        ld s4, 48(a1)
        ld s5, 56(a1)
        ld s6, 64(a1)
        ld s7, 72(a1)
        ld s8, 80(a1)
        ld s9, 88(a1)
        ld s10, 96(a1)
        ld s11, 104(a1)
		ret    /* return to ra */

总结一下用户级多线程的简单实现:需要一个线程队列;一个thread_create函数,负责创建线程;一个yield函数,负责让出cpu;一个schedule函数,在调用yield后陷入schedule,扫描线程队列进行上下文切换。最后一个thread_exit函数,线程执行完毕时调用,回收线程资源。

当然这里并没有给出如何实现线程的同步机制(用户级的多线程需要同步机制吗,在操作系统看来只有一个进程,意味着用户级的多线程同时只能有一个线程在cpu上跑,所以只需要最简单的锁机制就够了?我觉得。。可以选择让操作系统实现一个给用户进程提供锁的接口,或者用Peterson算法等软件方法来做一个锁就好了)。

3 多线程的使用

这部分就直接跳过把,我觉得lab的第二部分实在是简单过头了

4 barrier实现

static void 
barrier()
{
  // YOUR CODE HERE
  //
  // Block until all threads have called barrier() and
  // then increment bstate.round.
  // 
  pthread_mutex_lock(&bstate.barrier_mutex);
  bstate.nthread++;
  if(bstate.nthread == nthread){
	  bstate.round++;
	  bstate.nthread = 0;
	  pthread_cond_broadcast(&bstate.barrier_cond);
  }
  else{
	   pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
  }
  pthread_mutex_unlock(&bstate.barrier_mutex);
}

我觉得理解了条件锁的语义,解决这个问题也没什么难度的说。

5 总结收获

  1. 用户级多线程的基本实现方法。
  2. 条件锁的语义和应用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值