UNIX网络编程卷一 学习笔记 第二十六章 线程

news2024/11/16 3:42:55

在传统UNIX模式中,当一个进程需要另一个实体完成某事时,它就fork一个子进程,并让子进程去执行处理,Unix上大多网络服务器程序就是这么写的。

这种范式多年来一直用得很好,但fork调用存在一些问题:
1.fork调用代价大。fork函数要把父进程的内存映像复制到子进程,并在子进程中复制所有描述符,如此等等。当今实现使用写时复制技术,用以避免在子进程在真正需要自己的副本前就把父进程的数据空间复制到子进程。但即使有这样的优化措施,fork调用仍是昂贵的。

2.fork函数返回后父子进程之间信息的传递需要进程间通信(IPC)机制。调用fork前父进程向尚未存在的子进程传递信息很容易,因为子进程将获得父进程数据空间和所有描述符的一个副本,但从子进程往父进程返回信息比较费力。

线程有助于解决这两个问题,线程有时称为轻权进程(lightweight process,轻量级进程),线程的创建可能比进程的创建快10~100倍。

同一进程内所有线程共享相同的全局内存,这使得线程之间易于共享信息,但伴随这种简易性而来的却是同步问题。

同一进程内所有线程除了全局变量外还共享:
1.进程指令。

2.大多数数据。

3.打开的文件(即描述符)。

4.信号处理函数和信号处置。

5.当前工作目录。

6.用户ID和组ID。

但每个线程有各自的:
1.线程ID。

2.寄存器集合,包括程序计数器和栈指针。

3.栈(用于存放局部变量和函数的返回地址)。

4.errno。

5.信号掩码。

6.优先级。

信号处理函数可被类比作某种线程,即在传统的UNIX模型中,我们有主执行流(也称主控制流,即一个线程)和某个信号处理函数(另一个线程)。如果主执行流正在更改某个链表时发生一个信号,而该信号的处理函数也试图更改该链表,则后果通常是灾难性的,主执行流和信号处理函数共享同样的全局变量,但它们有各自的栈。

我们本章讲解POSIX线程,也称为Pthread,POSIX线程作为POSIX.1c标准的一部分在1995年得到标准化,大多UNIX版本将来会支持这类线程。所有Pthread函数都以pthread_打头。

当一个程序由exec函数启动执行时,称为初始线程或主线程的单个线程就创建了,其余线程通过pthread_create函数创建:
在这里插入图片描述
一个进程内的每个线程都由一个线程ID标识,其数据类型为pthread_t(往往是unsigned int),如果新线程成功创建,其ID就通过tid参数指针返回。

每个线程都有许多属性:优先级、初始栈大小、是否应成为一个守护线程等。我们可以在创建线程时通过初始化一个取代默认设置的pthread_attr_t变量指定这些属性,如果attr参数为空指针,则采用默认设置。

创建一个线程时func参数是由该线程执行的函数,arg参数是该函数的参数,该线程通过调用这个函数开始执行,然后或者显式地终止(调用pthread_exit),或者隐式终止(让该函数返回)。func参数指定的函数只接受1个参数arg,如果我们需要给该函数传递多个参数,就要把这些参数打包成一个结构,然后把这个结构的地址作为单个参数传递给这个初始函数。

func参数指向的函数接受一个void *指针参数,同时还返回一个void *指针,这使得我们可以把一个指针(可指向任何我们期望的内容)传递给线程,又允许线程返回一个指针(同样指向任何我们期望的内容)。

通常Pthread函数的返回值为0表示成功,非0表示出错。与套接字函数及大多数系统调用出错时返回-1并设置errno为某个正值所不同的是,Pthread函数出错时返回正值错误指示,如pthread_create函数因线程数目超过某个系统限制而不能创建新线程时返回EAGAIN。Pthread函数不设置errno。Pthread函数成功时返回0,失败时返回非0不成问题,因为sys/errno.h头文件中所有Exxx值都是正值,0值从来不被赋予任何Exxx。

我们可通过调用pthread_join等待一个给定线程终止,对比线程与UNIX进程,pthread_create函数类似于fork函数,pthread_join函数类似于waitpid函数。
在这里插入图片描述
调用pthread_join时,我们必须指定要等待线程的tid。Pthread没有办法等待任一线程终止(类似指定进程ID为-1调用waitpid)。

如果status参数指针非空,则来自所等待线程的返回值将存入status参数指向的位置。

每个线程都由一个在所属进程内标识自身的ID,线程ID由pthread_create函数返回,每个线程也可调用pthread_self获取自身线程ID:
在这里插入图片描述
对比线程和UNIX进程,pthread_self函数类似于getpid函数。

一个线程要么是可汇合的(joinable,默认值),要么是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程就像守护进程(创建守护进程的进程一般创建完就退出了,因此一般守护进程(daemon)的父进程会是init进程(pid为1)),当它终止时,所有相关资源都被释放,我们不能等待它终止。如果线程A需要知道线程B什么时候终止,最好将线程B保持默认可汇合。

pthread_detach函数把指定线程转变为脱离状态:
在这里插入图片描述
pthread_detach函数通常由想让自己脱离的线程调用:

pthread_detach(pthread_self());

让一个线程终止的方法之一是调用pthread_exit:
在这里插入图片描述
如果调用pthread_exit的线程未曾脱离,它的线程ID和退出状态将一直留存到调用进程内的某个其他线程对它调用pthread_join。

参数status不能指向调用线程的本地对象,因为线程终止时这样的对象也会消失。

让一个线程终止的其他方法:
1.启动线程的函数(pthread_create函数的第三个参数)返回,该函数的返回值是void指针,即相应线程的终止状态。

2.如果进程的main函数返回,或任何线程调用了exit,整个进程就终止,包括其中的线程。

我们把TCP回射客户程序中的str_cli函数改写为使用线程的:
在这里插入图片描述
以下是TCP回射客户程序中的str_cli函数的使用线程的版本:

// unpthread.h头文件中包含unp.h头文件,以及POSIX的pthread.h头文件
// 然后定义了我们为pthread_XXX函数编写的包裹函数的函数原型,这些包裹函数都以Pthread_打头
#include "unpthread.h"

void *copyto(void *);

static int sockfd;    /* global for both threads to access */
static FILE *fp;

void str_cli(FILE *fp_arg, int sockfd_arg) {
    char recvline[MAXLINE];
    pthread_t tid;

    // 我们即将创建的线程需要str_cli的两个参数,为简单起见,我们此处将其保存到外部变量中
    // 另外的方法是将这两个值放到一个结构中,然后把指向这个结构的指针作为参数传递给我们将要创建的线程
    sockfd = sockfd_arg;    /* copy arguments to externals */
    fp = fp_arg;

    // 创建线程,新线程id返回到tid变量中,新线程会执行copyto函数,没有参数传递给该线程
    Pthread_create(&tid, NULL, copyto, NULL);

    // 主线程调用readline和fputs,把从套接字读入的文本行复制到标准输出
    while (Readline(sockfd, recvline, MAXLINE) > 0) {
        Fputs(recvline, stdout);
    }
    // 当str_cli函数返回时,main函数会调用exit终止进程,进程内所有线程也随之被终止
    // 通常,copyto线程在从标准输入读到EOF时已经先于main函数的exit调用而终止
    // 但如果服务器过早终止,尚未读入EOF的copyto线程就会由main函数调用exit来终止
}

