热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

memcached源码分析半同步半异步网络模型

转载请注明出处:半同步半异步:     memcached使用半同步半异步网络模型处理客户端的连接和通信。    半同步半异步模型的基础设施:主线程创建多个子线程(这


        转载请注明出处:http://blog.csdn.net/luotuo44/article/details/42705475


半同步/半异步:

        memcached使用半同步/半异步网络模型处理客户端的连接和通信。

        半同步/半异步模型的基础设施:主线程创建多个子线程(这些子线程也称为worker线程),每一个线程都维持自己的事件循环,即每个线程都有自己的epoll,并且都会调用epoll_wait函数进入事件监听状态。每一个worker线程(子线程)和主线程之间都用一条管道相互通信。每一个子线程都监听自己对应那条管道的读端。当主线程想和某一个worker线程进行通信,直接往对应的那条管道写入数据即可。

        半同步/半异步模型的工作流程:主线程负责监听进程对外的TCP监听端口。当客户端申请连接connect到进程的时候,主线程负责接收accept客户端的连接请求。然后主线程选择其中一个worker线程,把客户端fd通过对应的管道传给worker线程。worker线程得到客户端的fd后负责和这个客户端进行一切的通信。


        半同步/半异步模型的工作示意图如下图所示:

        技术分享


        memcached里面的半同步/半异步和上面所说的差不多,区别在于:1. memcached使用libevent作为进行事件监听;2.memcached往管道里面写的内容不是fd,而是一个简单的字符。每一个worker线程都维护一个CQ队列,主线程把fd和一些信息写入一个CQ_ITEM里面,然后主线程往worker线程的CQ队列里面push这个CQ_ITEM。接着主线程使用管道通知worker线程:“我已经发了一个新客户给你,你去处理吧”。

        memcached的半同步/半异步如下面这幅经典的图所示:

        技术分享



memcached的具体实现:

        上图看到每一个worker线程都有一个CQ队列,主线程accept到新客户端后,就把新客户端的信息封装成一个CQ_ITEM,然后push到选定线程的CQ队列中。


CQ队列:

        现在我们来看一下CQ队列长什么样的。

typedef struct conn_queue_item  CQ_ITEM;

struct conn_queue_item {
    int               sfd;
    enum conn_states  init_state;
    int               event_flags;
    int               read_buffer_size;
    enum network_transport     transport;
    CQ_ITEM          *next;
};

/* A connection queue. */
typedef struct conn_queue  CQ;
struct conn_queue {
    CQ_ITEM *head;//指向队列的第一个节点
    CQ_ITEM *tail;//指向队列的最后一个节点
    pthread_mutex_t lock; //一个队列就对应一个锁
};

        可以看到结构体conn_queue(即CQ队列结构体)有一个pthread_mutex_t类型变量lock,这说明主线程往某个worker线程的CQ队列里面push一个CQ_ITEM的时候必然要加锁的。下面是初始化CQ队列,以及push、pop一个CQ_ITEM的代码。

static void cq_init(CQ *cq) {
    pthread_mutex_init(&cq->lock, NULL);
    cq->head = NULL;
    cq->tail = NULL;
}

static CQ_ITEM *cq_pop(CQ *cq) {
    CQ_ITEM *item;

    pthread_mutex_lock(&cq->lock);
    item = cq->head;
    if (NULL != item) {
        cq->head = item->next;
        if (NULL == cq->head)
            cq->tail = NULL;
    }
    pthread_mutex_unlock(&cq->lock);

    return item;
}

/*
 * Adds an item to a connection queue.
 */
static void cq_push(CQ *cq, CQ_ITEM *item) {
    item->next = NULL;

    pthread_mutex_lock(&cq->lock);
    if (NULL == cq->tail)
        cq->head = item;
    else
        cq->tail->next = item;
    cq->tail = item;
    pthread_mutex_unlock(&cq->lock);
}
        注意,cq_pop函数不同于STL里面的pop。cq_pop函数会返回一个CQ_ITEM。


        由上面代码得到的CQ队列如下图所示:

        技术分享



为worker线程构建CQ队列:

        主线程又是怎么访问各个worker线程的CQ队列呢?在C语言里面的答案当然是使用全局变量啦。memcached专门定义了结构体,如下:

typedef struct {
    pthread_t thread_id; //线程id      
    struct event_base *base; //线程所使用的event_base 
    struct event notify_event;//用于监听管道读事件的event
    int notify_receive_fd; //管道的读端fd
    int notify_send_fd;   //管道的写端fd
    struct conn_queue *new_conn_queue; /* queue of new connections to handle */
	...
} LIBEVENT_THREAD;

        看到LIBEVENT_THREAD结构体的这些成员,完全可以顾名思义。memcached定义了LIBEVENT_THREAD类型的一个全局变量指针threads。当确定了memcached有多少个worker线程后,就会动态申请一个LIBEVENT_THREAD数组,并让threads指向其。于是每一个worker线程都对应有一个LIBEVENT_THREAD结构体。主线程通过全局变量threads就可以很方便地访问每一个worker线程的CQ队列和通信管道

        上面介绍了每一个线程都有一个LIBEVENT_THREAD结构体,现在来看一下具体的代码实现。注意代码里面监听管道可读的event的回调函数是thread_libevent_process,回调参数是线程自己的LIBEVENT_THREAD结构体指针。

static LIBEVENT_THREAD *threads;
//参数nthreads是worker线程的数量。main_base则是主线程的event_base
//主线程在main函数 调用本函数,创建nthreads个worker线程
void thread_init(int nthreads, struct event_base *main_base) {
    int         i;
	 
//申请一个CQ_ITEM时需要加锁,后面会介绍
    pthread_mutex_init(&cqi_freelist_lock, NULL);
    cqi_freelist = NULL;


	//申请具有nthreads个元素的LIBEVENT_THREAD数组
    threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));

    for (i = 0; i base = event_init();//新建一个event_base

    /* Listen for notifications from other threads */
	//监听管道的读端
    event_set(&me->notify_event, me->notify_receive_fd,//监听管道的读端
              EV_READ | EV_PERSIST, thread_libevent_process, me); //等同于event_new
    event_base_set(me->base, &me->notify_event);//将event_base和event相关联

    if (event_add(&me->notify_event, 0) == -1) {
        fprintf(stderr, "Can't monitor libevent notify pipe\n");
        exit(1);
    }

	//创建一个CQ队列
    me->new_conn_queue = malloc(sizeof(struct conn_queue));

    cq_init(me->new_conn_queue);

	...
}


