可变参数(...)学习笔记

http://topic.csdn.net/t/20041124/09/3582660.html原文链接

 

最近应CSDN的邀请,C/C++值班室的几位兄弟为C++电子杂志写了一些文章,现将我的稿件预先刊发在论坛上,请兄弟们批评指正。也欢迎大家为CSDN c/c++电子杂志专刊投稿。杂志详情请见http://emag.csdn.net/
                                               可变参数学习笔记
                                               作者:laomai(原创,转载时请注明来自CSDN  的论坛及c/c++电子杂志)
前言:
   本文在很大程度上改编自网友kevintz的“C语言中可变参数的用法”一文,在行文之前先向这位前辈表示真诚的敬意和感谢。
一、什么是可变参数
     我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: 
int   printf(   const  char*   format,  ...);  
     它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式: 
    printf( "%d ",i); 
    printf( "%s ",s); 
    printf( "the  number   is  %d   ,string   is:%s ",  i,   s);       
   以上这些东西已为大家所熟悉。但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助.

二、写一个简单的可变参数的C函数  
       先看例子程序。该函数至少有一个整数参数,其后是占位符…,表示后面参数的个数不定.  在这个例子里,所有的输入参数必须都是整数,函数的功能是打印所有参数的值.
函数代码如下:
//示例代码1:可变参数函数的使用
#include   "stdio.h "
#include   "stdarg.h "
void   simple_va_fun(int   start,  ...)  
{  
       va_list   arg_ptr; 
       int   nArgValue  =start;
       int   nArgCout=0;         //可变参数的数目
       va_start(arg_ptr,start);  //以固定参数的地址为起点确定变参的内存起始地址。
       do  
       {
              ++nArgCout;
              printf( "the  %d   th   arg:  %d\n ",nArgCout,nArgValue);         //输出各参数的值
              nArgValue  =   va_arg(arg_ptr,int);                                    //得到下一个可变参数的值
       }   while(nArgValue  !=   -1);                           
       return;  
}
int   main(int   argc,  char*   argv[])
{
       simple_va_fun(100,-1);  
       simple_va_fun(100,200,-1); 
       return   0;
}

从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:  
⑴在程序中将用到以下这些宏:  
void   va_start(   va_list  arg_ptr,   prev_param  );  
type   va_arg(   va_list  arg_ptr,   type  );  
void   va_end(   va_list  arg_ptr   ); 
va在这里是variable-argument(可变参数)的意思.  
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.
⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,也就是最后一个固定参数。
⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。然后进行输出。
⑸设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完下面这几个宏的内部实现机制后,自然就会明白。

三、可变参数在编译器中的处理  
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,  由于1)硬件平台的不同  2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装目录下的\vc98\include\stdarg.h)

typedef   char   *    va_list;
#define   _INTSIZEOF(n)      (  (sizeof(n)   +   sizeof(int)  -   1)  &   ~(sizeof(int)  -   1)  )
#define   va_start(ap,v)    (   ap   =  (va_list)&v   +  _INTSIZEOF(v)   )
#define   va_arg(ap,t)        (  *(t   *)((ap  +=   _INTSIZEOF(t))   -  _INTSIZEOF(t))   )
#define   va_end(ap)           (   ap  =   (va_list)0  )
下面我们解释这些代码的含义:
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。
3、va_start的定义为&v+_INTSIZEOF(v),而&v是最后一个固定参数的起始地址,再加上其大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap,  v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。 
这里要知道两个事情:
     ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
   (2)在VC等绝大多数C编译器中,参数进栈的顺序是由右向左的,因此,
参数进栈以后的内存模型如下图所示:最后一个固定参数的地址正好位于第一个可变参数之下,并且是连续存储的。
|——   —————————————|
|     最后一个固定参数                       |     -> 高内存地址处
|—   ——————————————|
     ........................   
|-------------------------------|
|   第N个可变参数                              |    ->va_arg(arg_ptr,datatype)后arg_ptr所指的地方
|-------------------------------|
     ...................
|———   ————————————|
|     第一个可变参数                          |         ->va_start(arg_ptr,start)后arg_ptr所指的地方
|                                                     |         即第一个可变参数的地址
|———————————————   |        
|—————————————   ——|
|                                                     |
|     最后一个固定参数                       |       ->    start的起始地址
|——————————————   —|            
         ...............
|——————————————-     |
|                                                     |    
|———————————————   |    ->  低内存地址处