void *copyto(void *arg) {
    char sendline[MAXLINE];

    // 该线程只是把读自标准输入的文本行复制到套接字
    while (Fgets(sendline, MAXLINE, fp) != NULL) {
        Writen(sockfd, sendline, strlen(sendline));
    }

    // 当在标准输入上读到EOF时,它通过调用shutdown从套接字送出FIN
    Shutdown(sockfd, SHUT_WR);    /* EOF on stdin, send FIN */

    // 从启动该线程的函数return来终止该线程
    return NULL;    /* return (i.e., thread terminates) when EOF on stdin,i.e.的意思是即,也就是说 */
}

之前的回射客户程序中,当期待服务器回射文本行却收到EOF时,客户会显示server terminated prematurely,我们可以把以上线程版本也改为输出此消息,这个消息应该在主线程收到EOF,但另一个线程却还在运行时显示,一个简单的方法是声明名为done且初始化为0的外部变量,线程copyto在返回前把该变量设为1,主线程收到EOF后检查该变量,如果其值为0就显示此出错消息,由于设置该变量的线程只有1个,就没有同步的必要。

在第十六章中,我们对str_cli函数的各种版本进行了性能测量:
在这里插入图片描述
我们看到,线程版本略快于fork版本,但仍慢于非阻塞式IO版本,但对比非阻塞式IO版本的复杂性和线程版本的简单性,我们仍推荐使用线程版本,而不是非阻塞式IO版本。

重新编写TCP回射服务器程序,改为每个客户使用一个线程,我们同样适用自己的tcp_listen函数使该程序与协议无关:

#include "unpthread.h"

static void *doit(void *);    /* each thread executes this function */

int main(int argc, char **argv) {
    int listenfd, connfd;
    pthread_t tid;
    socklen_t addrlen, len;
    struct sockaddr *cliaddr;

    if (argc == 2) {
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    } else if (argc == 3) {
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    } else {
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");
    }

    cliaddr = Malloc(addrlen);

    for (; ; ) {
        len = addrlen;
        connfd = Accept(listenfd, cliaddr, &len);
        // 我们传递给doit函数的唯一参数是已连接套接字描述符connfd
        // 此处我们把整数描述符的类型强制转换成void指针,ANSI C不保证这能成功
        // 只有在整数大小大小等于指针大小的系统上,这种类型强制转换才能成功,所幸大多UNIX实现都是这样
        Pthread_create(&tid, NULL, &doit, (void *)connfd);
        // 主线程不像fork版本那样关闭已连接套接字,因为同一进程内所有线程共享描述符
        // 如果主线程调用close,就会终止相应连接
        // 创建新线程不影响已打开文件描述符的引用计数,这一点不同于fork
    }
}

static void *doit(void *arg) {
    // 先让自身脱离,因为主线程没有理由等待它创建的每个线程
    Pthread_detach(pthread_self());
    str_echo((int)arg);    /* same function as before */
    // 处理完后,close已连接套接字,因为本线程和主线程共享所有描述符
    // 如果此处不调用close,则此时客户发送了FIN,且服务器对该FIN发了ACK,此时连接处于半关闭状态
    // 服务器在半关闭状态下还可以发送数据,但此处我们没有发送,客户会一直处于FIN_WAIT_2状态
    // 源自Berkeley的实现在客户端保持FIN_WAIT_2状态超过11分钟后会超时断连,服务器最终可能会耗尽描述符
    // 对于使用fork函数的情形,子进程就不必close已连接套接字,因为子进程终止时,所有打开描述符都被关闭
    Close((int)arg);    /* done with connected socket */
    return NULL;
}

以上程序把整数变量connfd类型强制转换为void指针,这一点不保证在所有系统上都能成功。注意此处我们不能把connfd的地址传给新线程,即以下代码是错误的:

int main(int argc, char **argv) {
    int listenfd, connfd;
    ...
    
    for (; ; ) {
        len = addrlen;
        connfd = Accept(listenfd, cliaddr, &len);
        
        Pthread_create(&tid, NULL, &doit, &connfd);
    }
}

static void *doit(void *arg) {
    int connfd;
    
    connfd = *((int *)arg);
    Pthread_detach(pthread_self());
    str_echo(connfd);    /* same function as before */
    Close(connfd);    /* done with connected socket */
    return NULL;
}

以上程序从ANSI C角度看是可接受的,ANSI C保证我们能把一个整数指针类型强制转换为void指针,然后把这个void指针类型强制转换回原来的整数指针,但问题出在这个整数指针指在什么上。

主线程中只有一个整数变量connfd,每次调用accept该变量都会被覆写以一个新的已连接套接字描述符,因此可能发生以下情况:
1.accept函数返回,主线程把返回值(如5)存入connfd后调用pthread_create,pthread_create函数的最后一个参数是指向connfd的指针而非connfd的内容。

2.Pthread函数库创建一个线程,并准备调度doit函数启动执行。

3.另一个连接就绪且主线程在新创建的线程开始运行前再次运行,accept函数返回,主线程把返回值(如6)存入connfd后调用pthread_create。

尽管主线程一共创建了2个线程,但它们操作的都是存放在connfd中的值(上例中为6)。问题出在多个线程不是同步地访问一个共享变量。在直接将connfd转换为void指针值传递给pthread_create函数的例子中,是不存在该问题的,按照C向被调用函数传递整数值的方式(即把该值的一个副本压入被调用函数的栈中),这个解决方法是可行的。

以下是解决以上传参问题更好的方法:

#include "unpthread.h"

static void *doit(void *);    /* each thread executes this function */

int main(int argc, char **argv) {
    int listenfd, *iptr;
    pthread_t tid;
    socklen_t addrlen, len;
    struct sockaddr *cliaddr;

    if (argc == 2) {
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    } else if (argc == 3) {
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    } else {
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");
    }

    cliaddr = Malloc(addrlen);

    for (; ; ) {
        len = addrlen;
        // 每当调用accept前,先调用malloc分配一个整数变量的内存空间
        // 其中存放待accept函数返回的已连接描述符,这使得每个线程都由各自的已连接描述符副本
        iptr = Malloc(sizeof(int));
        *iptr = Accept(listenfd, cliaddr, &len);
        Pthread_create(&tid, NULL, &doit, iptr);
    }
}