static void create_worker(void *(*func)(void *), void *arg) {
    pthread_t       thread;
    pthread_attr_t  attr;
    int             ret;

    pthread_attr_init(&attr);

    if ((ret = pthread_create(&thread, &attr, func, arg)) != 0) {
        fprintf(stderr, "Can't create thread: %s\n",
                strerror(ret));
        exit(1);
    }
}



CQ_ITEM内存池:

        memcached在申请一个CQ_ITEM结构体时,并不是直接使用malloc申请的。因为这样做的话可能会导致大量的内存碎片(作为长期运行的服务器进程memcached需要考虑这个问题)。为此,memcached也为CQ_ITEM使用类似内存池的技术:预分配一块比较大的内存,将这块大内存切分成多个CQ_ITEM。下面是实现代码:

//本函数采用了一些优化手段.并非每调用一次本函数就申请一块内存。这会导致
 //内存碎片。这里采取的优化方法是,一次性分配64个CQ_ITEM大小的内存(即预分配).
 //下次调用本函数的时候,直接从之前分配64个中要一个即可。
 //由于是为了防止内存碎片,所以不是以链表的形式放置这64个CQ_ITEM。而是数组的形式。
 //于是,cqi_free函数就有点特别了。它并不会真正释放.而是像内存池那样归还
