2017-2018-1 20155320 《信息安全系统设计基础》第十三周学习总结

参考老师提供的教材内容导读

本周的内容是要学习觉得最重要的一章,我最终决定选择第12章——并发编程

因为对于并发这个概念在计算机学习中在很多方面都要用到。

贯彻十二章有几个概念:

  1. 进程
  2. 线程
  3. 并发
  4. 上下文调度 /切换

在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。

本次再度学习十二章一是对以前学过的内容查漏补缺二是巩固重点知识(尤其是线程的相关知识),因此将不会完全再描述每一个知识点但将结合实例进行学习

教材学习内容总结

  • 如果逻辑控制流在时间上重叠,那么他们就是并发的。
  • 现代操作系统提供了三种基本的构造并发程序的办法
    • 进程:每个逻辑控制流都是一个进程
    • I/O多路复用:在这种形式的并发编程中,应用程序在一个进程的上下文中显示地调度他们的逻辑流
    • 线程:线程是运行在一个单一进程上下文中的逻辑流。

以下就是这三种构造方法的学习

12.1基于进程的并发编程

  • 是构造并发程序最简单的方法

  • 父子进程的已连接描述符都指向同一个文件表项,父进程关闭它的连接描述符是至关重要的。

  • 父进程派生子进程为客户端提供服务,而父进程自己用来等待下一个连接请求。

  • 父进程派生一个子进程来处理每个新的连接请求

  • 关于进程的echo服务器

    使用SIGCHLD处理程序来回收僵死子进程的资源。

    父进程必须关闭他们各自的connfd拷贝(已连接的描述符),避免存储器泄露。

    因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。

  • 注意

    1.父进程需要关闭它的已连接描述符的拷贝(子进程也需要关闭)

    2.必须要包括一个SIGCHLD处理程序来回收僵死子进程的资源

    3.父子进程之间共享文件表,但是不共享用户地址空间。

  • 进程的优劣

父、子进程共享状态信息:共享文件表但不共享用户空间。

优点:防止虚拟存储器被错误覆盖

缺点:开销高,共享状态信息才需要IPC机制

12.2 基于I/O多路复用的并发编程

  • 基本思想:就是使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。

  • Select函数

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout); 

1.返回值

返回已准备好的描述符的非0个数,若出错则为-1

2.参数详解

int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!

fd_set * readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符

fd_set * writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

fd_set * errorfds同上面两个参数的意图,用来监视文件错误异常。

struct timeval * timeout是select的超时时间它可以使select处于三种状态:

第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;

第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;

第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。 
  • 通过实例来学习Select函数
#include <sys/types.h>   
#include <sys/time.h>   
#include <stdio.h>   
#include <fcntl.h>   
#include <sys/ioctl.h>   
#include <unistd.h>   
  
int main()   
{   
    char buffer[128];   
    int result, nread;   
    fd_set inputs, testfds;   
    struct timeval timeout;   
    FD_ZERO(&inputs);//用select函数之前先把集合清零    
     FD_SET(0,&inputs);//把要检测的句柄——标准输入(0),加入到集合里。  
     while(1)   
    {   
       testfds = inputs;   
       timeout.tv_sec = 2;   
       timeout.tv_usec = 500000;   
       result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, &timeout);   
       switch(result)   
       {   
       case 0:   
           printf("timeout/n");   
           break;  
       case -1:   
           perror("select");   
           exit(1);   
       default:   
           if(FD_ISSET(0,&testfds))   
           {   
               ioctl(0,FIONREAD,&nread);//取得从键盘输入字符的个数,包括回车。   
               if(nread == 0)   
               {   
                  printf("keyboard done/n");   
                  exit(0);   
               }   
               nread = read(0,buffer,nread);   
               buffer[nread] = 0;   
               printf("read %d from keyboard: %s", nread, buffer);   
         }   
         break;   
      }   
   }   
   return 0;  
}   
  • 上面这个简单的实例就是用来读取键盘输入值,超时间隔2.5秒,输出用户输入的字符个数。

  • select函数学习总结:
    对比而言,select有其优势,Select可以实现非阻塞,即进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。