static void *doit(void *arg) {
    int connfd;

    // 获取已连接描述符的值,然后释放内存空间
    connfd = *((int *)arg);
    free(arg);

    Pthread_detach(pthread_self());
    str_echo(connfd);    /* same function as before */
    Close(connfd);    /* done with connected socket */
    return NULL;
}

以上函数中,malloc和free函数历史上是不可重入的,在处于这两个函数之一的内部处理期间,从某个信号处理函数中调用这两个函数之一有可能导致灾难性后果,因为这两个函数操纵相同的静态数据结构。但我们现在可以安全地调用它们,因为POSIX要求以上两函数是线程安全的,这通常是库函数内做了一些对我们透明的某种同步机制。

POSIX.1要求由POSIX.1和ANSI C标准定义的所有函数都是线程安全的,但除了以下函数:
在这里插入图片描述
但POSIX未对网络编程API函数的线程安全性作出规定,上表最后5行来源于Unix 98。在第十一章中讨论过gethostbyname和gethostbyaddr的不可重入性质,当时提到有些厂家定义了这两个函数以_r结尾的线程安全版本,但这些线程安全函数没有标准可循,应避免使用。

从上图可知,很多函数定义了一个名字以_r结尾的新函数作为其线程安全的版本。而ctermid和tmpnam函数的线程安全条件是调用者为返回结果预先分配空间,并把指向该空间的指针作为参数传递给函数。

把一个程序转换成使用线程的版本时,有时会碰到因有些函数使用静态变量而引起的编程错误,和许多线程相关的编程错误相同,这个错误也会引起非确定性的结果。在无需考虑重入的环境下编写使用静态变量的函数无可非议,但当同一进程内的不同线程(信号处理函数也视为线程,但信号处理函数运行时,主控制流是暂停的)几乎同时调用这样的函数时就可能发生问题,因为这些函数使用的静态变量没有为不同线程保存各自的值。第三章中的readline函数就是非线程安全的:

#include "unp.h"

static int read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];

// 每次最多读MAXLINE个字节,每次返回一个字节
static ssize_t my_read(int fd, char *ptr) {
    if (read_cnt <= 0) {
    again:
        if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
            if (errno == EINTR) {
                goto again;
            }
            return -1;
        } else if (read_cnt == 0) {
            return 0;
        }
        read_ptr = read_buf;
    }
    
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

// readline函数本身唯一的变化是用my_read函数取代read函数
ssize_t readline(int fd, void *vptr, size_t maxlen) {
    ssize_t n, rc;
    char c, *ptr;
    
    ptr = vptr;
    for (n = 1; n < maxlen; ++n) {
        if ((rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n') {
                break;    /* new line is stored, like fgets() */
            }
        } else if (rc == 0) {
            *ptr = 0;
            return n - 1;    /* EOF, n - 1 bytes were read */
        } else {
            return -1;
        }
    }
    
    *ptr = 0;    /* null terminate like fgets() */
    return n;
}

// 此函数可以展露内部缓冲区的状态,便于调用者查看在当前文本行后是否收到了新数据
ssize_t readlinebuf(void **vptrptr) {
    if (read_cnt) {
        *vptrptr = read_ptr;
    }
    return read_cnt;
}

以上程序中的my_read函数使用3个静态变量,这些静态变量是为了增加性能而增设的,以上编程错误是在将现有的函数转换成在线程环境运行时经常碰到的问题,有多个解决方法:
1.使用线程特定数据。此方法并不简单,且将原函数转换成了只能在支持线程的系统上工作的函数,但本方法的优点是调用顺序无需变动,所有变动都体现在库函数中而非调用这些函数的应用中。

2.改变调用顺序,由调用者把readline函数的所有参数封装在一个结构中,并在该结构中存放以上程序中的3个静态变量,下图给出了新的结构和新的函数原型:
在这里插入图片描述
上图中新函数在支持线程和不支持线程的系统上都可使用,但调用readline的所有应用都需要修改调用方式。

3.改变接口结构,避免使用静态变量,这样函数就变成了线程安全的。对于上例,我们使用my_read函数是为了获得性能提升,我们可使用较老版本的readline(把my_read函数替换回read函数,每次使用read函数读1字节),但老版本极为低效,这个方法可能行不通。

使用线程特定数据是使现有函数变为线程安全的一个常用技巧。在介绍操纵线程特定数据的Pthread函数前,先说明一下线程特定数据这个概念本身和一个可能的实现,因为看起来这些函数很复杂,但实际并非如此。

线程特定数据相关的Pthread函数看起来复杂,部分复杂性源于许多关于线程使用的教材都把对线程特定数据的讲解写得读起来像是在描述Pthread标准本身,把键值对和键作为不透明对象来讨论(即忽略内部实现,只讲表现形式)。我们以索引和指针来描述线程特性数据,因为一般会实现把一个小整数索引用作键,与索引关联的值只是一个指向由线程malloc的某个内存区的指针。

每个系统支持有限数量的线程特定数据元素,POSIX要求每个进程的这个限制不小于128,后面的例子就采用128这个限制。系统(可能是线程函数库)为每个进程维护一个我们称之为Key的结构组成的结构数组:
在这里插入图片描述
Key结构中的标志字段指示这个数组元素是否正在使用,所有的标志都会初始化为不在使用。当线程调用pthread_key_create创建一个新线程特定元素时,系统搜索Key结构数组找出第一个不在使用的元素,该元素的索引(0~127)我们称为键,返回给调用线程的正是这个索引。

除了Key结构数组外,系统还在进程内维护了每个线程的多条信息,这些特定于线程的信息我们称为Pthread结构,其部分内容是我们称之为pkey数组的一个128个指针元素的数组:
在这里插入图片描述
每个线程的pkey数组的所有元素都被初始化为空指针,这128个指针与进程内的128个可能的键逐一关联,即这128个指针是键对应的该线程中的值。

当我们调用pthread_key_create创建一个键时,系统告诉我们这个键(即索引),每个线程可以随后为该键存储一个值(指针),而这个指针通常又是每个线程通过调用malloc获得的。线程特定数据中易于混淆的地方之一是:该指针是键值对中的值,但真正的线程特定数据却是该指针指向的任何内容。

我们现在查看一个如何使用线程特定数据的例子,以上述readline函数为例:
1.一个进程被启动,多个线程被创建。

2.其中一个线程(线程0)是首个调用readline函数的线程,在readline函数中,会调用pthread_key_create,系统会在Key数组中找到第一个未用元素,并把它的索引(0~127)返回给调用者,我们假设找到的索引为1。

我们将使用pthread_once函数确保pthread_key_create函数只被第一个调用readline的线程所调用。

