zoukankan      html  css  js  c++  java
  • 《网络编程》线程

    线程基本函数


            当一个程序被启动时,仅仅有一个主线程,若要实现对其它线程的基本操作,首先必须创建新的线程,新的线程创建能够使用 pthread_create 函数实现,该函数的 API 定义例如以下:


    /* 函数功能:创建新的线程;
     * 返回值:若成功则返回0,若出错则返回正的错误码;
     * 函数原型:
     */
    #include <pthread.h>
    
    int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
                        void*(*func)(void*), void *arg);
    /* 说明:
     * 当该函数成功返回时。由tid指向的内存单元被设置为新创建线程的线程ID;
     * attr參数用于设置线程的属性,若为NULL时,表示採用默认属性创建该线程;
     * 新创建的线程从func函数的地址開始执行。该函数仅仅有一个參数arg;
     * 若须要向func函数传递多个參数。则必须把须要传递的參数包装成一个结构体。
     * 然后把该结构体的地址作为arg參数传入。
     */
    


            在进程中。若调用了函数 exit,_exit。或_Exit 时,则该进程会终止,相同,若进程中的线程调用这三个函数时也会使线程所在的进程终止。

    那么若仅仅是想退出线程,而不终止线程所在的进程有什么办法?以下是在单线程模式下退出线程的三种方式(不会终止线程所在的进程):

    1. 线程仅仅是从启动例程中返回,返回值是线程的退出码;
    2. 线程被同一进程的其它线程取消;
    3. 线程调用 pthread_exit 函数。
           当一个线程被创建好之后,运行完任务之后。我们能够调用 pthread_exit 函数终止一个线程,该函数的 API 定义例如以下:

    /* 函数功能:终止一个线程。
     * 返回值:无。
     * 函数原型:
     */
    #include <pthread.h>
    void pthread_exit(void *status);
    
    /* 说明:
     * 若本线程不处于脱离状态,则其线程ID和退出状态码将一直保留到调用进程内的某个其它线程对它调用pthread_join函数;
     * status是向 线程的回收者传递其退出信息,运行完之后该信息不会返回给调用者。
     */
    


            通常父进程须要调用 wait 函数族等待子进程终止,避免子进程成为僵尸进程。

    在线程中为确保终止线程的资源对进程可用,即回收终止线程的资源,应该在每一个线程结束时分离它们。一个没有被分离的线程终止时会保留其虚拟内存,包含它们的堆栈和其它系统资源。

    分离线程意味着通知系统不再须要此线程,同意系统将分配给它的资源回收。

            调用 pthread_join 函数将自己主动分离指定的线程,被分离的线程就再也不能被其它线程连接了。即恢复了系统资源。若线程已处于分离状态,调用pthread_join 会失败,将返回EINVAL。所以,假设多个线程须要知道某个特定的线程何时结束,则这些线程应该等待某个条件变量而不是调用 pthread_join。一个进程中的全部线程都能够调用 pthread_join 函数来等待其它线程终止(即回收其它线程的系统资源),该函数的 API 定义例如以下:


    /* 函数功能:等待一个线程终止;
     * 返回值:若成功则返回0,若错误则返回正的错误码。
     * 函数原型:
     */
    #include <pthread.h>
    
    int pthread_join(pthread_t tid, void **status);
    /* 说明:
     * 一个进程中的全部线程能够调用该函数回收其它线程。即等待其它线程终止;
     * tid參数是目标线程的标识符,status參数是保存目标线程返回的退出码;
     * 该函数会一直堵塞,直到被回收的线程终止。
     */
    

           每一个线程都有自身的线程 ID ,线程 ID 由创建线程的函数 pthread_create 返回。能够使用函数 pthread_self 获取自身的线程 ID ,其 API 定义例如以下:


    /* 函数功能:获取自身的线程ID;
     * 返回值:返回调用线程的线程ID;
     * 函数原型:
     */
    #include <pthread.h>
    pthread_t pthread_self(void);
    

           一个线程或是可汇合状态(默认),或是脱离状态。当一个可汇合状态的线程终止时,它的线程 ID 和退出状态将保留到还有一个线程对其调用 pthread_join。

    脱离状态的线程像守护进程一样,当它终止时,全部相关资源都被释放,我们不能等待他们终止。若一个线程须要知道还有一个线程啥时候终止,那最好保持第二个线程的可汇合状态。

            调用 pthread_detach 函数可将指定的线程变为脱离状态。其定义例如以下:


    /* 函数功能:把指定的线程变为脱离状态;
     * 返回值:若成功则返回0。若出错则返回正的错误码。
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_detach(pthread_t tid);
    

           同一个进程中的全部线程能够调用 pthread_cancel 函数向请求取消其它线程,被请求取消的线程能够选择同意取消或怎样取消。取消线程相当于线程异常终止,该函数定义例如以下:


    /* 函数功能:请求取消同一进程的其它线程。
     * 返回值:若成功则返回0,出错则返回正的错误码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_cancel(pthread_t tid);
    /* 说明:
     * tid參数是目标线程的标识符;
     * 尽管能够请求取消某个线程,可是该线程能够决定是否同意被取消或者怎样取消,这分别由下面两个函数完毕:
     */
    #include <pthread.h>
    int pthread_setcancelstate(int state, int *oldstate);
    int pthread_setcanceltype(int type, int *oldtype);
    /* 说明:
     * 这两个函数中的第一个參数是分别用于设置取消状态(即是否同意取消)和取消类型(即怎样取消)。第二个參数则是分别记录线程原来的取消状态和取消类型。
     * state有两个可选的取值:
     * 1、PTHREAD_CANCEL_ENABLE     同意线程被取消(默认情况);
     * 2、PTHREAD_CANCEL_DISABLE    禁止线程被取消,这样的情况,若一个线程收到取消请求,则它会将请求挂起。直到该线程同意被取消。
     *
     * type也有两个可选取值:
     * 1、PTHREAD_CANCEL_ASYNCHRONOUS   线程随时能够被取消;
     * 2、PTHREAD_CANCEL_DEFERRED       同意目标线程推迟运行;
     */
    


    线程属性


            在前面介绍的线程操作中都是採用线程的默认属性进程操作。

    在创建新的线程时,我们能够使用系统默认的属性,也能够自己指定线程的主要属性。我们能够指定 pthread_attr_t 结构改动线程的默认属性。并把这个属性与创建线程联系起来。以下先看下线程的主要属性:


    /* 结构体定义 */
    #include <bits/pthreadtypes.h>
    #define __SIZEOF_PTHREAD_ATTR_T 36
    
    typedef union
    {
        char __size[__SIZEOF_PTHREAD_ATTR_T];
        long int __align;
    }pthread_attr_t;
    

         线程属性主要包含下面四种属性:


    /* 
     * 线程的主要属性: 
     * (1)detachstate     线程的脱离状态属性; 
     * (2)guardsize       线程栈末尾的警戒缓冲区大小(字节数); 
     * (3)stackaddr       线程栈的最低地址; 
     * (4)stacksize       线程栈的大小(字节数)。 
     */

        在进行线程属性操作之前必须对其进行初始化,初始化函数定义例如以下:


    /*
     * 函数功能:初始化属性结构。
     * 返回值:若成功则返回0,否则返回错误编码。
     * 函数原型:
     */
    #include <pthread.h>
    /* 初始化线程属性对象 */
    int pthread_attr_init(pthread_attr_t *attr);
    /* 若线程属性是动态分配内存的,则在释放内存之前。必须调用该函数销毁线程属性对象 */
    int pthread_attr_destroy(pthread_attr_t *attr);
    


    脱离状态属性

            我们能够通过 pthread_attr_t 结构改动线程脱离状态属性 detachstate,以下是关于对该属性操作的函数:


    /*
     * 函数功能:改动线程的分离状态属性。
     * 返回值:若成功则返回0。否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);//获取当前线程的分离状态属性;
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//设置当前线程的分离状态属性。
    /*
     * 说明:
     * detachstate的值为下面两种:
     * (1)PTHREAD_CREATE_DETACHED 以分离状态启动线程。
     * (2)PTHREAD_CREATE_JOINABLE 正常启动线程(默认,就可以汇合状态)。
     */
    


    栈属性

            能够通过以下的操作函数来获取或者改动线程的栈属性。


    /*
     * 函数功能:获取或改动线程的栈属性。
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_attr_getstack(const pthread_attr_t *attr, void ** stackaddr, size_t stacksize);//获取线程栈信息。
    int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);//改动线程栈信息;
    
    int pthread_attr_getstackaddr(const pthread_attr_t *attr, void ** stackaddr);//获取线程栈起始地址信息;
    int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);//改动线程栈起始地址信息;
    
    int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);//获取栈大小的信息;
    int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);//设置栈大小;
    
    int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
    int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
    


    相互排斥锁


            当多个控制线程共享同样的内存时,须要确保每一个线程看到一致的数据视图。

    当多个线程对可改动变量进行訪问时,就会出现变量的一致性问题,这时就会涉及到线程同步的问题。

            相互排斥锁也称为相互排斥量。能够通过使用 pthread 的相互排斥接口保护数据,确保同一时间仅仅有一个线程訪问数据。相互排斥量本质上就是一把锁,在訪问共享资源前对相互排斥量进行加锁。在訪问完毕后释放相互排斥量上的锁。

    对相互排斥量进行加锁以后。不论什么其它试图再次对相互排斥量加锁的线程将会被堵塞直到当前线程释放该相互排斥锁。

    假设释放相互排斥锁时有多个线程堵塞,全部在该相互排斥锁上的堵塞线程都会变成执行状态,第一个变为执行状态的线程能够对相互排斥量进行加锁,其它线程将会看到相互排斥锁依旧被锁住,仅仅能回去等待它又一次变为可用。在这样的方式下,每次仅仅有一个线程能够向前执行。

            相互排斥变量使用 pthread_mutex_t 数据类型来表示,在使用相互排斥量曾经。必须先对它进行初始化。能够把它设置为常量 PTHREAD_MUTEX_INITIALIZER (仅仅对静态分配的相互排斥量),也能够通过调用 pthread_mutex_init 函数进行初始化。假设动态地分配相互排斥量,那么在释放内存前须要调用 pthread_mutex_destroy。


    /* 相互排斥量 */
    /*
     * 函数功能:初始化相互排斥变量;
     * 返回值:若成功则返回0。否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    
    /* 初始化相互排斥锁 */
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    /* 销毁相互排斥锁,释放系统资源 */
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    /*
     * 说明:
     * attr表示相互排斥锁的属性。若attr为NULL,表示初始化相互排斥量为默认属性。
     */
    
    /*
     * 函数功能:对相互排斥量进行加、解锁;
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    int pthread_mutex_lock(pthread_mutex_t *mutex);//对相互排斥量进行加锁。线程被堵塞。
    int pthread_mutex_trylock(pthread_mutex_t *mutex);//对相互排斥变量加锁,但线程不堵塞。
    int pthread_mutex_unlock(pthread_mutex_t *mutex);//对相互排斥量进行解锁;
    /* 说明:
     * 调用pthread_mutex_lock对相互排斥变量进行加锁,若相互排斥变量已经上锁,则调用线程会被堵塞直到相互排斥量解锁。
     * 调用pthread_mutex_unlock对相互排斥量进行解锁。
     * 调用pthread_mutex_trylock对相互排斥量进行加锁,不会出现堵塞。否则加锁失败,返回EBUSY。
     */
    


    相互排斥锁属性

            相互排斥锁属性能够用 pthread_mutexattr_t 数据结构来进行操作。属性的初始化操作例如以下:


    /* 相互排斥量属性 */
    /*
     * 函数功能:初始化相互排斥量属性。
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_mutexattr_init(pthread_mutexattr_t *attr);
    int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
    /*
     * 说明:
     * pthread_mutexattr_init函数用默认的相互排斥量属性初始化pthread_mutexattr_t结构。
     * 两个属性是进程共享属性和类型属性;
     */
    
    /*
     * 函数功能:获取或改动进程共享属性;
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);//获取相互排斥量的进程共享属性
    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);//设置相互排斥量的进程共享属性
    /*
     * 说明:
     * 进程共享相互排斥量属性设置为PTHREAD_PROCESS_PRIVATE时,同意pthread线程库提供更加有效的相互排斥量实现。这在多线程应用程序中是默认的;
     * 在多个进程共享多个相互排斥量的情况下。pthread线程库能够限制开销较大的相互排斥量实现;
     *
     * 若设置为PTHREAD_PROCESS_PRIVATE,则表示相互排斥锁仅仅能被和锁初始化线程隶属于同一进程的线程共享;
     */
    
    /*
     * 函数功能:获取或改动类型属性;
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);//获取相互排斥量的类型属性
    int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);//改动相互排斥量的类型属性
    /* 说明:
     * type取值例如以下:
     * 1、PTHREAD_MUTEX_NORMAL          普通锁(默认)
     * 2、PTHREAD_MUTEX_ERRORCHECK      检错锁
     * 3、PTHREAD_MUTEX_RECUSIVE        嵌套锁
     * 4、PTHREAD_MUTEX_DEFAULT         默认锁
     */
    


    相互排斥锁类型:
    1. 普通锁:当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列。并在该锁解锁后按优先级获取该它。

      这样的锁easy引发死锁,即当同一个线程对一个已经加锁的普通锁再次加锁时,就会引发死锁。

    2. 检错锁:一个线程若对一个已经加锁的检错锁再次加锁时,则加锁操作返回 EDEADLK,对一个已被其它线程加锁的检错锁解锁,或对一个已经解锁的检错锁再次解锁。则解锁操作返回 EPERM。

    3. 嵌套锁:这样的锁同意一个线程在释放锁之前多次对它加锁而不发生死锁。可是其它线程若要获得该锁。则当前锁的拥有者必须运行对应次数的解锁操作。对一个已经被其它线程加锁的嵌套锁。或对一个已经解锁的嵌套锁再次解锁。则解锁操作返回 EPERM。
    4. 默认锁:一个线程若对一个已经加锁的默认锁再次加锁,或对一个已经被其它线程加锁的默认锁解锁,或对一个已经解锁的默认锁再次解锁,将导致不可预期的后果。这样的锁的实现可能会被映射为上面三种锁之中的一个。

    条件变量


            相互排斥锁是用于同步线程对共享数据的訪问。条件变量则是用于在线程之间同步共享数据的值。

    相互排斥锁提供相互排斥訪问机制,条件变量提供信号机制,当某个共享数据达到某个值时。唤醒等待这个共享数据的线程。条件变量能够将一个或多个线程进入堵塞状态。直到收到另外一个线程的通知,或者超时,或者发生了虚假唤醒,才干退出堵塞状态。

            条件变量与相互排斥量一起使用,同意线程以无竞争的方式等待特定的条件发生,条件本身是由相互排斥量保护。

    线程在改变条件状态前必须先锁住相互排斥量,条件变量同意线程等待特定条件发生。条件变量通过同意线程堵塞和等待还有一个线程发送信号的方法弥补了相互排斥锁的不足。它常和相互排斥锁一起使用。

    使用时,条件变量被用来堵塞一个线程。当条件不满足时。线程往往解开对应的相互排斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知对应的条件变量唤醒一个或多个正被此条件变量堵塞的线程。这些线程将又一次锁定相互排斥锁并又一次測试条件是否满足。一般说来,条件变量被用来进行线程间的同步。条件变量类型为 pthread_cond_t。使用前必须进行初始化。能够有两种初始化方式:把常量 PTHREAD_COND_INITIALIZER 赋给静态分配的条件变量。对于动态分配的条件变量,能够使用 pthread_cond_init 进行初始化。

    操作函数例如以下:


    /* 条件变量 */
    /*
     * 函数功能:初始化条件变量;
     * 返回值:若成功则返回0。否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
    int pthread_cond_destroy(pthread_cond_t *cond);
    /* 说明:
     * cond參数指向要操作的目标条件变量,attr參数指定条件变量的属性;
     */
    
    /*
     * 函数功能:等待条件变量变为真;
     * 返回值:若成功则返回0,否则返回错误编码。
     * 函数原型:
     */
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,
                const struct timespec *timeout);
    /*
     * 说明:
     * 传递给pthread_cond_wait的相互排斥量对条件进行保护。调用者把锁住的相互排斥量传给函数;
     * 函数把调用线程放到等待条件的线程列表上,然后对相互排斥量解锁。这两操作是原子操作;
     * 这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样就不会错过条件变化;
     * pthread_cond_wait返回时,相互排斥量再次被锁住;
     */
    
    /*
     * 函数功能:唤醒等待条件的线程;
     * 返回值:若成功则返回0。否则返回错误编码;
     * 函数原型:
     */
    int pthread_cond_signal(pthread_cond_t *cond);//唤醒等待该条件的某个线程,详细唤醒哪个线程取决于线程的优先级和调度策略;
    int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒等待该条件的全部线程;
    


    条件变量属性

            条件变量也仅仅有进程共享属性。其操作例如以下:


    /* 条件变量属性 */
    /*
     * 函数功能:初始化条件变量属性;
     * 返回值:若成功则返回0,否则返回错误编码。
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_condattr_init(pthread_condattr_t *attr);
    int pthread_condattr_destroy(pthread_condattr_t *attr);
    /*
     * 函数功能:获取或改动进程共享属性。
     * 返回值:若成功则返回0。否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared);
    int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);


    线程与信号

            当线程被创建时,它会继承进程的信号掩码,这个掩码就会变成线程私有的。所以每一个线程能够独立设置信号掩码。进程中的全部线程都是共享该进程的信号。

    多个线程是共享进程的地址空间,每一个线程对信号的处理函数是同样的,即假设某个线程改动了与某个信号相关的处理函数后,所在进程中的全部线程都必须共享这个处理函数的改变。也就是说。当在一个线程设置了某个信号的信号处理函数后,它将会覆盖其它线程为同一个信号设置的信号处理函数。

            每一个信号仅仅会被传递给一个线程,即进程中的信号是传递到单个线程的,传递给哪个线程是不确定的。

    假设信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中去。

    可是alarm 定时器是全部线程共享的资源。所以在多个线程中同一时候使用alarm 还是会互相干扰。

            在进程中能够调用 sigprocmask 来阻止信号发送,但在多线程的进程中它的行为并未定义,它能够不做不论什么事情。在主线程中调用 pthread_sigmask 使得全部线程都堵塞某个信号,也能够在某个线程中调用它来设置自己的掩码。



    /* 线程与信号 */
    
    /*
     * 函数功能:设置线程的信号屏蔽字;
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    #include <signal.h>
    
    int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset);
    /*
     * 说明:
     * 该函数的功能基本上与前面介绍的在进程中设置信号屏蔽字的函数sigprocmask同样;
     */
    
    /*
     * 函数功能:等待一个或多个信号发生。
     * 返回值:若成功则返回0。否则返回错误编码;
     * 函数原型:
     */
    int sigwait(const sigset_t *set, int *signop);
    /*
     * 说明:
     * set參数指出线程等待的信号集。signop指向的整数将作为返回值,表明发送信号的数量。
     */
    
    /*
     * 函数功能:给线程发送信号。
     * 返回值:若成功则返回0,否则返回错误编码。
     * 函数原型:
     */
    int pthread_kill(pthread_t thread, int signo);
    /*
     * 说明:
     * signo能够是0来检查线程是否存在,若信号的默认处理动作是终止整个进程,那么把信号传递给某个线程仍然会杀死整个进程;
     */
    


            假设信号集中的某个信号在sigwait 调用的时候处于未决状态,那么sigwait 将马上无堵塞的返回。在返回之前。sigwait 将从进程中移除那些处于未决状态的信号。

    为了避免错误动作的发生。线程在调用sigwait 之前,必须堵塞那些它正在等待的信号。

    sigwait 函数会自己主动取消信号集的堵塞状态,直到新的信号被递送。在返回之前。sigwait 将恢复线程的信号屏蔽字

    线程与进程


           多线程的父进程调用 fork 函数创建子进程时,子进程继承了整个地址空间的副本。子进程里面仅仅有一个线程,它是父进程中调用 fork 函数的线程的副本。在子进程中的线程继承了在父进程中同样的状态,即有同样的相互排斥锁和条件变量。假设父进程中的线程占用锁。则子进程也同样占有这些锁。仅仅是子进程不包括占有锁的线程的副本,所以并不知道详细占有哪些锁而且须要释放哪些锁。

            假设子进程从 fork 返回之后没有马上调用 exec 函数。则须要调用 fork 处理程序清理锁状态。能够调用 pthread_atfork 函数实现清理锁状态:


    /* 线程和 fork */
    
    /*
     * 函数功能:清理锁状态;
     * 返回值:若成功则返回0,否则返回错误编码;
     * 函数原型:
     */
    #include <pthread.h>
    int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
    /*
     * 说明:
     * 该函数最多能够安装三个帮助清理锁的函数;
     * prepare fork处理程序由父进程在fork创建子进程前调用。这个fork处理程序的任务是获取父进程定义的全部锁。
     *
     * parent fork处理程序是在fork创建子进程以后,但在fork返回之前在父进程环境中调用的。这个fork处理程序的任务是对prepare fork处理程序
     * 获取的全部锁进行解锁。
     *
     * child fork处理程序在fork返回之前在子进程环境中调用。与parent fork处理程序一样,child fork处理程序必须释放prepare fork处理程序获得的全部锁;
     */
    


    參考资料:

    《Unix 网络编程》

  • 相关阅读:
    00027_方法的重载
    Creating a Physical Standby Database 11g
    APUE信号-程序汇总
    随手记Swift基础和Optional Type(问号?和感叹号!)
    双十二即将来袭!阿里内部高并发系统设计手册终开源,你那系统能抗住“秒杀”吗?
    ajax初见
    编程基本功:BUG测试步骤尽可能用文档简化,突出重点
    年轻就该多尝试,教你20小时Get一项新技能
    微信小程序-封装请求基准路径、接口API 和使用
    理解Python闭包,这应该是最好的例子
  • 原文地址:https://www.cnblogs.com/wzzkaifa/p/6761467.html
Copyright © 2011-2022 走看看