12.3 基于线程的并发编程

这一种方法是基于前两中的复合

  • 线程:之前我对线程和进程总是分不清,线程就是运行在上下文的逻辑流,每个线程都有其唯一的整数ID(内核以此区分线程)。

  • 每个线程开始都是由单一线程开始的,称为主线程;在某一个时刻主线程创建一个对等线程,开始进场并发的运行,他们之间通过上下文切换

  • 线程执行与进程执行的区别

    • 一个线程的上下文比一个进程的上下文小得多,切换也要快得多
    • 一个线程能杀死他的任何对等线程或者等待他的任意对等线程终止

创建线程

  • 线程通过调用pthread_create来创建其他线程。
int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg);
成功则返回0,出错则为非零

  • 函数参数:

    • tidp:线程标识符指针
    • attr:线程属性指针(不要求则传空)
    • start_rtn:线程运行函数的起始地址,(需要用户自己定义)
    • arg:传递给线程运行函数的参数

函数返回时,参数tid包含新创建的线程的ID,新线程可以通过调用pthread_self函数来获得自己的线程ID。

pthread_t pthread_self(void);

返回调用者的线程ID。

  • 创建线程实践:
#include <pthread.h>

pthread_t ntid;

void printids(const char *s)
{
    pid_t   pid;
    pthread_t   tid;
    
    pid = getpid();
    tid = pthread_self();

    printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid);
}

void* thr_fn(void *arg)
{
    printids("new thread:");
    return((void*)0);
}

int main()
{
    int     err;
    
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if(err!=0)
    {
        printf("can't create thread\n");
        exit(1);
    }
    printids("main thread:");
    sleep(2);
    exit(0);
}
  • 此实例既使用了pthread_create()创建也使用了pthread_self()获取ID,打印出主线程与创建 的对等线程的ID

PS:有关线程相关代码编译时要加上 -lpthread,是因为连接时需要使用库libpthread.a否则出错

  • 问题1:线程和对等线程们是如何存储的以达到安全地共享数据的效果。
  • 解决1

终止线程和回收线程

  • 一个线程是通过以下方式之一来终止的。

    • 当顶层的线程例程返回时,线程会隐式地终止。

    • 通过调用pthread_exit函数,线程会显式地终止。他会等待其他对等线程终止然后再终止主线程

void pthread_exit(void *thread_return);
  • 问题2:使用函数pthread_exit退出线程,这是线程的主动行为;由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,那被占用的资源怎么释放呢?
  • 解决:可以用pthread_join()函数来同步并释放资源。
int pthread_join(pthread_t tid,void **thread_return);
成功则返回0,出错则为非零
  • 实践实例
#include <stdio.h>  
#include <pthread.h>  
  
/*线程一*/  
void thread_1(void)  
{  
    int i=0;  
    for(i=0;i<=6;i++)  
    {  
        printf("This is a pthread_1.\n");  
        if(i==2)  
            pthread_exit(0);                      //用pthread_exit()来调用线程的返回值,用来退出线程,但是退出线程所占用的资源不会随着线程的终止而得到释放  
        sleep(1);  
    }  
}  
  
/*线程二*/  
void thread_2(void)  
{  
    int i;  
    for(i=0;i<3;i++)  
        printf("This is a pthread_2.\n");           
    pthread_exit(0);                              //用pthread_exit()来调用线程的返回值,用来退出线程,但是退出线程所占用的资源不会随着线程的终止而得到释放  
}  
  
int main(void)  
{  
    pthread_t id_1,id_2;  
    int i,ret;  
/*创建线程一*/  
    ret=pthread_create(&id_1,NULL,(void  *) thread_1,NULL);  
    if(ret!=0)  
    {  
        printf("Create pthread error!\n");  
    return -1;  
    }  
/*创建线程二*/  
     ret=pthread_create(&id_2,NULL,(void  *) thread_2,NULL);  
    if(ret!=0)  
    {  
        printf("Create pthread error!\n");  
    return -1;  
    }  
/*等待线程结束*/  
    pthread_join(id_1,NULL);  
    pthread_join(id_2,NULL);  
    return 0;  
}  
  • pthread_exit()调用线程的返回值,可由其他函数如pthread_join来检索获取

