zoukankan      html  css  js  c++  java
  • visual studio C++ 使用OpenMP 进行并行计算

    https://blog.csdn.net/dengm155/article/details/78836447?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

    那么用openMP怎么实现并行数组求和呢?下面我们先给出一个基本的解决方案。该方案的思想是,首先生成一个数组sumArray,其长度为并行执行的线程的个数(默认情况下,该个数等于CPU的核数),在for循环里,让各个线程更新自己线程对应的sumArray里的元素,最后再将sumArray里的元素累加到sum里,代码如下
     
     1 #include <iostream>
     2 #include <omp.h>
     3 int main(){
     4     int sum = 0;
     5     int a[10] = {1,2,3,4,5,6,7,8,9,10};
     6     int coreNum = omp_get_num_procs();//获得处理器个数
     7     int* sumArray = new int[coreNum];//对应处理器个数,先生成一个数组
     8     for (int i=0;i<coreNum;i++)//将数组各元素初始化为0
     9         sumArray[i] = 0;
    10 #pragma omp parallel for
    11     for (int i=0;i<10;i++)
    12     {
    13         int k = omp_get_thread_num();//获得每个线程的ID
    14         sumArray[k] = sumArray[k]+a[i];
    15     }
    16     for (int i = 0;i<coreNum;i++)
    17         sum = sum + sumArray[i];
    18     std::cout<<"sum: "<<sum<<std::endl;
    19     return 0;
    20 }
     
    需要注意的是,在上面代码里,我们用omp_get_num_procs()函数来获取处理器个数,用omp_get_thread_num()函数来获得每个线程的ID,为了使用这两个函数,我们需要include <omp.h>。
    上面的代码虽然达到了目的,但它产生了较多的额外操作,比如要先生成数组sumArray,最后还要用一个for循环将它的各元素累加起来,有没有更简便的方式呢?答案是有,openMP为我们提供了另一个工具,归约(reduction),见下面代码:
     
     1 #include <iostream>
     2 int main(){
     3     int sum = 0;
     4     int a[10] = {1,2,3,4,5,6,7,8,9,10};
     5 #pragma omp parallel for reduction(+:sum)
     6     for (int i=0;i<10;i++)
     7         sum = sum + a[i];
     8     std::cout<<"sum: "<<sum<<std::endl;
     9     return 0;
    10 }
     
    上面代码里,我们在#pragma omp parallel for 后面加上了 reduction(+:sum),它的意思是告诉编译器:下面的for循环你要分成多个线程跑,但每个线程都要保存变量sum的拷贝,循环结束后,所有线程把自己的sum累加起来作为最后的输出。
    reduction虽然很方便,但它只支持一些基本操作,比如+,-,*,&,|,&&,||等。有些情况下,我们既要避免race condition,但涉及到的操作又超出了reduction的能力范围,应该怎么办呢?这就要用到openMP的另一个工具,critical。来看下面的例子,该例中我们求数组a的最大值,将结果保存在max里。
     
     1 #include <iostream>
     2 int main(){
     3     int max = 0;
     4     int a[10] = {11,2,33,49,113,20,321,250,689,16};
     5 #pragma omp parallel for
     6     for (int i=0;i<10;i++)
     7     {
     8         int temp = a[i];
     9 #pragma omp critical
    10         {
    11             if (temp > max)
    12                 max = temp;
    13         }
    14     }
    15     std::cout<<"max: "<<max<<std::endl;
    16     return 0;
    17 }
     
    上例中,for循环还是被自动分成N份来并行执行,但我们用#pragma omp critical将 if (temp > max) max = temp 括了起来,它的意思是:各个线程还是并行执行for里面的语句,但当你们执行到critical里面时,要注意有没有其他线程正在里面执行,如果有的话,要等其他线程执行完再进去执行。这样就避免了race condition问题,但显而易见,它的执行速度会变低,因为可能存在线程等待的情况。
    第二部分转载于:http://www.cnblogs.com/wzyj/p/4501348.html
    
    OpenMp之sections用法
    
    section语句是用在sections语句里用来将sections语句里的代码划分成几个不同的段
    #pragma omp [parallel] sections [子句] 
    { 
       #pragma omp section 
       { 
                代码块 
       }  
    } 
         当存在可选参数#pragma omp parallel sections时,块中的代码section才会并行处理,而#pragma omp  sections是串行的程序。详见下面的代码: 
    
    #include<stdio.h>
    
    #include<stdlib.h>
    
    #include<omp.h>
    
    #include <unistd.h>
    
    int main()
    
    {
    
     
    
     
    
       printf("parent threadid:%d
    ",omp_get_thread_num());
    
       #pragma omp  sections
    
       {
    
         #pragma omp section
    
         {
    
              printf("section 0,threadid=%d
    ",omp_get_thread_num());
    
              sleep(1);
    
         }
    
         #pragma omp section
    
         {
    
              printf("section 1,threadid=%d
    ",omp_get_thread_num());
    
              //sleep(1);
    
         }
    
         #pragma omp section
    
         {
    
              printf("section 2,threadid=%d
    ",omp_get_thread_num());
    
              sleep(1);
    
         }
    
       }
    
       #pragma omp parallel sections
    
       {
    
          #pragma omp section
    
         {
    
              printf("section 3,threadid=%d
    ",omp_get_thread_num());
    
              sleep(1);
    
         }
    
          #pragma omp section
    
         {
    
              printf("section 4,threadid=%d
    ",omp_get_thread_num());
    
              sleep(1);
    
         }
    
          #pragma omp section
    
         {
    
              printf("section 5,threadid=%d
    ",omp_get_thread_num());
    
              sleep(1);
    
         }
    
       }
    
     
    
     return 0;
    
    }
    输出结果为: 
    
    parent threadid:0
    section 0,threadid=0
    section 1,threadid=0
    section 2,threadid=0
    section 3,threadid=0
    section 4,threadid=2
    section 5,threadid=1
    
    针对上面的代码,首先应该明确下面几点:
       1. sections之间是串行的。主线程把section0~2执行完之后才执行的第二个sections。
       2.没有加parallel的sections里面的section是串行的,为此我专门sleep了一秒,结果显示0~2都是主线程做的。
       3.第二个sections里面是并行的,进程编号分别为0,21。
    问题来了,第二部分的0是不是主线程呢?还是第二部分新开的一个线程?为此需要真正输出每个线程在内核中的线程编号:
    
    #include<stdio.h>
    
    #include<stdlib.h>
    
    #include<omp.h>
    
    #include <unistd.h>
    
    #include <sys/types.h>
    
    #include <sys/syscall.h>
    
     
    
    int main()
    
    {
    
     
    
       printf("pid:%d,tid=%ld
    ",getpid(),syscall(SYS_gettid));
    
       #pragma omp sections
    
       {
    
         #pragma omp section
    
         {
    
              printf("section 0,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
         #pragma omp section
    
         {
    
              printf("section 1,tid=%ld
    ",syscall(SYS_gettid));
    
              //sleep(1);
    
         }
    
         #pragma omp section
    
         {
    
              printf("section 2,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
       }
    
       #pragma omp parallel sections
    
       {
    
          #pragma omp section
    
         {
    
              printf("section 3,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
          #pragma omp section
    
         {
    
              printf("section 4,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
          #pragma omp section
    
         {
    
              printf("section 5,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
       }
    
     
    
     return 0;
    
    }
    输出结果:
     
    pid:7619,tid=7619
    section 0,tid=7619
    section 1,tid=7619
    section 2,tid=7619
    section 5,tid=7621
    section 4,tid=7619
    section 3,tid=7620
    从结果中可以看出以下几点:
    OpenMP上说当程序执行到第二个sections是并行的,主线程是休眠的,一直等所有的子线程都执行完毕之后才唤醒,可是在第二个sections中有个线程id和主线程id一致?其实是不一致的,首先从两者的类型上来看,线程编号是long int的,而进程是int的,数字一致并不能说两者相同。另外一方面,在linuxthreads时代,线程称为轻量级进程(LWP),也就是每个线程就是个进程,每个线程(进程)ID不同;而从2.4.10后,采用NPTL(Native Posix Thread Library)的线程库, 各个线程同样是通过fork实现的,并且具备同一个父进程。
    主进程id为7619,同时它又有个线程id也是7619,又一次证明在linux中线程进程差别不大。
    猜测主线程并不是休眠,而是将原先的上下文保存,然后自身也作为并行的一份子进行并行程序的执行,当并行程序完成之后,再回复原先的上下文信息。
    下面是一个比较复杂的例子
    
    #include<stdio.h>
    
    #include<stdlib.h>
    
    #include<omp.h>
    
    #include <unistd.h>
    
    #include <sys/types.h>
    
    #include <sys/syscall.h>
    
     
    
    int main()
    
    {
    
    #pragma omp parallel
    
    {
    
       printf("pid:%d,tid=%ld
    ",getpid(),syscall(SYS_gettid));
    
       #pragma omp sections
    
       {
    
         #pragma omp section
    
         {
    
              printf("section 0,tid=%ld
    ",syscall(SYS_gettid));
    
              //sleep(1);
    
         }
    
         #pragma omp section
    
         {
    
              printf("section 1,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
         #pragma omp section
    
         {
    
              printf("section 2,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
       }
    
       #pragma omp sections
    
       {
    
          #pragma omp section
    
         {
    
              printf("section 3,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
          #pragma omp section
    
         {
    
              printf("section 4,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
          #pragma omp section
    
         {
    
              printf("section 5,tid=%ld
    ",syscall(SYS_gettid));
    
              sleep(1);
    
         }
    
       }
    
    }
    
     
    
     return 0;
    
    }
    
    输出结果:
     
    pid:7660,tid=7660
    section 0,tid=7660
    section 1,tid=7660
    pid:7660,tid=7662
    section 2,tid=7662
    pid:7660,tid=7663
    pid:7660,tid=7661
    section 3,tid=7660
    section 5,tid=7661
    section 4,tid=7662
     
    #pragma omp parallel里面的代码是并行处理的,但是并不意味着代码要执行N次(其中N为核数),sections之间是串行的,而并行的实际部分是sections内部的代码。当线程7660在处理0,1时,因为section1休眠1s,所以section2在此期间会被新的线程进行处理。第一个sections真正处理完成之后,第二个sections才开始并行处理。
    另外值得注意的是,printf并不是并行的函数,它是将结果输出到控制台中,可是控制台资源并不是共享的。当被某个线程占用之后,其余的线程只能等待,拿输出的结果为例。对于#pragma omp parallel里面的代码是并行的,可是线程之间还是有先后的次序的,次序和线程的创建时间有关,对于线程7660来说,本身就已经存在了,所以首先获得printf函数,而直到它执行section0里面的printf时,其他的线程还没有创建完毕,接着是setion1里面的printf,即使是这个时候有其他的线程创建完成了,也只能等待,在section1中,sleep了1秒钟,printf函数被新的线程使用,下面也如此。

    那么用openMP怎么实现并行数组求和呢?下面我们先给出一个基本的解决方案。该方案的思想是,首先生成一个数组sumArray,其长度为并行执行的线程的个数(默认情况下,该个数等于CPU的核数),在for循环里,让各个线程更新自己线程对应的sumArray里的元素,最后再将sumArray里的元素累加到sum里,代码如下

    复制代码
     1 #include <iostream>
    2 #include <omp.h>
    3 int main(){
    4 int sum = 0;
    5 int a[10] = {1,2,3,4,5,6,7,8,9,10};
    6 int coreNum = omp_get_num_procs();//获得处理器个数
    7 int* sumArray = new int[coreNum];//对应处理器个数,先生成一个数组
    8 for (int i=0;i<coreNum;i++)//将数组各元素初始化为0
    9 sumArray[i] = 0;
    10 #pragma omp parallel for
    11 for (int i=0;i<10;i++)
    12 {
    13 int k = omp_get_thread_num();//获得每个线程的ID
    14 sumArray[k] = sumArray[k]+a[i];
    15 }
    16 for (int i = 0;i<coreNum;i++)
    17 sum = sum + sumArray[i];
    18 std::cout<<"sum: "<<sum<<std::endl;
    19 return 0;
    20 }
    复制代码

    需要注意的是,在上面代码里,我们用omp_get_num_procs()函数来获取处理器个数,用omp_get_thread_num()函数来获得每个线程的ID,为了使用这两个函数,我们需要include <omp.h>。

    上面的代码虽然达到了目的,但它产生了较多的额外操作,比如要先生成数组sumArray,最后还要用一个for循环将它的各元素累加起来,有没有更简便的方式呢?答案是有,openMP为我们提供了另一个工具,归约(reduction),见下面代码:

    复制代码
     1 #include <iostream>
    2 int main(){
    3 int sum = 0;
    4 int a[10] = {1,2,3,4,5,6,7,8,9,10};
    5 #pragma omp parallel for reduction(+:sum)
    6 for (int i=0;i<10;i++)
    7 sum = sum + a[i];
    8 std::cout<<"sum: "<<sum<<std::endl;
    9 return 0;
    10 }
    复制代码

    上面代码里,我们在#pragma omp parallel for 后面加上了 reduction(+:sum),它的意思是告诉编译器:下面的for循环你要分成多个线程跑,但每个线程都要保存变量sum的拷贝,循环结束后,所有线程把自己的sum累加起来作为最后的输出。

    reduction虽然很方便,但它只支持一些基本操作,比如+,-,*,&,|,&&,||等。有些情况下,我们既要避免race condition,但涉及到的操作又超出了reduction的能力范围,应该怎么办呢?这就要用到openMP的另一个工具,critical。来看下面的例子,该例中我们求数组a的最大值,将结果保存在max里。

    复制代码
     1 #include <iostream>
    2 int main(){
    3 int max = 0;
    4 int a[10] = {11,2,33,49,113,20,321,250,689,16};
    5 #pragma omp parallel for
    6 for (int i=0;i<10;i++)
    7 {
    8 int temp = a[i];
    9 #pragma omp critical
    10 {
    11 if (temp > max)
    12 max = temp;
    13 }
    14 }
    15 std::cout<<"max: "<<max<<std::endl;
    16 return 0;
    17 }
    复制代码

    上例中,for循环还是被自动分成N份来并行执行,但我们用#pragma omp critical将 if (temp > max) max = temp 括了起来,它的意思是:各个线程还是并行执行for里面的语句,但当你们执行到critical里面时,要注意有没有其他线程正在里面执行,如果有的话,要等其他线程执行完再进去执行。这样就避免了race condition问题,但显而易见,它的执行速度会变低,因为可能存在线程等待的情况。

    第二部分转载于:http://www.cnblogs.com/wzyj/p/4501348.html

     

    OpenMp之sections用法

     

    section语句是用在sections语句里用来将sections语句里的代码划分成几个不同的段

    #pragma omp [parallel] sections [子句]
    {
       #pragma omp section
       {
                代码块
       } 
    }
         当存在可选参数#pragma omp parallel sections时,块中的代码section才会并行处理,而#pragma omp  sections是串行的程序。详见下面的代码:
    1.  
      #include<stdio.h>
    2.  
      #include<stdlib.h>
    3.  
      #include<omp.h>
    4.  
      #include <unistd.h>
    5.  
      int main()
    6.  
      {
    7.  
       
    8.  
       
    9.  
      printf("parent threadid:%d ",omp_get_thread_num());
    10.  
      #pragma omp sections
    11.  
      {
    12.  
      #pragma omp section
    13.  
      {
    14.  
      printf("section 0,threadid=%d ",omp_get_thread_num());
    15.  
      sleep(1);
    16.  
      }
    17.  
      #pragma omp section
    18.  
      {
    19.  
      printf("section 1,threadid=%d ",omp_get_thread_num());
    20.  
      //sleep(1);
    21.  
      }
    22.  
      #pragma omp section
    23.  
      {
    24.  
      printf("section 2,threadid=%d ",omp_get_thread_num());
    25.  
      sleep(1);
    26.  
      }
    27.  
      }
    28.  
      #pragma omp parallel sections
    29.  
      {
    30.  
      #pragma omp section
    31.  
      {
    32.  
      printf("section 3,threadid=%d ",omp_get_thread_num());
    33.  
      sleep(1);
    34.  
      }
    35.  
      #pragma omp section
    36.  
      {
    37.  
      printf("section 4,threadid=%d ",omp_get_thread_num());
    38.  
      sleep(1);
    39.  
      }
    40.  
      #pragma omp section
    41.  
      {
    42.  
      printf("section 5,threadid=%d ",omp_get_thread_num());
    43.  
      sleep(1);
    44.  
      }
    45.  
      }
    46.  
       
    47.  
      return 0;
    48.  
      }

    输出结果为:

    parent threadid:0
    section 0,threadid=0
    section 1,threadid=0
    section 2,threadid=0
    section 3,threadid=0
    section 4,threadid=2
    section 5,threadid=1

     

    针对上面的代码,首先应该明确下面几点:

       1. sections之间是串行的。主线程把section0~2执行完之后才执行的第二个sections。

       2.没有加parallel的sections里面的section是串行的,为此我专门sleep了一秒,结果显示0~2都是主线程做的。

       3.第二个sections里面是并行的,进程编号分别为0,2,1。

    问题来了,第二部分的0是不是主线程呢?还是第二部分新开的一个线程?为此需要真正输出每个线程在内核中的线程编号:

    1.  
      #include<stdio.h>
    2.  
      #include<stdlib.h>
    3.  
      #include<omp.h>
    4.  
      #include <unistd.h>
    5.  
      #include <sys/types.h>
    6.  
      #include <sys/syscall.h>
    7.  
       
    8.  
      int main()
    9.  
      {
    10.  
       
    11.  
      printf("pid:%d,tid=%ld ",getpid(),syscall(SYS_gettid));
    12.  
      #pragma omp sections
    13.  
      {
    14.  
      #pragma omp section
    15.  
      {
    16.  
      printf("section 0,tid=%ld ",syscall(SYS_gettid));
    17.  
      sleep(1);
    18.  
      }
    19.  
      #pragma omp section
    20.  
      {
    21.  
      printf("section 1,tid=%ld ",syscall(SYS_gettid));
    22.  
      //sleep(1);
    23.  
      }
    24.  
      #pragma omp section
    25.  
      {
    26.  
      printf("section 2,tid=%ld ",syscall(SYS_gettid));
    27.  
      sleep(1);
    28.  
      }
    29.  
      }
    30.  
      #pragma omp parallel sections
    31.  
      {
    32.  
      #pragma omp section
    33.  
      {
    34.  
      printf("section 3,tid=%ld ",syscall(SYS_gettid));
    35.  
      sleep(1);
    36.  
      }
    37.  
      #pragma omp section
    38.  
      {
    39.  
      printf("section 4,tid=%ld ",syscall(SYS_gettid));
    40.  
      sleep(1);
    41.  
      }
    42.  
      #pragma omp section
    43.  
      {
    44.  
      printf("section 5,tid=%ld ",syscall(SYS_gettid));
    45.  
      sleep(1);
    46.  
      }
    47.  
      }
    48.  
       
    49.  
      return 0;
    50.  
      }

    输出结果:

    复制代码
    pid:7619,tid=7619
    section 0,tid=7619
    section 1,tid=7619
    section 2,tid=7619
    section 5,tid=7621
    section 4,tid=7619
    section 3,tid=7620

    从结果中可以看出以下几点:

    1. OpenMP上说当程序执行到第二个sections是并行的,主线程是休眠的,一直等所有的子线程都执行完毕之后才唤醒,可是在第二个sections中有个线程id和主线程id一致?其实是不一致的,首先从两者的类型上来看,线程编号是long int的,而进程是int的,数字一致并不能说两者相同。另外一方面,在linuxthreads时代,线程称为轻量级进程(LWP),也就是每个线程就是个进程,每个线程(进程)ID不同;而从2.4.10后,采用NPTL(Native Posix Thread Library)的线程库, 各个线程同样是通过fork实现的,并且具备同一个父进程。
    2. 主进程id为7619,同时它又有个线程id也是7619,又一次证明在linux中线程进程差别不大。
    3. 猜测主线程并不是休眠,而是将原先的上下文保存,然后自身也作为并行的一份子进行并行程序的执行,当并行程序完成之后,再回复原先的上下文信息。

    下面是一个比较复杂的例子

    1.  
      #include<stdio.h>
    2.  
      #include<stdlib.h>
    3.  
      #include<omp.h>
    4.  
      #include <unistd.h>
    5.  
      #include <sys/types.h>
    6.  
      #include <sys/syscall.h>
    7.  
       
    8.  
      int main()
    9.  
      {
    10.  
      #pragma omp parallel
    11.  
      {
    12.  
      printf("pid:%d,tid=%ld ",getpid(),syscall(SYS_gettid));
    13.  
      #pragma omp sections
    14.  
      {
    15.  
      #pragma omp section
    16.  
      {
    17.  
      printf("section 0,tid=%ld ",syscall(SYS_gettid));
    18.  
      //sleep(1);
    19.  
      }
    20.  
      #pragma omp section
    21.  
      {
    22.  
      printf("section 1,tid=%ld ",syscall(SYS_gettid));
    23.  
      sleep(1);
    24.  
      }
    25.  
      #pragma omp section
    26.  
      {
    27.  
      printf("section 2,tid=%ld ",syscall(SYS_gettid));
    28.  
      sleep(1);
    29.  
      }
    30.  
      }
    31.  
      #pragma omp sections
    32.  
      {
    33.  
      #pragma omp section
    34.  
      {
    35.  
      printf("section 3,tid=%ld ",syscall(SYS_gettid));
    36.  
      sleep(1);
    37.  
      }
    38.  
      #pragma omp section
    39.  
      {
    40.  
      printf("section 4,tid=%ld ",syscall(SYS_gettid));
    41.  
      sleep(1);
    42.  
      }
    43.  
      #pragma omp section
    44.  
      {
    45.  
      printf("section 5,tid=%ld ",syscall(SYS_gettid));
    46.  
      sleep(1);
    47.  
      }
    48.  
      }
    49.  
      }
    50.  
       
    51.  
      return 0;
    52.  
      }

    输出结果:

    复制代码
    pid:7660,tid=7660
    section 0,tid=7660
    section 1,tid=7660
    pid:7660,tid=7662
    section 2,tid=7662
    pid:7660,tid=7663
    pid:7660,tid=7661
    section 3,tid=7660
    section 5,tid=7661
    section 4,tid=7662
    复制代码

    #pragma omp parallel里面的代码是并行处理的,但是并不意味着代码要执行N次(其中N为核数),sections之间是串行的,而并行的实际部分是sections内部的代码。当线程7660在处理0,1时,因为section1休眠1s,所以section2在此期间会被新的线程进行处理。第一个sections真正处理完成之后,第二个sections才开始并行处理。

    另外值得注意的是,printf并不是并行的函数,它是将结果输出到控制台中,可是控制台资源并不是共享的。当被某个线程占用之后,其余的线程只能等待,拿输出的结果为例。对于#pragma omp parallel里面的代码是并行的,可是线程之间还是有先后的次序的,次序和线程的创建时间有关,对于线程7660来说,本身就已经存在了,所以首先获得printf函数,而直到它执行section0里面的printf时,其他的线程还没有创建完毕,接着是setion1里面的printf,即使是这个时候有其他的线程创建完成了,也只能等待,在section1中,sleep了1秒钟,printf函数被新的线程使用,下面也如此。

  • 相关阅读:
    17年day4
    11.7NOIP模拟题
    P3389 【模板】高斯消元法
    洛谷 P2136 拉近距离 题解
    浅谈SPFA判负环
    洛谷 P1629 邮递员送信 题解
    洛谷 P1821 [USACO07FEB]银牛派对Silver Cow Party 题解
    洛谷 P3905 道路重建 题解
    洛谷 P1351 联合权值 题解
    洛谷 P4779 【模板】单源最短路径(标准版) 题解
  • 原文地址:https://www.cnblogs.com/profession/p/13697215.html
Copyright © 2011-2022 走看看