3.readline函数调用pthread_getspecific获取本线程的pkey[1]值,返回值是空指针,readline函数于是调用malloc分配内存,用于为本线程保存特定于线程的信息。readline函数初始化完该内存,并调用pthread_setspecific把对应所创建键的线程特定数据指针pkey[1]设置为指向刚分配的内存区:
在这里插入图片描述
上图我们指出,Pthread结构是系统(可能是线程函数库)维护的,而我们malloc的真正线程特性数据是由我们的函数维护的。pthread_setspecific函数所做的只是在Pthread结构中键对应的指针指向readline函数分配的内存区,类似地,pthread_getspecific函数所做的只是返回键对应的指针。

4.另一个线程(线程n)调用readline,此时也许线程0仍在执行readline函数。

readline调用pthread_once试图初始化它的线程特定数据所用的键,但初始化函数已被线程0调用过,因此就不再被调用。

5.线程n在readline函数中调用pthread_getspecific获取线程n的pkey[1]值,返回值是一个空指针,线程n于是像线程0那样先调用malloc,再调用pthread_setspecific,以初始化线程n的键1所对应的线程特定数据:
在这里插入图片描述
6.线程n继续在readline函数中执行,使用和修改它自己的线程特定数据。

当一个线程终止时,如上例中调用readline的线程终止时,readline函数已经分配了一个需要释放的内存区,这正是Key结构中的析构函数指针的用处,一个线程调用pthread_key_create创建某个线程特定数据时,所指定的函数之一就是指向某个析构函数的指针,当线程终止时,系统将扫描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。

处理线程特定数据时通常先调用pthread_once和pthread_key_create:
在这里插入图片描述
当使用线程特定数据的函数被调用时,pthread_once函数会被该函数调用,pthread_once函数通过onceptr参数指向的变量,确保init参数所指的函数在进程范围内只被调用一次。

在进程范围内对于一个给定键,pthread_key_create函数只被调用一次,所创建的键通过keyptr指针参数返回,如果destructor参数指针非空,它所指的函数将被使用过对应线程特定数据的线程在终止时调用。

以上两个函数的典型用法如下(未考虑出错返回):

pthread_key_t rl_key;
pthread_once_t rl_once = PTHREAD_ONCE_INIT;

void readline_destructor(void *ptr) {
    free(ptr);
}

void readline_once(void) {
    // 创建一个线程特定数据的键存放在rl_key中
    pthread_key_create(&rl_key, readline_destructor);
}

ssize_t readline( ... ) {
    ...
    // 每次调用readline时,都会调用pthread_once
    // pthread_once函数使用onceptr参数指向的值(变量rl_once)来确保readline_once函数只被调用一次
    pthread_once(&rl_once, readline_once);
    
    if ((ptr = pthread_getspecific(rl_key)) == NULL) {
        ptr = Malloc( .. );
        pthread_setspecific(rl_key, ptr);
        /* initialize memory pointed to by ptr */
    }
    ...
    /* use values pointed to by ptr */
}

函数pthread_getspecific和pthread_setspecific分别用于获取和存放某个键关联的值,该值就是Pthread结构中的指针,该指针具体指向取决于应用,通常它指向一个动态分配的缓冲区:
在这里插入图片描述
pthread_key_create函数的参数是一个指向某个键的指针(因为该函数会在其中存放由系统赋予该键的值),而以上两个get和set函数的参数是键本身。

以下是使用线程特定数据的readline函数:

#include "unpthread.h"

static pthread_key_t rl_key;
static pthread_once_t rl_once = PTHREAD_ONCE_INIT;

static void readline_destructor(void *ptr) {
    free(ptr);
}

static void readline_once(void) {
    Pthread_key_create(&rl_key, readline_destructor);
}

// Rline结构含有原readline函数中导致问题的3个static变量
typedef struct {
    int rl_cnt;    /* initialize to 0 */
    char *rl_bufptr;    /* initialize to rl_buf */
    char rl_buf[MAXLINE];
} Rline;

// 第一个参数是预先为本线程分配的Rline结构的指针
static ssize_t my_read(Rline *tsd, int fd, char *ptr) {
    if (tsd->rl_cnt <= 0) {
        again:
        if ((tsd->rl_cnt = read(fd, tsd->rl_buf, MAXLINE)) < 0) {
            if (errno == EINTR) {
                goto again;
            }
            return -1;
        } else if (tsd->rl_cnt == 0) {
            return 0;
        }
        tsd->rl_bufptr = tsd->rl_buf;
    }

    --tsd->rl_cnt;
    *ptr = *tsd->rl_bufptr++;
    return 1;
}