分离线程

  • 在任何一个时间点上,线程是可结合或可分离的。一个可结合的线程能够被其他线程收回其资源和杀死,在被回收之前,它的存储器资源是没有被释放的。分离的线程则相反,资源在其终止时自动释放。
int pthread_deacth(pthread_t tid);
成功则返回0,出错则为非零

  • 实例
 #include <pthread.h>
 #include <stdio.h>
            void *doit(void *arg){
                printf("arg...\n");
                sleep(1);
                return  NULL;
            }
            int main(void){
                pthread_t tid;
                pthread_create(&tid,NULL,doit,NULL);
                pthread_detach(tid);
            sleep(1);
                return 0;
            }

初始化线程

  • pthread_once允许初始化与线程例程相关的状态。
pthread_once_t once_control=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_contro,

void (*init_routine)(void));
总是返回0

用信号量实现同步/互斥

  • 转换规则:

    • 合法的转换是向右或者向上,即某一个线程中的一条指令完成
    • 两条指令不能在同一时刻完成,即不允许出现对角线
    • 程序不能反向运行,即不能出现向下或向左
  • 信号量与操作系统中的没有区别

    1.P(s):如果s是非零的,那么P将s减一,并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零。

    2.V(s):将s加一,如果有任何线程阻塞在P操作等待s变为非零,那么V操作会重启线程中的一个,然后该线程将s减一,完成P操作。

int sem_init(sem_t *sem,0,unsigned int value);//将信号量初始化为value

int sem_wait(sem_t *s);//P(s)
int sem_post(sem_t *s);//V(s)
  • 用信号量实现同步实例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

void *thread_function(void *arg);
sem_t bin_sem;

#define WORK_SIZE 1024
char work_area[WORK_SIZE];      /* 用来存放输入内容 */

int main() {
  int res;                    /* 暂存一些命令的返回结果 */
  pthread_t a_thread;         /* 织带新建的线程 */
  void *thread_result;       /* 存放线程处理结果 */

  res = sem_init(&bin_sem, 0, 0);   /* 初始化信号量,并且设置初始值为0*/
  if (res != 0) {
    perror("Semaphore initialization failed");
    exit(EXIT_FAILURE);
  }
  res = pthread_create(&a_thread, NULL, thread_function, NULL);   /* 创建新线程 */
  if (res != 0) {
    perror("Thread creation failed");
    exit(EXIT_FAILURE);
  }
  printf("Inout some text, Enter 'end' to finish\n");
  while(strncmp("end", work_area, 3) != 0) {             /* 当工作区内不是以end开头的字符串时...*/
    fgets(work_area, WORK_SIZE, stdin);                  /* 从标准输入获取输入到worl_area */
    sem_post(&bin_sem);                                  /* 信号量+1 */
  }
  printf("\nWaiting for thread to finish...\n");
  res = pthread_join(a_thread, &thread_result);         /* 等待线程结束 */
  if (res != 0) {
    perror("Thread join failed");
    exit(EXIT_FAILURE);
  }
  printf("Thread joined\n");
  sem_destroy(&bin_sem);                               /* 销毁信号量 */
  exit(EXIT_SUCCESS);
}

void *thread_function(void *arg) {
  sem_wait(&bin_sem);                                 /* 等待信号量有大于0的值然后-1 */
  while(strncmp("end", work_area, 3) != 0) {
    printf("You input %ld characters\n", strlen(work_area)-1);   /* 获取输入字符串长度 8*/
    sem_wait(&bin_sem);                               /* 等待信号量有大于0的值然后-1 */
  }
  pthread_exit(NULL);
}
  • 获取输入的字符串长度,wait信号量有大于0的值然后-1

  • 用信号量实现互斥在调度共享资源的时候尤其重要

    • 二元信号量(互斥锁):将每个共享变量与一个信号量s联系起来,然后用P(s)(加锁)和V(s)(解锁)操作将相应的临界区包围起来。

    • 禁止区:s<0,因为信号量的不变性,没有实际可行的轨迹线能够直接接触不安全区的部分

  • 互斥实例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