static CQ_ITEM *cqi_new(void) {
	//所有线程都会访问cqi_freelist的。所以需要加锁
    CQ_ITEM *item = NULL;
    pthread_mutex_lock(&cqi_freelist_lock);
    if (cqi_freelist) {
        item = cqi_freelist;
        cqi_freelist = item->next;
    }
    pthread_mutex_unlock(&cqi_freelist_lock);

    if (NULL == item) {//没有多余的CQ_ITEM了
        int i;

        item = malloc(sizeof(CQ_ITEM) * ITEMS_PER_ALLOC);//该宏等于64

        //item[0]直接返回为调用者,不用next指针连在一起。调用者负责将
        //item[0].next赋值为NULL
        for (i = 2; i next = cqi_freelist;
    cqi_freelist = item;  //头插法归还
    pthread_mutex_unlock(&cqi_freelist_lock);
}


主线程的工作:

        前面展示了在半同步/半异步中worker线程是怎么构建基础设施的。接下来看看主线程为了构建基础需要完成哪些工作。首先我们来看一下main函数,该main函数已经被我删除得很精简了。

int main (int argc, char **argv) {
	
	//检查libevent的版本是否足够新.1.3即可
    if (!sanitycheck()) {
        return EX_OSERR;
    }

	//对memcached的关键设置取默认值
    settings_init();

	...//解析memcached启动参数

	//main_base是一个struct event_base类型的全局变量
    main_base = event_init();//为主线程创建一个event_base

    conn_init();//先不管,后面会说到

	//创建settings.num_threads个worker线程,并且为每个worker线程创建一个CQ队列
	//并为这些worker申请各自的event_base,worker线程然后进入事件循环中	
    thread_init(settings.num_threads, main_base);

	//设置一个定时event(也叫超时event),定时(频率为一秒)更新current_time变量
	//这个超时event是add到全局变量main_base里面的,所以主线程负责更新current_time(这是一个很重要的全局变量)
    clock_handler(0, 0, 0);


    /* create the listening socket, bind it, and init */
    if (settings.socketpath == NULL) {
        FILE *portnumber_file = NULL;
		//创建监听客户端的socket
        if (settings.port && server_sockets(settings.port, tcp_transport,//tcp_transport是枚举类型
                                           portnumber_file)) {
            vperror("failed to listen on TCP port %d", settings.port);
            exit(EX_OSERR);
        }

 		... 
    }


   
    if (event_base_loop(main_base, 0) != 0) {//主线程进入事件循环
        retval = EXIT_FAILURE;
    }

    return retval;
}

        在main函数中,主线程创建了属于自己的event_base,存放在全局变量main_base中。在main函数的最后,主线程调用event_base_loop进入事件循环中。中间的server_sockets函数是创建一个监听客户端的socket,并将创建一个event监听该socket的可读事件。下面就看一下这个函数。为了简单起见下面的代码都忽略错误处理。

//port是默认的11211或者用户使用-p选项设置的端口号
//主线程在main函数会调用本函数
static int server_sockets(int port, enum network_transport transport,
                          FILE *portnumber_file) {

    //settings.inter里面可能有多个IP地址.如果有多个那么将用逗号分隔
    char *b;
    int ret = 0;
    //复制一个字符串,避免下面的strtok_r函数修改(污染)全局变量settings.inter
    char *list = strdup(settings.inter);

	//这个循环主要是处理多个IP的情况
    for (char *p = strtok_r(list, ";,", &b);
         p != NULL; //分割出一个个的ip,使用分号;作为分隔符
         p = strtok_r(NULL, ";,", &b)) {
        int the_port = port;
        char *s = strchr(p, ':');//启动的可能使用-l ip:port 参数形式
        //ip后面接着端口号,即指定ip的同时也指定了该ip的端口号
        //此时采用ip后面的端口号,而不是采用-p指定的端口号
        if (s != NULL) {
            *s = '\0';//截断后面的端口号,使得p指向的字符串只是一个ip
            ++s;
            if (!safe_strtol(s, &the_port)) {//非法端口号参数值
                return 1;
            }
        }
        if (strcmp(p, "*") == 0) {
            p = NULL;
        }
		//处理其中一个IP。有p指定ip(或者hostname)
        ret |= server_socket(p, the_port, transport, portnumber_file);
    }
    free(list);
    return ret;
}


static conn *listen_cOnn= NULL;//监听队列(可能要同时监听多个IP)


 //interface是一个ip、hostname或者NULL。这个ip字符串后面没有端口号。端口号由参数port指出
static int server_socket(const char *interface,
                         int port,
                         enum network_transport transport,
                         FILE *portnumber_file) {
    int sfd;
    struct linger ling = {0, 0};
    struct addrinfo *ai;
    struct addrinfo *next;
    struct addrinfo hints = { .ai_flags = AI_PASSIVE,
                              .ai_family = AF_UNSPEC };
    char port_buf[NI_MAXSERV];
    int success = 0;
    int flags =1;

    hints.ai_socktype = IS_UDP(transport) ? SOCK_DGRAM : SOCK_STREAM;


    snprintf(port_buf, sizeof(port_buf), "%d", port);
    getaddrinfo(interface, port_buf, &hints, &ai);

    //如果interface是一个hostname的话,那么可能就有多个ip
    for (next= ai; next; next= next->ai_next) {
        conn *listen_conn_add;

        //创建一个套接字,然后设置为非阻塞的
        sfd = new_socket(next);//调用socket函数
        bind(sfd, next->ai_addr, next->ai_addrlen);

        success++;
        listen(sfd, settings.backlog);


        if (!(listen_conn_add = conn_new(sfd, conn_listening,
                                         EV_READ | EV_PERSIST, 1,
                                         transport, main_base))) {
            fprintf(stderr, "failed to create listening connection\n");
            exit(EXIT_FAILURE);
        }

		//将要监听的多个conn放到一个监听队列里面
        listen_conn_add->next = listen_conn;
        listen_cOnn= listen_conn_add;

    }

    freeaddrinfo(ai);

    /* Return zero iff we detected no errors in starting up connections */
    return success == 0;
}


static int new_socket(struct addrinfo *ai) {
    int sfd;
    int flags;
    sfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    flags = fcntl(sfd, F_GETFL, 0);
    fcntl(sfd, F_SETFL, flags | O_NONBLOCK);

    return sfd;
}

        上面代码的流程还是蛮清晰的。就是根据用户的IP和端口号建立一个socket,bind、listen监听客户端的到来。因为主线程申请的socketfd已经设置为非阻塞的,所以listen函数会立刻返回。在main函数中,主线程最终将调用event_base_loop函数进入事件监听循环,处理客户端的连接请求。


连接管理者conn:

        现在我们来关注一下conn_new函数。因为在这里函数里面会创建一个用于监听socket fd的event,并调用event_add加入到主线程的event_base中。从conn_new的函数名来看,是new一个conn。确实如何。事实上memcached为每一个socket fd(也就是一个连接)都创建一个conn结构体,用于管理这个socket fd(连接)。因为一个连接会有很多数据和状态信息,所以需要一个结构体来负责管理。所以阅读conn_new函数之前,还需要先阅读一下conn_init函数,了解conn结构体的一些初试化。

        在《命令行参数详解》中有提到,可以在启动memcached的时候通过命令行参数-c num指定memcached允许的最大同时在线客户端数量。即使没有使用该参数,memcached也会采用默认值的,具体的默认值可以参数《关键配置的默认值》。也就是说在启动memcached之后就可以确定最多允许多少个客户端同时在线。有了这个数值就不用一有新连接就malloc一个conn结构体(这样会很容易造成内存碎片)。有了这个数值那么可以在一开始(conn_init函数),就申请动态申请一个数组。有新连接就从这个数组中分配一个元素即可。

conn **conns;
static void conn_init(void) {
    /* We're unlikely to see an FD much higher than maxconns. */
	//已经dup返回当前未使用的最小正整数,所以next_fd等于此刻已经消耗了的fd个数
    int next_fd = dup(1);//获取当前已经使用的fd的个数
    //预留一些文件描述符。也就是多申请一些conn结构体。以免有别的需要把文件描述符
    //给占了。导致socket fd的值大于这个数组长度
    int headroom = 10;//预留一些文件描述符  /* account for extra unexpected open FDs */
    struct rlimit rl;

	//settings.maxconns的默认值是1024.
    max_fds = settings.maxconns + headroom + next_fd;

    /* But if possible, get the actual highest FD we can possibly ever see. */
    if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
        max_fds = rl.rlim_max;
    } else {
        fprintf(stderr, "Failed to query maximum file descriptor; "
                        "falling back to maxconns\n");
    }

    close(next_fd);//next_fd只是用来计数的,并没有其他用途

	//注意,申请的conn结构体数量是比settings.maxconns这个客户端同时在线数
	//还要大的。因为memcached是直接用socket fd的值作为数组下标的。也正是
	//这个原因,前面需要使用headroom预留一些空间给突发情况
    if ((cOnns= calloc(max_fds, sizeof(conn *))) == NULL) {//注意是conn指针不是conn结构体
        fprintf(stderr, "Failed to allocate connection structures\n");
        /* This is unrecoverable so bail out early. */
        exit(1);
    }
}

        上面代码中,calloc申请的是conn*指针数组而不是conn结构体数组。主要是因为conn结构体是比较大的一个结构体(成员变量很多)。不一定会存在settings.maxconns个同时在线的客户端。所以可以等到需要conn结构体的时候再去动态申请。需要时去动态申请,这样会有内存碎片啊!非也!!因为可以循环使用的。如果没有这个conn*指针数组,那么当这个连接断开后就要free这个conn结构体所占的内存(不然就内存泄漏了)。有了这个数组那么就可以不free,由数组管理这个内存。下面的conn_new函数展示了这一点。

//为sfd分配一个conn结构体,并且为这个sfd建立一个event,然后让base监听这个event
conn *conn_new(const int sfd, enum conn_states init_state,//init_state值为conn_listening
                const int event_flags,
                const int read_buffer_size, enum network_transport transport,
                struct event_base *base) {
    conn *c;

    assert(sfd >= 0 && sfd sfd = sfd;
        conns[sfd] = c; //将这个结构体交由conns数组管理
    }

   	...//初始化另外一些成员变量
	c->state = init_state;//值为conn_listening

	//等同于event_assign,会自动关联current_base。event的回调函数是event_handler
    event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
    event_base_set(base, &c->event);
    c->ev_flags = event_flags;

    if (event_add(&c->event, 0) == -1) {
        perror("event_add");
        return NULL;
    }

    return c;
}


        综合上面的代码可以看到,主线程的基础实施也已经搭好了。注意,主线程对于socket fd 可读事件的回调函数是event_handler,回调参数是conn这个结构体指针。



牛刀小试:

        主线程和worker线程的基础设施都已经搭建好了,现在来尝试一下accept一个客户端。在跑一遍整个流程之前,先回忆一下回调函数。worker线程对于管道可读事件的回调函数是ethread_libevent_process函数。主线程对于socket fd可读事件的回调函数是event_handler函数。conn结构体成员state的值为conn_listening。现在走起!!直奔event_handler函数。

void event_handler(const int fd, const short which, void *arg) {
    conn *c;

    c = (conn *)arg;
    assert(c != NULL);

    c->which = which;
    if (fd != c->sfd) {
        conn_close(c);
        return;
    }

    drive_machine(c);
    return;
}

        太简单了吧,有没有搞错。event_handler函数确实简单,但其调用的drive_machine函数就确实很复杂。drive_machine函数内部是一个有限状态机。本文已经很长了,所以不会详解讲解有限状态机。下面只挑出处理新连接的那部分讲解。

static void drive_machine(conn *c) {
    bool stop = false;
    int sfd;
    socklen_t addrlen;
    struct sockaddr_storage addr;
    int res;
    const char *str;

    assert(c != NULL);

	//drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换
	//此时就需要一个循环,当进行状态转换时,也能处理
    while (!stop) {

        switch(c->state) {
        case conn_listening:
            addrlen = sizeof(addr);

            sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);

			...

			//选定一个worker线程,new一个CQ_ITEM,把这个CQ_ITEM仍给这个线程.
			dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
								 DATA_BUFFER_SIZE, tcp_transport);

            stop = true;
            break;

			...
        }
    }

    return;
}