ssize_t readline(int fd, void *vptr, size_t maxlen) {
    size_t n, rc;
    char c, *ptr;
    Rline *tsd;

    // 本进程内第一个调用readline的线程通过调用pthread_once创建线程特定数据对应的键
    Pthread_once(&rl_once, readline_once);
    // 获取特定于本线程的Rline结构指针,如果这次是本线程首次调用readline,返回值将是空指针
    if ((tsd = pthread_getspecific(rl_key)) == NULL) {
        tsd = Calloc(1, sizeof(Rline));    /* init to 0 */
        // 为本线程存储这个指向本线程特定数据的指针
        // 下次本线程调用readline时,pthread_getspecific函数将返回刚存储的指针
        Pthread_setspecific(rl_key, tsd);
    }

    ptr = vptr;
    for (n = 1; n < maxlen; ++n) {
        if ((rc = my_read(tsd, fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n') {
                break;
            }
        } else if (rc == 0) {
            *ptr = 0;
            return n - 1;    /* EOF, n - 1 bytes read */
        } else {
            return -1;    /* error, errno set by read() */
        }
    }
    *ptr = 0;
    return n;
}

将第十六章中的web客户程序重新编写成使用线程的版本,而非原来的使用非阻塞connect函数的版本。改用线程后,我们可以让套接字停留在默认的阻塞模式,因为我们为每个连接都创建了一个线程,每个线程可以阻塞在它的connect调用中,内核(也可能是线程函数库)会在其阻塞时转而运行另外某个就绪的线程:

#include "unpthread.h"
// 除了通常的pthread.h头文件外,我们还包含thread.h头文件,因为我们除了使用Pthread线程外,还使用Solaris线程
#include <thread.h>    /* Solaris threads */

#define MAXFILES 20
#define SERV "80"    /* port number or service name */

// 我们在file结构中增加了一个f_tid成员来存放线程ID
// 我们在线程版本中不再使用select函数,因此不再需要描述符集和最大描述符值变量maxfd
struct file {
    char *f_name;    /* filename */
    char *f_host;    /* hostname or IP address */
    int f_fd;    /* descriptor */
    int f_flags;    /* F_xxx below */
    pthread_t f_tid;    /* thread ID */
} file[MAXFILES];
#define F_CONNECTING 1    /* connect() in progress */
#define F_READING 2    /* connect() complete; now reading */
#define F_DONE 4    /* all done */

#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"

int nconn, nfiles, nlefttoconn, nlefttoread;

void *do_get_read(void *);
// home_page函数没有改变
void home_page(const char *, const char *);
void write_get_cmd(struct file *);

int main(int argc, char **argv) {
    int i, n, maxnconn;
    pthread_t tid;
    struct file *fptr;

    if (argc < 5) {
        err_quit("usage: web <#conns> <IPaddr> <homepage> file1 ...");
    }
    maxnconn = atoi(argv[1]);

    nfiles = min(argc - 4, MAXFILES);
    for (i = 0; i < nfiles; ++i) {
        file[i].f_name = argv[i + 4];
        file[i].f_host = argv[2];
        file[i].f_flags = 0;
    }
    printf("nfiles = %d\n", nfiles);

    home_page(argv[2], argv[3]);

    nlefttoread = nlefttoconn = nfiles;
    nconn = 0;
    while (nlefttoread > 0) {
        // 如果还有文件没有创建线程开始读,且最大连接数量没有满,我们就创建一个线程去读该文件
        while (nconn < maxnconn && nlefttoconn > 0) {
            /* find a file to read */
            for (i = 0; i < nfiles; ++i) {
                if (file[i].f_flags == 0) {
                    break;
                }
            }
            if (i == nfiles) {
                err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
            }

            file[i].f_flags = F_CONNECTING;
            // 每个新线程执行的函数是do_get_read,传递给它的参数是file结构的指针
            Pthread_create(&tid, NULL, &do_get_read, &file[i]);
            file[i].f_tid = tid;
            ++nconn;
            --nlefttoconn;
        }

        // 通过指定第一个参数为0调用Solaris线程函数thr_join,等待任何一个线程终止
        // Pthread没有提供等待任一线程终止的手段,pthread_join函数要求我们显式指定我们要等待的线程ID
        // Pthread解决本问题的方法较复杂,需要使用条件变量供即将终止的线程通知主线程自身何时终止
        // 我们给出的Solaris线程函数thr_join难以移植到所有环境,但我们还是使用它
        // 因为我们不希望引入条件变量和互斥锁而搞复杂对它的讨论
        // 还好我们可以在Solaris环境下混合使用Pthread线程和Solaris线程
        if ((n = thr_join(0, &tid, (void **)&fptr)) != 0) {
            errno = n, err_sys("thr_join error");
        }

        --nconn;
        --nlefttoread;
        printf("thread id %d for %s done\n", tid, fptr->f_name);
    }

    exit(0);
}

void *do_get_read(void *vptr) {
    int fd, n;
    char line[MAXLINE];
    struct file *fptr;

    fptr = (struct file *)vptr;

    // 建立TCP连接,套接字fd默认是阻塞式套接字,因此线程将阻塞在connect调用中,直到连接建立
    fd = Tcp_connect(fptr->f_host, SERV);
    fptr->f_fd = fd;
    printf("do_get_read for %s, fd %d, thread %d\n", fptr->f_name, fd, fptr->f_tid);

    // 给服务器发送一个HTTP GET命令,其中会设置本文件的F_READING标志
    write_get_cmd(fptr);    /* write() the GET command */

    /* Read server's reply */
    for (; ; ) {
        if ((n = Read(fd, line, MAXLINE)) == 0) {
            break;    /* server closed connection */
        }

        printf("read %d bytes from %s\n", n, fptr->f_name);
    }
    printf("end-of-file on %s\n", fptr->f_name);
    Close(fd);
    fptr->f_flags = F_DONE;    /* clears F_READING */

    return fptr;    /* terminate thread */
}

作者Stevens曾在Usenet上抱怨pthread_join函数不能等待任一线程终止,一些参与过Pthread标准工作的人员为这个设计决策辩解说,在进程模型中存在父子关系,因此wait或waitpid函数具备等待任一子进程的能力是有意义的,而线程没有类似父子进程的层次关系,调用pthread_join等待终止的线程不一定是调用线程创建的,他们还补充说,如果有人需要等待任一线程,也可使用条件变量实现它(并不简单)。无论他们如何争辩,作者仍认为pthread_join函数的设计存在瑕疵。

对于以上程序,主循环在某个线程终止后,在主循环中递减nconn和nlefttoread,我们也能把这两个递减操作放在do_get_read函数中,让每个线程在即将终止前递减这两个计数器,但这么做是一个并发编程错误。

把计数器递减代码放在每个线程均执行的函数中的问题在于那两个变量是全局的,而不是特定于线程的。如果一个线程在递减某个变量的中途被挂起,而另一个线程执行并递减同一个变量,就可能导致错误。举例来说,假设C编译器将递减运算符转换成3条机器指令:从内存装载到寄存器、递减寄存器、从寄存器存储到内存,考虑如下可能的情形:
1.线程A运行,把nconn的值3装载到一个寄存器。

2.系统把运行线程从A切换到B,A的寄存器被保存,B的寄存器被恢复。

3.线程B执行--nconn相对应的3条指令,把新值2存储到nconn。

4.一段时间后,系统把运行线程从B切换回A,A的寄存器被恢复,A继续执行,把寄存器中的值从3减为2,再把2存储到nconn。

最终的结果本该为1,实际却为2,运行结果错误。

这种类型的并发编程错误很难被发现,原因如下:
1.这些编程错误很少导致错误的发生,但它们毕竟是错误,在持续运行中错误总会发生(墨菲定律)。

2.这些编程错误导致的运行差错难以再现(需要像以上描述的那样,两个线程同时修改同一共享数据,且修改数据到一半切换其他线程同时修改此数据)。

3.某些系统上递减运算符的硬件指令可能是原子的,即在这些系统中存在可递减内存中某个整数的单条硬件指令来替换我们上例中说的3指令序列,且这条指令的执行期间硬件不能被中断,但我们不能保证所有系统都是如此,因此可能会发生在一个系统上出问题,在另一个系统上却不出问题的现象。

我们称线程编程为并发编程或并行编程,因为多个线程可以并发地(或并行地)运行且访问相同变量。虽然我们上例讨论的是单CPU情形,但如果线程A和线程B同时运行在某多处理系统的不同CPU上,问题还是会存在。对于多进程编程,fork后父子进程除描述符外不共享任何东西,因此不会碰到这些并发编程问题,但在进程之间的共享内存区中,还是会碰到同类问题。

我们可以使用线程轻易复现以上问题,以下程序会创建两个线程,每个线程会递增同一个全局变量5000次:

#include "unpthread.h"

#define NLOOP 5000

int counter;    /* incremented by threads */

void *doit(void *);

int main(int argc, char **argv) {
    pthread_t tidA, tidB;

    Pthread_create(&tidA, NULL, &doit, NULL);
    Pthread_create(&tidB, NULL, &doit, NULL);

    /* wait for both threads to terminate */
    Pthread_join(tidA, NULL);
    Pthread_join(tidB, NULL);

    exit(0);
}

void *doit(void *vptr) {
    int i, val;

    /*
     * Each thread fetches, prints, and increments the counter NLOOP times.
     * The value of the counter should increse monotonically.
     */
    for (i = 0; i < NLOOP; ++i) {
        // 为强化出错的可能性,我们先将counter的当前值保存在val中,然后再显示它的新值,再存储这个新值
        val = counter;
        printf("%d: %d\n", pthread_self(), val + 1);
        counter = val + 1;
    }
    return NULL;
}

运行以上程序:
在这里插入图片描述
注意系统首次从线程4切换到线程5时发生的错误,此时两个线程中存储到counter中的值都是518,这种错误在10000行输出中出现了多次。

如果我们运行以上程序多次,每次运行的结果应该都不同于前一次运行,如果我们把程序的输出重定向到磁盘文件,有时候就不会发生运行差错,因为不打印时程序运行地更快,线程间切换的机会也更少。试验中运行差错出现得最多的情形是:交互地运行该程序,把程序的输出写到慢速终端上,同时使用Unix的script程序把整个交互过程的输出保存到一个文件中。

以上讨论的多个线程更改一个共享变量的问题是最简单的问题,我们可以用一个互斥锁保护这个共享变量,访问该变量的前提是持有该互斥锁,按照Pthread,互斥锁是类型为pthread_mutex_t的变量,我们使用以下函数为一个互斥锁上锁和解锁:
在这里插入图片描述
如果试图对已被另一线程锁住的互斥锁加锁,本线程将被阻塞,直到该互斥锁被解锁。

如果某个互斥锁变量是静态分配(内存分配发生在编译和链接时)的,我们必须把它初始化为常值PTHREAD_MUTEX_INITIALIZER。如果我们在共享内存区中分配一个互斥锁,那么必须通过调用pthread_mutex_init函数在运行时把它初始化。

有些系统(如Solaris)把PTHREAD_MUTEX_INITIALIZER定义为0,因而可以忽略初始化步骤,因为静态分配的变量会被自动初始化为0,但不是所有系统都可以忽略初始化步骤,因为其他系统(如Digital Unix)把初始化常值定义为非0。

以下程序使用单个互斥锁保护由两个线程共同访问的计数器:

#include "unpthread.h"

#define NLOOP 5000

int counter;    /* incremented by threads */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit(void *);

int main(int argc, char **argv) {
    pthread_t tidA, tidB;

    Pthread_create(&tidA, NULL, &doit, NULL);
    Pthread_create(&tidB, NULL, &doit, NULL);

    /* wait for both threads to terminate */
    Pthread_join(tidA, NULL);
    Pthread_join(tidB, NULL);

    exit(0);
}

void *doit(void *vptr) {
    int i, val;

    /* 
     * Each thread fetches, prints, and increments the counter NLOOP times.
     * The value of the counter should increase monotonically.
     */
    for (i = 0; i < NLOOP; ++i) {
        // 线程在操纵counter变量前必须锁住该互斥量
        Pthread_mutex_lock(&counter_mutex);

        val = counter;
        printf("%d: %d\n", pthread_self(), val + 1);
        counter = val + 1;

        Pthread_mutex_unlock(&counter_mutex);
    }

    return NULL;
}

使用互斥锁的开销有多大呢,我们把以上程序和不加互斥量版本的程序各运行50000次,并把输出定向到/dev/null,然后测量时间,结果使用互斥量的版本的CPU时间多10%,互斥锁上锁开销并不大。

互斥锁适用于防止同时访问某个共享变量,我们还需要另外某种在等待某个条件发生期间能让我们进入睡眠的东西,例如以上web客户程序中,我们想把Solaris的thr_join函数替换为pthread_join函数,但在知道某个线程已经终止前,我们不能调用这个Pthread函数,我们首先声明一个已终止线程的全局变量计数器,并使用一个互斥锁保护它:

int ndone;    /* number of terminated threads */
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;

我们接着要求每个线程在即将终止前使用互斥锁递增这个计数器:

void *do_get_read(void *vptr) {
    ...
    Pthread_mutex_lock(&ndone_mutex);
    ++ndone;
    Pthread_mutex_unlock(&ndone_mutex);

    return fptr;    /* terminate thread */
}

问题是怎样编写主循环,主循环需要一次又一次地锁住这个互斥锁,以便检查是否有线程终止了:

while (nlefttoread > 0) {
    while (nconn < maxnconn && nlefttoconn > 0) {
        /* find a file to read */
        ...
    }
    
    /* See if one of the threads is done */
    Pthread_mutex_lock(&ndone_mutex);
    if (ndone > 0) {
        for (i = 0; i < nfiles; ++i) {
            if (file[i].f_flags & F_DONE) {
                Pthread_join(file[i].f_tid, (void **)&fptr);
                /* update file[i] for terminated thread */
                ...
            }
        }
    }
    Pthread_mutex_unlock(&ndone_mutex)
}

尽管这样编写主循环是正确的,但这样主循环永远不进入睡眠,它不断循环,每次循环检查一下ndone,这种方法称为轮询,它很浪费CPU时间。

我们需要一个让主循环进入睡眠,直到某个线程通知它有事可做才醒来的方法,条件变量结合互斥锁能提供这个功能。互斥锁提供互斥机制,条件变量提供信号机制。

按照Pthread,条件变量是类型为pthread_cond_t的变量,以下两函数使用条件变量:
在这里插入图片描述
第二个函数名字中的signal一词并不指Unix的SIGxxx信号。

我们举例说明这些函数,回到我们的Web客户程序的例子,我们现在给计数器ndone同时关联一个条件变量和一个互斥锁:

int ndone;
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;

通过在持有该互斥量期间递增该计数器并发送信号到该条件变量,一个线程通知主循环自身即将终止:

Pthread_mutex_lock(&ndone_mutex);
++ndone;
Pthread_cond_signal(&ndone_cond);
Pthread_mutex_unlock(&ndone_mutex);

主循环阻塞在pthread_cond_wait调用中,等待某个即将终止的线程发送信号到与ndone关联的条件变量:

while (nlefttoread > 0) {
    while (nconn < maxnconn && nlefttoconn > 0) {
        /* find a file to read */
        ...
    }
    /* Wait for one of the threads to terminate */
    Pthread_mutex_lock(&ndone_mutex);
    while (ndone == 0) {
        Pthread_cond_wait(&ndone_cond, &ndone_mutex);
    }
    
    for (i = 0; i < nfiles; ++i) {
        if (file[i].f_flags & F_DONE) {
            Pthread_join(file[i].f_tid, (void **)&fptr);
            
            /* update file[i] for terminated thread */
            ...
        }
    }
    Pthread_mutex_unlock(&ndone_mutex);
}

主循环仍然只是在持有互斥锁期间检查ndone变量,然后,如果发现无事可做,就调用pthread_cond_wait,该函数把调用线程投入睡眠并释放调用线程持有的互斥锁,此外,当调用线程后来从pthread_cond_wait函数返回时(其他某线程发送信号到与ndone关联的条件变量后),该线程再次持有该互斥量。

为什么每个条件变量都要关联一个互斥锁呢,因为条件同时是线程之间共享的某个变量的值,允许不同线程设置和测试该变量要求有一个与该变量关联的互斥锁,例如,如果上例中没有使用互斥锁,则主循环将这样测试变量ndone:

/* Wait for one of the threads to terminate */
while (ndone == 0) {
    Pthread_cond_wait(&ndone_cond, &ndone_mutex);
}

这里存在这样的可能性:主线程外最后一个线程在主循环测试ndone==0之后,调用pthread_cond_wait前递增了ndone,这样信号就丢失了,造成主循环永远阻塞在pthread_cond_wait调用中,等待永远不再发生的某事再次出现。

因此要求pthread_cond_wait函数被调用时其所关联的互斥锁必须是上锁的,该函数作为单个原子操作解锁该互斥量并把调用线程投入睡眠也是一样的原因,如果该函数不解锁该互斥锁且不在返回时再给它上锁,调用线程就需要自己做这些操作,测试变量ndone的代码将变为:

/* Wait for one of the threads to terminate */
Pthread_mutex_lock(&ndone_mutex);
while (ndone == 0) {
    Pthread_mutex_unlock(&ndone_mutex);
    Pthread_cond_wait(&ndone_cond, &ndone_mutex);
    Pthread_mutex_lock(&ndone_mutex);
}

但这里再次存在相同的出错可能:主线程外最后一个线程在主线程调用pthread_mutex_unlock和pthread_cond_wait之间终止并递增ndone的值。

pthread_cond_signal函数通常唤醒在相应条件变量上的单个线程,有时一个线程直到自己应唤醒多个线程,此时它可以调用pthread_cond_broadcast唤醒等在相应条件变量上的所有线程:
在这里插入图片描述
pthread_cond_timedwait函数允许线程设置一个阻塞时间限制,abstime参数是一个timespec结构,指定该函数必须返回时刻的系统时间,即到这个时间如果相应条件变量尚未收到信号也会返回,如果发生这样的超时,会返回ETIME错误。

abstime参数是一个绝对时间,而非时间增量,即abstime参数是函数应该返回时刻的系统时间,即从1970年1月1日UTC时间以来的秒数和纳秒数。这一点不同于select和pselect函数,它们指定的是从调用时刻开始到函数应该返回时刻的秒数和微秒数(对于pselect函数为纳秒数)。我们通常这样获取abstime参数值:调用gettimeofday获取当前时间的timeval结构值,再把它复制到一个timespec结构中,再加上期望的时间限制,如:

struct timeval tv;
struct timespec ts;

if (gettimeofday(&tv, NULL) < 0) {
    err_sys("gettimeofday error");
}
ts.tv_sec = tv.tv_sec + 5;    /* 5 seconds in future */
ts.tv_nsec = tv.tv_usec * 1000;    /* microsec to nanosec */

pthread_cond_timedwait(..., &ts);

使用绝对时间取代增量时间的优点是,如果该函数过早返回(可能是捕获了某信号),那么不必改动timespec结构参数的内容就能再次调用该函数,缺点是首次调用该函数前需要调用gettimeofday。

POSIX规范定义了一个名为clock_gettime的函数,它把当前时间返回为一个timespec结构。

我们现在重新编写以上web客户程序,把其中的Solaris函数thr_join改换为pthread_join函数,这样我们需要明确指定等待哪一个线程。我们还需要使用条件变量。

全局变量的唯一变动是增加一个新标志和一个条件变量:

#define F_JOINED 8    /* main has pthread_join'ed */

int ndone;    /* number of terminated threads */
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;

do_get_read函数的唯一变动是在本线程终止前递增ndone并通知主循环:

    printf ("end-of-file on %s\n", fptr->f_name);
    Close(fd);
    
    Pthread_mutex_lock(&ndone_mutex);
    fptr->f_flags = F_DONE;    /* clears F_READING */
    ++ndone;
    Pthread_cond_signal(&ndone_cond);
    Pthread_mutex_unlock(&ndone_mutex);
    
    return fptr;    /* terminate thread */
}