void *thread_function(void *arg);
pthread_mutex_t work_mutex;

#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;                           /* 用来控制是否退出*/

int main() {
  int res;
  pthread_t a_thread;
  void *thread_result;

  res = pthread_mutex_init(&work_mutex,NULL);    /* 初始化一个互斥锁 */
  if (res != 0) {
    perror("Mutex initialization failed");
    exit(EXIT_FAILURE);
  }
  res = pthread_create(&a_thread, NULL, thread_function, NULL);  /* 创建一个新线程 */
  if (res != 0) {
    perror("Thread creation failed");
    exit(EXIT_FAILURE);
  }
  pthread_mutex_lock(&work_mutex);                       /* 尝试对互斥量加锁 */
  printf("Input some text, Enter 'end' to finish\n");
  while(!time_to_exit) {                                   /* 检查是不是该退出*/
    fgets(work_area, WORK_SIZE, stdin);                   /* 从标准输入获取输入到work_area */
    pthread_mutex_unlock(&work_mutex);                   /* 解锁互斥量 */
    while(1) {
      pthread_mutex_lock(&work_mutex);
      if (work_area[0] != '\0') {                      /* 持续检查work_area 是否为空, 如果不为空继续等待,如果为空,则重新读取输入到work_area*/
        pthread_mutex_unlock(&work_mutex);
        sleep(1);
      }
      else {
        break;
      }
    }
  }
  pthread_mutex_unlock(&work_mutex);
  printf("\nWaiting for thread to finish...\n");
  res = pthread_join(a_thread, &thread_result);
  if (res != 0) {
    perror("Thread join failed");
    exit(EXIT_FAILURE);
  }
  printf("Thread joined\n");
  pthread_mutex_destroy(&work_mutex);
  exit(EXIT_SUCCESS);
}

void *thread_function(void *arg) {
  sleep(1);
  pthread_mutex_lock(&work_mutex);                     /* 尝试加锁互斥量 */
  while(strncmp("end", work_area, 3) != 0) {           /* 当work_area里的值不是以end开头时*/
    printf("You input %ld characters\n", strlen(work_area) -1);     /* 输出输入的字符长度 */
    work_area[0] = '\0';                                      /* work_area设置为空 */
    pthread_mutex_unlock(&work_mutex);
    sleep(1);
    pthread_mutex_lock(&work_mutex);
    while (work_area[0] == '\0') {              /* 持续检查work_area 直到它里面有输入值*/
      pthread_mutex_unlock(&work_mutex);
      sleep(1);
      pthread_mutex_lock(&work_mutex);
    }
  }
  time_to_exit = 1;                        /* 当输入end后,设置退出标志 */
  work_area[0] = '\0';
  pthread_mutex_unlock(&work_mutex);
  pthread_exit(0);
}
  • 实现结果与同步没有区别,但是重点是互斥锁的加和解

详细学习了一下互斥锁

参考互斥锁
互斥锁的基本流程为:

初始化一个互斥锁:pthread_mutex_init()函数

加锁:pthread_mutex_lock()函数或者pthread_mutex_trylock()函数

对共享资源的操作

解锁:pthread_mutex_unlock()函数

注销互斥锁:pthread_mutex_destory()函数
  • 加锁需要注意:
    pthread_mutex_lock()函数和pthread_mutex_trylock()函数的过程略有不同:
  1. 当使用pthread_mutex_lock()函数进行加锁时,若此时已经被锁,则尝试加锁的线程会被阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock()函数有返回值时,说明加锁成功;

  2. 而使用pthread_mutex_trylock()函数进行加锁时,若此时已经被锁,则会返回EBUSY的错误码。

  • 解锁的两个条件:

    解锁前,互斥锁必须处于锁定状态;

    必须由加锁的线程进行解锁。