(4)  va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了:
#define   va_arg(ap,t)        (  *(t   *)((ap  +=   _INTSIZEOF(t))   -  _INTSIZEOF(t))   )
这个宏做了两个事情,
     ①用用户输入的类型对参数地址进行强制类型转换,得到用户所需要的值
     ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。
(5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再  指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不  会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.  在这里大家要注意一个问题:由于参数的地址用于va_start宏,所  以参数不能声明为寄存器变量或作为函数或数组类型.   关于va_start,  va_arg,   va_end的描述就是这些了,我们要注意的  是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

四、可变参数在编程中要注意的问题  
因为va_start,   va_arg,  va_end等定义成宏,所以它显得很愚蠢,  可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能   地识别不同参数的个数和类型.  有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数  printf是从固定参数format字符串来分析出参数的类型,再调用va_arg  的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.  例如,在C的经典教材《the   c programming》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。

五、小结:  
1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
⑴在固定参数中设标志——   printf函数就是用这个办法。后面也有例子。
⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法——当可变参数的值为-1时,即认为得到参数列表的结尾。
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。这是一个不太方便
3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②参数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现—将不再使用的指针设为NULL,这样可以防止以后的误操作。
4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信有经验的读者就可以写出适合于自己机器的实现来。下面就是一个例子
六、实践——自己实现简单的可变参数的函数。
下面是一个简单的printf函数的实现,参考了 <The   C  programming  language> 中的156页的例子,读者可以结合书上的代码与本文参照。
#include   "stdio.h "
#include   "stdlib.h "
void   myprintf(char*   fmt,  ...)              //一个简单的类似于printf的实现,参数必须都是int   类型
{  
       char*   pArg=NULL;                         //等价于原来的va_list  
       char   c;
      
       pArg   =  &fmt;                    //注意不要写成p  =   fmt  !!因为这里要对参数取址,而不是取值
       pArg   +=  sizeof(fmt);         //等价于原来的va_start                 

       do
       {
              c  =*fmt;
              if   (c  !=   '% ')
              {
                    putchar(c);                     //照原样输出字符
              }
              else
            {
                    //按格式字符输出数据
                    switch(*++fmt)  
                   {
                     case  'd ':
                           printf( "%d ",*((int*)pArg));
                           break;
                     case  'x ':
                           printf( "%#x ",*((int*)pArg));
                         
                           break;
                    default:
                           break;
                     } 
                     pArg  +=   sizeof(int);                         //等价于原来的va_arg
              }
              ++fmt;
       }while   (*fmt  !=   '\0 '); 
       pArg   =  NULL;                                                     //等价于va_end
       return;  
}
int   main(int   argc,  char*   argv[])
{
       int   i   =  1234;
       int   j   =  5678;
      
       myprintf( "the   first  test:i=%d\n ",i,j);  
       myprintf( "the   secend  test:i=%d;   %x;j=%d;\n",i,0xabcd,j);  
       system( "pause ");
return   0;
}
在intel+win2k+vc6的机器执行结果如下:
the   first   test:i=1234
the   secend   test:i=1234;  0xabcd;j=5678;

 

 

对村长大作的一点儿补充:(关于可变参数实现的一些细节)

下面主要针对stdarg.h文件中的宏进行分析,由于stdarg.h涉及可变参数在多种平台上的

c/c++实现,且基本原理相同,所以为简单起见,本报告只分析x86平台上的c++实现。以下

是该特定环境下与可变参数实现相关的定义:

typedef   char   *    va_list;  //指向可变参数的指针

#define   _ADDRESSOF(v)      (  &reinterpret_cast <const  char  &> (v)   )  //用于取地址

#define   _INTSIZEOF(n)      (  (sizeof(n)   +   sizeof(int)  -   1)  &   ~(sizeof(int)  -   1)   ) 
//用于计算变量在堆栈中所占的空间

#define   va_start(ap,v)    (   ap   =  (va_list)_ADDRESSOF(v)   +  _INTSIZEOF(v)   ) 
//用于初始化可变参数的指针

#define   va_arg(ap,t)        (  *(t   *)((ap  +=   _INTSIZEOF(t))   -  _INTSIZEOF(t))   ) 
//用于获得当前指向的可变参数

#define   va_end(ap)           (   ap  =   (va_list)0  )   //指针归零

接下来是具体的分析:

1.   可变参数实现的基本原理。

       由于x86平台上c++采用堆栈式,从右向左压入的方法传递参数。所以第一个参数位于堆栈

       如果知道了栈顶的地址,又知道了每个参数的类型和数量,那么被调函数就可以从堆栈中

       取出参数了。当然,默认情况下,这些工作时由编译器完成的。但当被调函数的参数可变

       时,没有足够的参数类型和数量信息提供给编译器,所以编译器在编译时自动完成的工作

       就要程序员在编写程序时手动完成。这就是上面列出的宏所要完成的工作。

2.   具体实现。

       (1).   typedef  char   *    va_list;

             这个typedef定义比较简单,就是定义一个用于指向参数堆栈的指针。

              [注:为什么不使用void  *而使用了char   *,是因为

              (  &reinterpret_cast <const  void  &> (v)  )是不合法的。而为什么一

              定要用(  &reinterpret_cast <const  void  &> (v)  )呢?下面会有说明]

       (2).   #define  _ADDRESSOF(v)      (  &reinterpret_cast <const  char  &> (v)   )

             这个宏的作用就是取变量v的地址。它先把v重新解释为const   char  &,然后再取它的

             地址。后面会看到,它用于得到参数的栈顶地址。[注1:为什么不先取v的地址,然后

              再把它重新解释为const  char   *呢?(即,使用这样的宏:

              (  reinterpret_cast <const   char  *> (&v)  ))原因是在c++中,如果v是用户自定义的变

             量类型,那么用户可能会重载&运算符,那么&v返回的就不一定是这个变量在内存中的

             地址。因此,使用&v是不安全的。继而只能先将v重新解释为某个编译器内部的数据类

             型的引用,这时再取v的地址就完美了!当然,你不能将某个变量重新解释为void  &类

              型,这是非法的,这也恰恰是(1)中使用了char  *而没有使用void   *的原因。]

       (3).   #define  _INTSIZEOF(n)      (  (sizeof(n)   +   sizeof(int)  -   1)  &   ~(sizeof(int)  -   1)  )

             这个宏的作用是计算类型/变量n在堆栈中所占的空间。因为编译器将参数压入堆栈的

              时候是按照int类型的大小(16位编译器中为2  bytes,32位编译器中为4  bytes)进行

              对齐的。也就是说变量会被对齐到sizeof  (int)的整数倍边界上。而中间的空缺会用

             0(无符号数时)或0xff(带符号数时)填充。所以变量在堆栈中的大小可能会和实际的大

             小不一样。例如32位编译器中,char变量在堆栈中的大小会是4  bytes,而不是我们想

              象的1  byte。因此有必要对其大小进行重新计算。方法是将sizeof(n)的低位加上一个

              sizeof(int)  -   1,这样低位会在> =  1时产生一个进位。而后再用&~(sizeof(int)  -   1)

              将低位清零。这样的计算等同于下面的表达式:

              (sizeof(n)  /   sizeof(int))   ? 
                     sizeof(n)  +   sizeof(int)  -   sizeof(n)  %   sizeof(int)  :   sizeof(n)

       (4).   #define  va_start(ap,v)    (   ap   =  (va_list)_ADDRESSOF(v)   +  _INTSIZEOF(v)   ) 

             这个宏将指针ap指向第一个可变参数。即参数v后面的第一个参数。

       (5).   #define  va_arg(ap,t)        (  *(t   *)((ap  +=   _INTSIZEOF(t))   -  _INTSIZEOF(t))   )

             这个宏返回ap指向的t类型可变参数的引用,同时将ap指向下一个可变参数。

       (6).   #define  va_end(ap)           (   ap   =  (va_list)0   )

             作为一个好的习惯,当使用完一个指针以后,将指针归零。表示该指针不再有效

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java并发编程 背景介绍 并发历史 必要性 进程 资源分配的最小单位 线程 CPU调度的最小单位 线程的优势 (1)如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率 (2)建模简单:通过使用线程可以讲复杂并且异步的工作流进一步分解成一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置交互 (3)简化异步事件的处理:服务器应用程序在接受来自多个远程客户端的请求时,如果为每个连接都分配一个线程并且使用同步IO,就会降低开发难度 (4)用户界面具备更短的响应时间:现代GUI框架中大都使用一个事件分发线程(类似于中断响应函数)来替代主事件循环,当用户界面用有事件发生时,在事件线程中将调用对应的事件处理函数(类似于中断处理函数) 线程的风险 线程安全性:永远不发生糟糕的事情 活跃性问题:某件正确的事情迟早会发生 问题:希望正确的事情尽快发生 服务时间过长 响应不灵敏 吞吐率过低 资源消耗过高 可伸缩性较低 线程的应用场景 Timer 确保TimerTask访问的对象本身是线程安全的 Servlet和JSP Servlet本身要是线程安全的 正确协同一个Servlet访问多个Servlet共享的信息 远程方法调用(RMI) 正确协同多个对象中的共享状态 正确协同远程对象本身状态的访问 Swing和AWT 事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 定义 当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的 无状态对象一定是线程安全的,大多数Servlet都是无状态的 原子性 一组不可分割的操作 竞态条件 基于一种可能失效的观察结果来做出判断或执行某个计算 复合操作:执行复合操作期间,要持有锁 锁的作用 加锁机制、用锁保护状态、实现共享访问 锁的不恰当使用可能会引起程序性能下降 对象的共享使用策略 线程封闭:线程封闭的对象只能由一个线程拥有并修改 Ad-hoc线程封闭 栈封闭 ThreadLocal类 只读共享:不变对象一定是线程安全的 尽量将域声明为final类型,除非它们必须是可变的 分类 不可变对象 事实不可变对象 线程安全共享 封装有助于管理复杂度 线程安全的对象在其内部实现同步,因此多个接口可以通过公有接口来进行访问 保护对象:被保护的对象只能通过特定的锁来访问 将对象封装到线程安全对象中 由特定锁保护 保护对象的方法 对象的组合 设计线程安全的类 实例封闭 线程安全的委托 委托是创建线程安全类的最有效策略,只需要让现有的线程安全类管理所有的状态 在现有线程安全类中添加功能 将同步策略文档化 基础构建模块 同步容器类 分类 Vector Hashtable 实现线程安全的方式 将状态封装起来,对每个公有方法都进行同步 存在的问题 复合操作 修正方式 客户端加锁 迭代器 并发容器 ConcurrentHashMap 用于替代同步且基于散列的Map CopyOnWriteArrayList 用于在遍历操作为主要操作的情况下替代同步的List Queue ConcurrentLinkedQueue *BlockingQueue 提供了可阻塞的put和take方法 生产者-消费者模式 中断的处理策略 传递InterruptedException 恢复中断,让更高层的代码处理 PriorityQueue(非并发) ConcurrentSkipListMap 替代同步的SortedMap ConcurrentSkipListSet 替代同步的SortedSet Java 5 Java 6 同步工具类 闭锁 *应用场景 (1)确保某个计算在其需要的所有资源都被初始化后才能继续执行 (2)确保某个服务在其所依赖的所有其他服务都已经启动之后才启动 (3)等待知道某个操作的所有参与者都就绪再继续执行 CountDownLatch:可以使一个或多个线程等待一组事件发生 FutureTask *应用场景 (1)用作异步任务使用,且可以使用get方法获取任务的结果 (2)用于表示一些时间较长的计算 状态 等待运行 正在运行 运行完成 使用Callable对象实例化FutureTask类 信号量(Semaphore) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量 管理者一组虚拟的许可。acquire获得许可(相当于P操作),release释放许可(相当于V操作) 应用场景 (1)二值信号量可用作互斥体(mutex) (2)实现资源池,例如数据库连接池 (3)使用信号量将任何一种容器变成有界阻塞容器 栅栏 能够阻塞一组线程直到某个事件发生 栅栏和闭锁的区别 所有线程必须同时到达栅栏位置,才能继续执行 闭锁用于等待事件,而栅栏用于等待线程 栅栏可以重用 形式 CyclicBarrier 可以让一定数量的参与线程反复地在栅栏位置汇集 应用场景在并行迭代算法中非常有用 Exchanger 这是一种两方栅栏,各方在栅栏位置上交换数据。 应用场景:当两方执行不对称的操作(读和取) 线程池 任务与执行策略之间的隐形耦合 线程饥饿死锁 运行时间较长的任务 设置线程池的大小 配置ThreadPoolExecutor 构造参数 corePoolSize 核心线程数大小,当线程数= corePoolSize的时候,会把runnable放入workQueue中 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了” keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。 workQueue 保存任务的阻塞队列 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务 threadFactory 创建线程的工厂 handler 拒绝策略 unit 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值 线程的创建与销毁 管理队列任务 饱和策略 AbortPolicy DiscardPolicy DiscardOldestPolicy CallerRunsPolicy 线程工厂 在调用构造函数后再定制ThreadPoolExecutor 扩展 ThreadPoolExecutor afterExecute(Runnable r, Throwable t) beforeExecute(Thread t, Runnable r) terminated 递归算法的并行化 构建并发应用程序 任务执行 在线程中执行任务 清晰的任务边界以及明确的任务执行策略 任务边界 大多数服务器以独立的客户请求为界 在每个请求中还可以发现可并行的部分 任务执行策略 在什么(What)线程中执行任务? 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)? 有多少个(How Many)任务能并发执行? 在队列中有多少个(How Many)任务在等待执行? 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝? 在执行一个任务之前或之后,应该进行什么(What)动作? 使用Exector框架 线程池 newFixedThreadPool(固定长度的线程池) newCachedThreadPool(不限规模的线程池) newSingleThreadPool(单线程线程池) newScheduledThreadPool(带延迟/定时的固定长度线程池) 具体如何使用可以查看JDK文档 找出可利用的并行性 某些应用程序中存在比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性 任务的取消和关闭 任务取消 停止基于线程的服务 处理非正常的线程终止 JVM关闭 线程池的定制化使用 任务和执行策略之间的隐性耦合 线程池的大小 配置ThreadPoolExecutor(自定义的线程池) 此处需要注意系统默认提供的线程池是如何配置的 扩展ThreadPoolExector GUI应用程序探讨 活跃度(Liveness)、性能、测试 避免活跃性危险 死锁 锁顺序死锁 资源死锁 动态的锁顺序死锁 开放调用 在协作对象之间发生的死锁 死锁的避免与诊断 支持定时的显示锁 通过线程转储信息来分析死锁 其他活跃性危险 饥饿 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。 糟糕的响应性 如果由其他线程完成的工作都是后台任务,那么应该降低它们的优先级,从而提高前台程序的响应性。 活锁 要解决这种活锁问题,需要在重试机制中引入随机性(randomness)。为了避免这种情况发生,需要让它们分别等待一段随机的时间 性能与可伸缩性 概念 运行速度(服务时间、延时) 处理能力(吞吐量、计算容量) 可伸缩性:当增加计算资源时,程序的处理能力变强 如何提升可伸缩性 Java并发程序中的串行,主要来自独占的资源锁 优化策略 缩
一直以为机器学习的重点在于设计精巧、神秘的算法来模拟人类解决问题。学了这门课程才明白如何根据实际问题优化、调整模型更为重要。事实上,机器学习所使用的核心算法几十年来都没变过。 什么是机器学习呢?以二类分类监督学习为例,假设我们已经有了一堆训练数据,每个训练样本可以看作n维空间里的一个点,那么机器学习的目标就是利用统计算法算出一个将这个n维空间分成两个部分(也就是把空间切成两半)的分界面,使得相同类别的训练数据在同一个部分里(在分界面的同侧)。而所用的统计算法无非是数学最优化理论的那些算法,梯度下降法等等。 在机器学习的模型中,神经网络是一个比较特殊的模型。因为它比较万能。万能二字可不是随便说说的,有定理为证,万能近似定理说,当神经网络的隐藏单元足够多,它就能逼近任意函数。也就是说,只要提供的训练数据量充足,就一定能用一个隐藏单元够多的神经网络去拟合这些训练数据。然而神经网络也有一个很严重的缺点:收敛速度太慢。这一缺点导致很长时间以来神经网络基本上都只能当作理论的标杆而很少被应用于实际问题。 近年来神经网络的兴起得益于三点:1. 算法进展;2. 大数据;3. 硬件提升。这三点使得神经网络(特别是深层网络)的训练速度大幅度提升。前面有说到,模型优化调整过程对于模型的建立至关重要。使用机器学习解决实际问题是一个持续迭代探索优化的过程,需要不断地试错。就好比在走迷宫,你不可能一开始就知道正确的路线在哪,只能加快步伐,尽可能快,尽可能早地走过每一条死路,并祈祷出口是存在着的。优化调整需要反复地训练模型,观察结果。在以前,一次训练可能耗时几个月甚至几年,这种情况下进行迭代调优的时间成本是不可接受的。而现在一次迭代可能只需要很短的一段时间,同时并发技术也使得同时训练不同参数的模型的方案变得可行。快速迭代,优化调整,使神经网络能够越来越多的应用于各种实际问题。 吴恩达的课程数学上是比较基础的。课程前面部分讲解了神经网络相关的主要算法,后面则侧重于讲工程上如何使用各种策略来调整优化模型使之能够快速地拟合实际问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值