大多数变动发生在主循环中,以下是新版本的主循环:

    while (nlefttoread > 0) {
        while (nconn < maxnconn && nlefttoconn > 0) {
            /* find a file to read */
            for (i = 0; i < nfiles; ++i) {
                if (file[i].f_flags == 0) {
                    break;
                }
            }
            if (i == nfiles) {
                err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
            }
            
            file[i].f_flags = F_CONNECTING;
            Pthread_create(&tid, NULL, &do_get_read, &file[i]);
            file[i].f_tid = tid;
            ++nconn;
            --nlefttoconn;
        }
        
        /* Wait for thread to terminate */
        Pthread_mutex_lock(&ndone_mutex);
        while (ndone == 0) {
            Pthread_cond_wait(&ndone_cond, &ndone_mutex);
        }
        
        for (i = 0; i < nfiles; ++i) {
            if (file[i].f_flags & F_DONE) {
                Pthread_join(file[i].f_tid, (void **)&fptr);
                
                if (&file[i] != fptr) {
                    err_quit("file[i] != fptr");
                }
                fptr->f_flags = F_JOINED;    /* clears F_DONE */
                --ndone;
                --nconn;
                --nlefttoread;
                printf("thread %d for %s done\n", fptr->f_tid, fptr->f_name);
            }
        }
        Pthread_mutex_unlock(&ndone_mutex);
    }
    
    exit(0);
}