感想:个人感觉互斥锁与操作系统中提到的门很像,加锁就是关门,当达到加锁条件时关门,只有等门里边的执行完,才能解锁。虽然互斥锁叫互斥锁,但是他是用来解决同步问题的,使得执行起来更有条理,不会乱。

以下为几个重要问题的学习

生产者-消费者问题

新感悟:再参考了一下网上的资料进行了学习,相比当时在操作系统上的理论学习和书本上关于同步互斥的讲解又有了新的理解,很多网上的资料都说要先同步再互斥,但我感觉这样并不是必须的,如果生产者中先同步再互斥,而消费者先互斥再同 步,或反之,都不会出现死锁,因为它们间并没有交叉关系。但是先同步,再互斥可以得到更 好的并发性。

PS:以下两句顺序不要变

sem_wait(&room_sem);  
pthread_mutex_lock(&mutex);  

#include 
#include 
#include 
#include 
#include
#define N 2   // 消费者或者生产者的数目
#define M 10 // 缓冲数目
int in = 0;   // 生产者放置产品的位置
int out = 0; // 消费者取产品的位置
int buff[M] = {0}; // 缓冲初始化为0, 开始时没有产品
sem_t empty_sem; // 同步信号量, 当满了时阻止生产者放产品
sem_t full_sem;   // 同步信号量, 当没产品时阻止消费者消费
pthread_mutex_t mutex; // 互斥信号量, 一次只有一个线程访问缓冲
int product_id = 0;   //生产者id
int prochase_id = 0; //消费者id
/* 打印缓冲情况 */
void print()
{
int i;
for(i = 0; i < M; i++)
   printf("%d ", buff[i]);
printf("\n");
}
/* 生产者方法 */ 
void *product()
{
int id = ++product_id;

while(1)
{
   // 用sleep的数量可以调节生产和消费的速度,便于观察
   sleep(1);
   //sleep(1);
  
   sem_wait(&empty_sem);
   pthread_mutex_lock(&mutex);
  
   in = in % M;
   printf("product%d in %d. like: \t", id, in);
  
   buff[in] = 1;  
   print();  
   ++in;
  
   pthread_mutex_unlock(&mutex);
   sem_post(&full_sem);  
}
}
/* 消费者方法 */
void *prochase()
{
int id = ++prochase_id;
while(1)
{
   // 用sleep的数量可以调节生产和消费的速度,便于观察
   sleep(1);
//sleep(1);
  
   sem_wait(&full_sem);
   pthread_mutex_lock(&mutex);
  
   out = out % M;
   printf("prochase%d in %d. like: \t", id, out);
  
   buff[out] = 0;
   print();
   ++out;
  
   pthread_mutex_unlock(&mutex);
   sem_post(&empty_sem);
}
}
int main()
{
pthread_t id1[N];
pthread_t id2[N];
int i;
int ret[N];

// 初始化同步信号量
int ini1 = sem_init(&empty_sem, 0, M); 
int ini2 = sem_init(&full_sem, 0, 0);  
if(ini1 && ini2 != 0)
{
   printf("sem init failed \n");
   exit(1);
} 
//初始化互斥信号量 
int ini3 = pthread_mutex_init(&mutex, NULL);
if(ini3 != 0)
{
   printf("mutex init failed \n");
   exit(1);
} 
// 创建N个生产者线程
for(i = 0; i < N; i++)
{
   ret[i] = pthread_create(&id1[i], NULL, product, (void *)(&i));
   if(ret[i] != 0)
   {
    printf("product%d creation failed \n", i);
    exit(1);
   }
}
//创建N个消费者线程
for(i = 0; i < N; i++)
{
   ret[i] = pthread_create(&id2[i], NULL, prochase, NULL);
   if(ret[i] != 0)
   {
    printf("prochase%d creation failed \n", i);
    exit(1);
   }
}
//销毁线程
for(i = 0; i < N; i++)
{
   pthread_join(id1[i],NULL);
   pthread_join(id2[i],NULL);
}
exit(0); 
}