static int last_thread = -1;

//参数 sfd, conn_new_cmd, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, tcp_transport
void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
                       int read_buffer_size, enum network_transport transport) {
    CQ_ITEM *item = cqi_new();//申请一个CQ_ITEM

    char buf[1];

    int tid = (last_thread + 1) % settings.num_threads;//轮询的方式选定一个worker线程

    LIBEVENT_THREAD *thread = threads + tid;

    last_thread = tid;

    item->sfd = sfd;
    item->init_state = init_state;//conn_new_cmd
    item->event_flags = event_flags;//EV_READ | EV_PERSIST
    item->read_buffer_size = read_buffer_size;//DATA_BUFFER_SIZE(2048)
    item->transport = transport;

    cq_push(thread->new_conn_queue, item);//把这个item放到选定的worker线程的CQ队列中

    buf[0] = 'c';
    if (write(thread->notify_send_fd, buf, 1) != 1) {//通知worker线程,有新客户端连接到来
        perror("Writing to thread notify pipe");
    }
}



        现在主线程已经通知了选定的worker线程。接下来就是worker线程怎么处理这个通知了。下面看一下worker线程的管道可读事件回调函数thread_libevent_process。

static void thread_libevent_process(int fd, short which, void *arg) {
    LIBEVENT_THREAD *me = arg;
    CQ_ITEM *item;
    char buf[1];

    read(fd, buf, 1);

    switch (buf[0]) {
    case 'c':
        //从CQ队列中读取一个item,因为是pop所以读取后,CQ队列会把这个item从队列中删除
        item = cq_pop(me->new_conn_queue);

        if (NULL != item) {
            //为sfd分配一个conn结构体,并且为这个sfd建立一个event,然后让base监听这个event
            //这个sfd的事件回调函数是event_handler
            conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                               item->read_buffer_size, item->transport, me->base);

            c->thread = me;

            cqi_free(item);
        }
        break;

    }
}

        正如前面所说的,memcached为每一个连接申请一个conn结构体进行维护。conn_new函数内部会为这个socket fd申请一个event并添加到该worker线程的event_base里面。当客户端发送命令时,worker线程就能监听到。这个conn_new函数前面已经说过了,这里也就不给出代码了。


        在以后,都是worker线程负责这里这个客户端的一切通信,也是worker线程负责完成客户端的命令,包括申请内存存储数据、查询数据、删掉数据。这些苦工都是worker线程完成的,而没有其它线程帮忙。不过大可放心,memcached对于这命令一般都能在常数时间时间复杂度内完成。所以,即使一个worker线程有多个客户端连接,也完全应付得过来。






memcached源码分析-----半同步半异步网络模型


推荐阅读
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 本文讲述了作者通过点火测试男友的性格和承受能力,以考验婚姻问题。作者故意不安慰男友并再次点火,观察他的反应。这个行为是善意的玩人,旨在了解男友的性格和避免婚姻问题。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
author-avatar
走走看看1971
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有