假设一个服务器同时服务100个客户,如果使用fork函数,会使用101个描述符,其中1个是监听套接字描述符,其余100个是已连接套接字描述符,这101个进程中每个只打开着一个描述符。如果是使用线程,则是单个进程中有101个描述符,每个线程各处理其中一个。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/817448.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

揭秘电脑上的几大流氓软件,查看你的设备是否中招?

当我们使用电脑时&#xff0c;不可避免地会接触到各种软件。有些软件为我们提供了便利和安全保障&#xff0c;而另一些则隐藏着不良企图。这些被称为"流氓软件"的程序&#xff0c;可能给我们的电脑带来麻烦、干扰甚至威胁我们的数据安全。让我们一起盘点一下电脑上的…

基于Ko-time的Springboot单体化调用链追踪实践

目录 前言 一、关于Ko-Time 1、是什么&#xff1f; 2、ko-time更新时间线 二、Ko-time怎么用&#xff1f; 1、依赖引入 2、配置集成 3、权限放行 三、链路追踪 1、系统运行 2、链路追踪 3、长时间调用模拟 总结 前言 熟悉微服务的老司机一定了解&#xff0c;在微服务模…

Java - 多态的使用

多态 多态基本介绍 方法或对象具有多种形态。是面向对象的三大特征&#xff0c;多态是建立在封装和继承基础之上的。 多态的具体体现 1、方法的多态&#xff1a; 重写和重载就能体现多态。 2、对象的多态&#xff1a;【背下来&#xff0c;记住】 一个对象的编译类型和运行…