读者-写者问题

  • 写者优先

当新的写者希望写时,不允许该写者后续的读者访问数据区,但必须保证之前的读者读完。

  1. 有写者正在写或者等待写,须等到没有写者才能读

  2. 没有写者,可以读

  3. 写者与写者互斥。当其它写者正在写时,其它写者不能写。

  4. 写者与读者互斥。之前只有读者在读,当写者出现时,必须等到之前的读者都读完才能写。这尊重了之前读者的意愿。

  5. 写者可以有条件地插读者的队。当前有写者正写,有读者在等,这时来了新写者,新写者可以在那些读者之前执行。

代码:

#include "stdio.h"  
#include <stdlib.h>  
#include <pthread.h>  
  
  
#define N_WRITER 2 //写者数目  
#define N_READER 20 //读者数目  
#define W_SLEEP 1 //控制写频率  
#define R_SLEEP  0.5 //控制读频率  
  
  
pthread_t wid[N_WRITER],rid[N_READER];  
const int MAX_RAND = 1000;//产生的最大随机数  
int data = 0;  
int readerCnt = 0, writerCnt = 0;  
pthread_mutex_t accessReaderCnt = PTHREAD_MUTEX_INITIALIZER;  
pthread_mutex_t accessWriterCnt = PTHREAD_MUTEX_INITIALIZER;  
pthread_mutex_t writeLock = PTHREAD_MUTEX_INITIALIZER;  
pthread_mutex_t readerLock = PTHREAD_MUTEX_INITIALIZER;  
pthread_mutex_t outerLock = PTHREAD_MUTEX_INITIALIZER;  
  
void write()  
{  
    int rd = rand()%MAX_RAND;  
    printf("write %d\n",rd);  
    data = rd;  
}  
void read()  
{  
    printf("read %d\n",data);  
}  
void * writer(void * in)  
{  
    while(1)  
    {  
        pthread_mutex_lock(&accessWriterCnt);  
        {//临界区,希望修改 writerCnt,独占 writerCnt  
            writerCnt++;  
            if(writerCnt == 1){  
                //阻止后续的读者加入待读队列  
                pthread_mutex_lock(&readerLock);  
            }  
        }  
        pthread_mutex_unlock(&accessWriterCnt);  
          
          
        pthread_mutex_lock(&writeLock);  
        {//临界区,限制只有一个写者修改数据  
            write();  
        }  
        pthread_mutex_unlock(&writeLock);  
          
        pthread_mutex_lock(&accessWriterCnt);  
        {//临界区,希望修改 writerCnt,独占 writerCnt  
            writerCnt--;  
            if(writerCnt == 0){  
                //阻止后续的读者加入待读队列  
                pthread_mutex_unlock(&readerLock);  
            }  
        }  
        pthread_mutex_unlock(&accessWriterCnt);  
        sleep(W_SLEEP);  
    }  
    pthread_exit((void *) 0);  
}  
  
void * reader (void * in)  
{  
    while(1)  
    {  
        //假如写者锁定了readerLock,那么成千上万的读者被锁在这里  
        pthread_mutex_lock(&outerLock);  
        {//临界区  
            pthread_mutex_lock(&readerLock);//只被一个读者占有  
            {//临界区  
                pthread_mutex_lock(&accessReaderCnt);//代码段 1  
                {//临界区  
                    readerCnt++;  
                    if(readerCnt == 1){  
                        pthread_mutex_lock(&writeLock);  
                    }  
                }  
                pthread_mutex_unlock(&accessReaderCnt);  
            }  
            pthread_mutex_unlock(&readerLock);//释放时,写者将优先获得readerLock  
        }  
        pthread_mutex_unlock(&outerLock);  
  
        read();  
          
        pthread_mutex_lock(&accessReaderCnt);//代码段2  
        {//临界区  
            readerCnt--;  
            if(readerCnt == 0){  
                pthread_mutex_unlock(&writeLock);//在最后一个并发读者读完这里开始禁止写者执行写操作  
            }  
        }  
        pthread_mutex_unlock(&accessReaderCnt);  
          
        sleep(R_SLEEP);  
    }  
    pthread_exit((void *) 0);  
}  
  
int main()  
{  
    int i = 0;  
    for(i = 0; i < N_READER; i++)  
    {  
        pthread_create(&rid[i],NULL,reader,NULL);  
    }  
    for(i = 0; i < N_WRITER; i++)  
    {  
        pthread_create(&wid[i],NULL,writer,NULL);  
    }  
    while(1){  
        sleep(10);  
    }  
    return 0;  
}  

练习

  • 课本练习题12.1: 在下图中,并发服务器的第33行上,父进程关闭了已连接描述符后,子进程仍能够使用该描述符和客户端通信,为什么?

image

  • 课本练习题12-1解决:父进程派生子进程时,相关文件表中的引用计数从1增加到2。父进程一关闭描述符副本,引用计数就由2减少到1,因为内核不会关闭一个文件,直到文件表中的引用计数值变为0,所以子进程这边的连接端将保持打开。

  • 课本练习题12.5:在下图1中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图2中,基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程,为什么?

image

image

  • 课本练习题12-5解决:因为线程运行在同一个进程中,他们共享同一个描述符表,所以在描述符表中的引用计数与线程的多少是没有关系的都为1,因此只需要一个close就够了。

  • 课本练习题12.10下图所示的对第一类读者-写者问题的解答给予读者较高的优先级,但是从某种意义上说,这种优先级是很弱的,因为一个离开临界区的写者可能重启一个在等待的写者,而不是一个在等待的读者。描述一个场景,其中这种弱优先级会导致一群写者使得一个读者饥饿。

image

  • 课本练习题12-10解决:假设一个特殊的信号量实现为每一个信号量使用了一个LIFO的线程栈。当一个线程在P操作中阻塞在一个信号量上,它的ID就被压入栈中。类似地,V操作从栈中弹出栈顶的线程ID,并重启这个线程。根据这个栈的实现,一个在它的临界区中竞争的写者会简单的等待,直到在他释放这个信号量之前另一个写者阻塞在这个信号量上。在这种场景中,当两个写者来回地传递控制权时,正在等待的读者可能会永远的等待下去。

教材学习中的问题和解决过程

  • 问题1:显式终止与隐式终止的区别
  • 问题1解决方案:显示终止:就是自然终止或者显式调用ExitTread,隐式终止就是隐式调用pthread_exit。

其他问题加在上文中

代码调试中的问题和解决过程

在上文中

代码托管

结对及互评

本周结对学习情况

  • 20155326
    • 结对照片

    • 结对学习内容

      • 第四章内容

其他(感悟、思考等,可选)

本周再次学习了多线程、进程和并发的相关内容,虽然当时在操作系统也学习了,自己也学习了,但是这次再学的时候还是发现问题多多,在很多的理解方面都不细致,和结对对象讨论也会发现一些新的思路,而且感觉虽然在JAVA上也对相应内容进行了学习,但是在多线程多进程方面总是理不清的感觉,这次一起边实践边回顾,感觉这次学下来,效果更好,收获还是蛮多的。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 200小时
第一周 5/5 1/1 15/15
第二周 1/2 23/38
第三周 206/327 1/3 28/66
第四周 206/327 1/4 10/77
第五周 285/568 1/5 20/97 主要学习了汇编及反汇编的相关知识
第六周 160/683 3/8 20/107
第七周 / 2/10 20/127 第四章学习内容和第二次实验
第八周 2/12 22/149 第十一章、第十二章
第九周 408/ 2582 3/15 21/170 第六章内容、第三次实验、课后pwd的实现
第十一周 174 / 3035 3/18 20/200 第九章内容、实验四
第十三周 741/3776 2/22 20/220 第十二章再学习
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。

参考:软件工程软件的估计为什么这么难软件工程 估计方法

参考资料

posted on 2017-12-17 14:38  20155320罗佳琪  阅读(243)  评论(1编辑  收藏  举报