快应用编译前如何统一替换字符串

假设你有一个需求&#xff0c;要把代码里的ad-button替换为div&#xff0c;因为是mi看ad-button不爽。 这还不简单么&#xff0c;webpack有那么多成熟的plugins和loaders&#xff0c;本身我对webpack也只是略知一二&#xff0c;随便一搜网上的解决方案&#xff0c; string-re…

【Java基础教程】(五十)JDBC篇:JDBC概念及操作步骤、主要类与接口解析、批处理与事务处理~

Java基础教程之JDBC &#x1f539;本章学习目标1️⃣ JDBC概念2️⃣ 连接数据库3️⃣ Statement 接口3.1 数据更新操作3.2 数据查询 4️⃣ PreparedStatement 接口4.1 Statement 接口问题4.2 PreparedStatement操作 5️⃣ 批处理与事务处理&#x1f33e; 总结 &#x1f539;本…

C++学习day--16 野指针和空指针

1、什么是野指针&#xff1f; 野指针就是指向的位置是不可知的&#xff08;随机的、不正确的、没有明确限制的&#xff09; 造成野指针的原因&#xff1a; 1、指针未初始化 2、指针越界 3、指针指向的空间被释放 规避野指针的方法&#xff1a; 1. 指针初始化 2. 小心指针越界…

【数据分享】2000—2022年250米分辨率逐月归一化植被指数(NDVI)数据(免费获取/全国/分省/分市)

NDVI&#xff0c;全名为Normalized Difference Vegetation Index&#xff0c;中文名称为归一化植被指数。这个指数可以用来定性和定量评价植被覆盖及其生长活力&#xff0c;我们也可以简单地将它理解为体现植被密度和健康状况的一个指标。 之前我们给大家分享过来源于MOD13A3数…

嵌入式面试常见题目收藏(超总结)

​ 这篇文章来自很多博客主和其他网站的作者&#xff0c;如有侵权&#xff0c;联系必删 文章出处标注&#xff1a; https://blog.csdn.net/qq_44330858/article/details/128947083 ***如需PDF或者原稿可私信 *** ***如需PDF或者原稿可私信 *** ***如需PDF或者原稿可私信 *** 1.…

AC+FIT(瘦AP)配置浅谈

FIT ensp实验材料 &#xff1a;pc、路由器、三层交换机、二层交换机、ac、ap 保证连通性&#xff1a; 根据ac与ap设计好的ip配置&#xff0c;使之可以通讯 ac与ap可以实现跨网段管理 1、设置三层交换机的vlan 与vlanif信息 dhcp enable //开启dhcp ip pool forap //…

Spring整合Mybatis、Spring整合JUnit

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaweb 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 Spring整合 一、Spring整合Mybatis1.1 整合Mybatis&#x…

链表刷题常用技巧——快慢指针

强大&#xff0c;不动如山的强大&#xff0c;不会输给自己的真正的强大。 往期回顾&#xff1a; 数据结构——单链表 单链表力扣刷题 文章目录 经典例题&#xff1a;链表的中间结点 题目分析及双指针思路引入 双指针图解 leetcode 核心代码 判断环形链表——快慢指针…

查看本地mysql账号密码

使用Navicat工具打开本地mysql&#xff0c;新建查询输入下面查询语句 SELECT user, authentication_string FROM mysql.user WHERE userroot将authentication_string 中的加密密码复制出来打开链接&#xff1a; Magic Data 5输入加密的密码&#xff0c;和验证码&#xff0c;点…

【目标检测】基于yolov5的水下垃圾检测(附代码和数据集,7684张图片)

写在前面: 首先感谢兄弟们的订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 路虽远,行则将至;事虽难,做则必成。只要有愚公移山的志气、滴水穿石的毅力,脚踏实地,埋头苦干,积跬步以至千里,就…

HDFS Erasure coding-纠删码介绍和原理

HDFS Erasure coding-纠删码介绍和原理 三副本策略弊端Erasure Coding&#xff08;EC&#xff09;简介Reed- Solomon&#xff08;RS&#xff09;码 EC架构 三副本策略弊端 为了提供容错能力&#xff0c;hdfs回根据replication factor&#xff08;复制因子&#xff09;在不同的…

可视化——安装Manim软件——试错篇

Manim: 一个数学可视化的动画引擎 官网&#xff1a;https://3b1b.github.io/manim/index.html 名词解析 python3.7是python语言的解释器, 运行python程序的环境必备品. 这个没啥说的,大家都能懂. 虽然官方建议3.7,但是我用3.8发现也没问题.考虑未来的历史进程,大伙最好还是装…

使用web-view实现网页端和uni-app端是数据传输

要实现这个功能 第一步&#xff1a;要在vue的public文件夹下面引入 <script type"text/javascript" src"https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script> 第二步&#xff1a;建立一个新的空的uni-app项目…

EP4CE6E22C8N Error: Can‘t recognize silicon ID for device 1

经过各种排查&#xff0c;发现是AS配置不对&#xff0c;仅供参考 工程 参考某处的工程画板配置的FPGA板子&#xff0c;用于学习入门FPGA。 烧录sof文件是正常的&#xff0c;并能正常运行。 但是烧录jic是failed&#xff0c;查看报错为&#xff1a;Error: Can’t recognize si…

八大排序算法--冒泡排序(动图理解)

冒泡排序 算法思路 冒泡排序的原理是&#xff1a;从左到右&#xff0c;相邻元素进行比较。每次比较一轮&#xff0c;就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。 以从小到大排序为例&#xff0c;第一轮比较后&#xff0c;所有数中最大的那个数就会…

多重背包三队列优化的理解以及代码记忆:

首先是多重背包得一维朴素版本&#xff08;除了完全背包和多重背包得队列优化都是从大到小&#xff09;&#xff1a; 通过这个执行过程我们发现这个是分类更新得&#xff0c;可以按照余数是进行更新&#xff0c;而且物品数量最多有三件&#xff0c;物品数量决定了窗口的宽度所…

JAVA基础知识-进制的介绍与书写格式

1. 进制的介绍与书写格式 1.1 进制的介绍与书写格式 代码 : public class Demo1 {/*十进制&#xff1a;Java中&#xff0c;数值默认都是10进制&#xff0c;不需要加任何修饰。二进制&#xff1a;数值前面以0b开头&#xff0c;b大小写都可以。八进制&#xff1a;数值前面以0开…