libuv笔记 (一)Threads

2023-05-16

Threads

线程在现代程序开发中会很常见,当然Libuv也不能缺席这一块,记得你在使用过程中要非常认真的处理 各种原始的同步问题。

线程会在内部使用,用来在执行系统调用时伪造异步的假象。libuv通过线程还可以使得程序异步地执行一个阻塞的任务。方法就是大量地生成新线程,然后收集线程执行返回的结果。

当下有两个占主导地位的线程库:windows下的线程实现和POSIX的pthread。libuv的线程API与pthread的API在使用方法和语义上很接近。

值得注意的是,libuv的线程模块是自成一体的。比如,其他的功能模块都需要依赖于event loop和回调的原则,但是线程并不是这样。它们是不受约束的,会在需要的时候阻塞,通过返回值产生信号错误,还有像接下来的这个例子所演示的这样,不需要在event loop中执行。

因为线程API在不同的系统平台上,句法和语义表现得都不太相似,在支持程度上也各不相同。考虑到libuv的跨平台特性,libuv支持的线程API个数很有限。

最后要强调一句:只有一个主线程,主线程上只有一个event loop。不会有其他与主线程交互的线程了。(除非使用uv_async_send)。

线程主要操作

下面这个例子不会很复杂,你可以使用uv_thread_create()开始一个线程,再使用uv_thread_join()等待其结束。

thread-create/main.c

int main() {
    int tracklen = 10;
    uv_thread_t hare_id;
    uv_thread_t tortoise_id;
    uv_thread_create(&hare_id, hare, &tracklen);
    uv_thread_create(&tortoise_id, tortoise, &tracklen);

    uv_thread_join(&hare_id);
    uv_thread_join(&tortoise_id);
    return 0;
}
TIP

在Unix上uv_thread_t只是pthread_t的别名, 但是这只是一个具体实现,不要过度地依赖它,认为这永远是成立的。

uv_thread_create的第二个参数指向了要执行的函数的地址。最后一个参数用来传递自定义的参数。最终,函数hare将在新的线程中执行,由操作系统调度。

thread-create/main.c

void hare(void *arg) {
    int tracklen = *((int *) arg);
    while (tracklen) {
        tracklen--;
        sleep(1);
        fprintf(stderr, "Hare ran another step\n");
    }
    fprintf(stderr, "Hare done running!\n");
}

uv_thread_join不像pthread_join那样,允许线线程通过第二个参数向父线程返回值。想要传递值,必须使用线程间通信Inter-thread communication。

线程同步

因为本教程重点不在线程,所以我只罗列了libuv API中一些神奇的地方。剩下的你可以自行阅读pthreads的手册。

1. Mutexes

libuv上的互斥量函数与pthread上存在一一映射。如果对pthread上的mutex不是很了解可以看这里。

相关函数

UV_EXTERN int uv_mutex_init(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_destroy(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_lock(uv_mutex_t* handle);
UV_EXTERN int uv_mutex_trylock(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_unlock(uv_mutex_t* handle);

uv_mutex_inituv_mutex_trylock在成功执行后,返回0,或者在错误时,返回错误码。

如果libuv在编译的时候开启了调试模式,uv_mutex_destroy(), uv_mutex_lock()uv_mutex_unlock()会在出错的地方调用abort()中断。类似的,uv_mutex_trylock()也同样会在错误发生时中断,而不是返回EAGAINEBUSY

递归地调用互斥量函数在某些系统平台上是支持的,但是你不能太过度依赖。因为例如在BSD上递归地调用互斥量函数会返回错误,比如你准备使用互斥量函数给一个已经上锁的临界区再次上锁的时候,就会出错。比如,像下面这个例子:

uv_mutex_lock(a_mutex);
uv_thread_create(thread_id, entry, (void *)a_mutex);
uv_mutex_lock(a_mutex);
// more things here

可以用来等待其他线程初始化一些变量然后释放a_mutex锁,但是第二次调用uv_mutex_lock(), 在调试模式下会导致程序崩溃,或者是返回错误。

注意

在linux中是支持递归上锁的,但是在libuv的API中并未实现。

2. Lock

读写锁是更细粒度的实现机制。两个读者线程可以同时从共享区中读取数据。当读者以读模式占有读写锁时,写者不能再占有它。当写者以写模式占有这个锁时,其他的写者或者读者都不能占有它。读写锁在数据库操作中非常常见,下面是一个玩具式的例子:

locks/main.c - simple rwlocks

#include <stdio.h>
#include <uv.h>

uv_barrier_t blocker;
uv_rwlock_t numlock;
int shared_num;

void reader(void *n)
{
    int num = *(int *)n;
    int i;
    for (i = 0; i < 20; i++) {
        uv_rwlock_rdlock(&numlock);
        printf("Reader %d: acquired lock\n", num);
        printf("Reader %d: shared num = %d\n", num, shared_num);
        uv_rwlock_rdunlock(&numlock);
        printf("Reader %d: released lock\n", num);
    }
    uv_barrier_wait(&blocker);
}

void writer(void *n)
{
    int num = *(int *)n;
    int i;
    for (i = 0; i < 20; i++) {
        uv_rwlock_wrlock(&numlock);
        printf("Writer %d: acquired lock\n", num);
        shared_num++;
        printf("Writer %d: incremented shared num = %d\n", num, shared_num);
        uv_rwlock_wrunlock(&numlock);
        printf("Writer %d: released lock\n", num);
    }
    uv_barrier_wait(&blocker);
}

int main()
{
    uv_barrier_init(&blocker, 4);

    shared_num = 0;
    uv_rwlock_init(&numlock);

    uv_thread_t threads[3];

    int thread_nums[] = {1, 2, 1};
    uv_thread_create(&threads[0], reader, &thread_nums[0]);
    uv_thread_create(&threads[1], reader, &thread_nums[1]);

    uv_thread_create(&threads[2], writer, &thread_nums[2]);

    uv_barrier_wait(&blocker);
    uv_barrier_destroy(&blocker);

    uv_rwlock_destroy(&numlock);
    return 0;
}

试着来执行一下上面的程序,看读者有多少次会同步执行。在有多个写者的时候,调度器会给予他们高优先级。因此,如果你加入两个读者,你会看到所有的读者趋向于在读者得到加锁机会前结束。

在上面的例子中,我们也使用了屏障。因此主线程来等待所有的线程都已经结束,最后再将屏障和锁一块回收。

3.其他

libuv同样支持信号量(uv_sem_xx),条件变量(uv_cond_xx)和屏障(uv_barrier_xx),而且API的使用方法和pthread中的用法很类似。(如果你对上面的三个名词还不是很熟,可以看这里,这里,这里)。

还有,libuv提供了一个简单易用的函数uv_once()。多个线程调用这个函数,参数可以使用一个uv_once_t和一个指向特定函数的指针,最终只有一个线程能够执行这个特定函数,并且这个特定函数只会被调用一次

/* Initialize guard */
static uv_once_t once_only = UV_ONCE_INIT;

int i = 0;

void increment() {
   i++;
}

void thread1() {
   /* ... work */
   uv_once(once_only, increment);
}

void thread2() {
   /* ... work */
   uv_once(once_only, increment);
}

int main() {
   /* ... spawn threads */
}

当所有的线程执行完毕时,i == 1

在libuv的v0.11.11版本里,推出了uv_key_t结构和操作线程局部存储TLS的API,使用方法同样和pthread类似。

libuv work queue

uv_queue_work()是一个便利的函数,它使得一个应用程序能够在不同的线程运行任务,当任务完成后,回调函数将会被触发。它看起来好像很简单,但是它真正吸引人的地方在于它能够使得任何第三方的库都能以event-loop的方式执行。当使用event-loop的时候,最重要的是不能让loop线程阻塞,或者是执行高cpu占用的程序,因为这样会使得loop慢下来,loop event的高效特性也不能得到很好地发挥。

然而,很多带有阻塞的特性的程序(比如最常见的I/O)使用开辟新线程来响应新请求(最经典的‘一个客户,一个线程‘模型)。使用event-loop可以提供另一种实现的方式。libuv提供了一个很好的抽象,使得你能够很好地使用它。

下面有一个很好的例子,灵感来自<<nodejs is cancer>>。我们将要执行fibonacci数列,并且睡眠一段时间,但是将阻塞和cpu占用时间长的任务分配到不同的线程,使得其不会阻塞event loop上的其他任务。

queue-work/main.c - lazy fibonacci

void fib(uv_work_t *req) {
    int n = *(int *) req->data;
    if (random() % 2)
        sleep(1);
    else
        sleep(3);
    long fib = fib_(n);
    fprintf(stderr, "%dth fibonacci is %lu\n", n, fib);
}

void after_fib(uv_work_t *req, int status) {
    fprintf(stderr, "Done calculating %dth fibonacci\n", *(int *) req->data);
}

任务函数很简单,也还没有运行在线程之上。uv_work_t是关键线索,你可以通过void *data传递任何数据,使用它来完成线程之间的沟通任务。但是你要确信,当你在多个线程都在运行的时候改变某个东西的时候,能够使用适当的锁。

触发器是uv_queue_work

queue-work/main.c

int main() {
    loop = uv_default_loop();

    int data[FIB_UNTIL];
    uv_work_t req[FIB_UNTIL];
    int i;
    for (i = 0; i < FIB_UNTIL; i++) {
        data[i] = i;
        req[i].data = (void *) &data[i];
        uv_queue_work(loop, &req[i], fib, after_fib);
    }

    return uv_run(loop, UV_RUN_DEFAULT);
}

线程函数fbi()将会在不同的线程中运行,传入uv_work_t结构体参数,一旦fib()函数返回,after_fib()会被event loop中的线程调用,然后被传入同样的结构体。

为了封装阻塞的库,常见的模式是用baton来交换数据。

从libuv 0.9.4版后,添加了函数uv_cancel()。它可以用来取消工作队列中的任务。只有还未开始的任务可以被取消,如果任务已经开始执行或者已经执行完毕,uv_cancel()调用会失败。

当用户想要终止程序的时候,uv_cancel()可以用来清理任务队列中的等待执行的任务。例如,一个音乐播放器可以以歌手的名字对歌曲进行排序,如果这个时候用户想要退出这个程序,uv_cancel()就可以做到快速退出,而不用等待执行完任务队列后,再退出。

让我们对上述程序做一些修改,用来演示uv_cancel()的用法。首先让我们注册一个处理中断的函数。

queue-cancel/main.c

int main() {
    loop = uv_default_loop();

    int data[FIB_UNTIL];
    int i;
    for (i = 0; i < FIB_UNTIL; i++) {
        data[i] = i;
        fib_reqs[i].data = (void *) &data[i];
        uv_queue_work(loop, &fib_reqs[i], fib, after_fib);
    }

    uv_signal_t sig;
    uv_signal_init(loop, &sig);
    uv_signal_start(&sig, signal_handler, SIGINT);

    return uv_run(loop, UV_RUN_DEFAULT);
}

当用户通过Ctrl+C触发信号时,uv_cancel()回收任务队列中所有的任务,如果任务已经开始执行或者执行完毕,uv_cancel()返回0。

queue-cancel/main.c

void signal_handler(uv_signal_t *req, int signum)
{
    printf("Signal received!\n");
    int i;
    for (i = 0; i < FIB_UNTIL; i++) {
        uv_cancel((uv_req_t*) &fib_reqs[i]);
    }
    uv_signal_stop(req);
}

对于已经成功取消的任务,他的回调函数的参数status会被设置为UV_ECANCELED

queue-cancel/main.c

void after_fib(uv_work_t *req, int status) {
    if (status == UV_ECANCELED)
        fprintf(stderr, "Calculation of %d cancelled.\n", *(int *) req->data);
}

uv_cancel()函数同样可以用在uv_fs_tuv_getaddrinfo_t请求上。对于一系列的文件系统操作函数来说,uv_fs_t.errorno会同样被设置为UV_ECANCELED

Tip

一个良好设计的程序,应该能够终止一个已经开始运行的长耗时任务。
Such a worker could periodically check for a variable that only the main process sets to signal termination.

##Inter-thread communication

很多时候,你希望正在运行的线程之间能够相互发送消息。例如你在运行一个持续时间长的任务(可能使用uv_queue_work),但是你需要在主线程中监视它的进度情况。下面有一个简单的例子,演示了一个下载管理程序向用户展示各个下载线程的进度。

progress/main.c

uv_loop_t *loop;
uv_async_t async;

int main() {
    loop = uv_default_loop();

    uv_work_t req;
    int size = 10240;
    req.data = (void*) &size;

    uv_async_init(loop, &async, print_progress);
    uv_queue_work(loop, &req, fake_download, after);

    return uv_run(loop, UV_RUN_DEFAULT);
}

因为异步的线程通信是基于event-loop的,所以尽管所有的线程都可以是发送方,但是只有在event-loop上的线程可以是接收方(或者说event-loop是接收方)。在上述的代码中,当异步监视者接收到信号的时候,libuv会激发回调函数(print_progress)。

WARNING

应该注意: 因为消息的发送是异步的,当uv_async_send在另外一个线程中被调用后,回调函数可能会立即被调用, 也可能在稍后的某个时刻被调用。libuv也有可能多次调用uv_async_send,但只调用了一次回调函数。唯一可以保证的是: 线程在调用uv_async_send之后回调函数可至少被调用一次。 如果你没有未调用的uv_async_send, 那么回调函数也不会被调用。 如果你调用了两次(以上)的uv_async_send, 而 libuv 暂时还没有机会运行回调函数, 则libuv可能会在多次调用uv_async_send后只调用一次回调函数,你的回调函数绝对不会在一次事件中被调用两次(或多次)。

progress/main.c

void fake_download(uv_work_t *req) {
    int size = *((int*) req->data);
    int downloaded = 0;
    double percentage;
    while (downloaded < size) {
        percentage = downloaded*100.0/size;
        async.data = (void*) &percentage;
        uv_async_send(&async);

        sleep(1);
        downloaded += (200+random())%1000; // can only download max 1000bytes/sec,
                                           // but at least a 200;
    }
}

在上述的下载函数中,我们修改了进度显示器,使用uv_async_send发送进度信息。要记住:uv_async_send同样是非阻塞的,调用后会立即返回。

progress/main.c

void print_progress(uv_async_t *handle) {
    double percentage = *((double*) handle->data);
    fprintf(stderr, "Downloaded %.2f%%\n", percentage);
}

函数print_progress是标准的libuv模式,从监视器中抽取数据。最后最重要的是把监视器回收。

progress/main.c

void after(uv_work_t *req, int status) {
    fprintf(stderr, "Download complete\n");
    uv_close((uv_handle_t*) &async, NULL);
}

在例子的最后,我们要说下data域的滥用,bnoordhuis指出使用data域可能会存在线程安全问题,uv_async_send()事实上只是唤醒了event-loop。可以使用互斥量或者读写锁来保证执行顺序的正确性。

Note

互斥量和读写锁不能在信号处理函数中正确工作,但是uv_async_send可以。

一种需要使用uv_async_send的场景是,当调用需要线程交互的库时。例如,举一个在node.js中V8引擎的例子,上下文和对象都是与v8引擎的线程绑定的,从另一个线程中直接向v8请求数据会导致返回不确定的结果。但是,考虑到现在很多nodejs的模块都是和第三方库绑定的,可以像下面一样,解决这个问题:

1.在node中,第三方库会建立javascript的回调函数,以便回调函数被调用时,能够返回更多的信息。

var lib = require('lib');
lib.on_progress(function() {
    console.log("Progress");
});

lib.do();

// do other stuff

2.lib.do应该是非阻塞的,但是第三方库却是阻塞的,所以需要调用uv_queue_work函数。
3.在另外一个线程中完成任务想要调用progress的回调函数,但是不能直接与v8通信,所以需要uv_async_send函数。
4.在主线程(v8线程)中调用的异步回调函数,会在v8的配合下执行javscript的回调函数。(也就是说,主线程会调用回调函数,并且提供v8解析javascript的功能,以便其完成任务)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

libuv笔记 (一)Threads 的相关文章

  • 【libuv】入门:queue work 的跨线程异步通信

    通过阅读2012年的uv book 入门 有中文版 Handles and Requests libuv works by the user expressing interest in particular events This is
  • 【hive】基于Qt5和libuv udp 的lan chat

    作者已经不更新了 但是很棒 在线用户列表 聊天窗口 主程序 单独的网络线程 network thread data管理关联网络管理的 程序update升级更新 和消息收到 即可
  • 【libuv】1.44 windows构建

    uv a 作为一个独立的工程构建的 c 工程里c符号找不到 链接不过 Build started span class token punctuation span span class token operator span class
  • libuv笔记 (一)Threads

    Threads 线程在现代程序开发中会很常见 xff0c 当然Libuv也不能缺席这一块 xff0c 记得你在使用过程中要非常认真的处理 各种原始的同步问题 线程会在内部使用 xff0c 用来在执行系统调用时伪造异步的假象 libuv通过线
  • libuv初学者学习笔记

    开始阅读前 我简单的理解觉得是 类似于leetcode的一种题 外部同时启动开始 但是内部严格的按照线程1 gt 线程2的顺序发生过程 libuv框架 从上往下看 从左往右分成网络IO与文件IO等操作 网络I O看 linux平台通过底层e
  • libuv之async使用

    libuv中async的使用比较难理解一些 我们来一起学习下 简介 vu async t是我们要用的handle 这个handle用来在线程间通信的 我们看一下官方的解释 uv async t is a subclass of uv han
  • nodejs libuv学习

    读了一下libuv源代码 简单记录一些见解 https github com libuv libuv libev就是一个基于epoll封装事件的函数库 自身不带有线程池等操作 而libuv则是在libev基础上 加上线程操作的功能 大体运作
  • libuv 多线程与队列

    libuv 多线程与队列 一 libuv编译环境 1 可查看另一篇 libuv 介绍与编译 http mp blog csdn net postedit 79193274 二 原理图 程序代码 main c include
  • Libuv源码分析 —— 6. 事件循环【uv_run】

    通过之前的学习 咱们已经明白了在事件循环中的三个核心内容 分别是 Libuv源码分析 定时器 Libuv源码分析 idle prepare check Libuv源码分析 poll io 现在让咱们从头捋一遍事件循环到底完成了什么功能呢 u
  • linux下nodejs依赖库libuv库,开发环境准备

    nodejs底层使用libuv库实现异步IO 如果对nodejs的回调函数习以为常 而不知libuv 那岂不是很遗憾 libuv在github上托管了自己的源码 但是我要学习的是希望适用于nodejs某一个版本的 这样的代码是可以经过简单处
  • libuv异步文件读写

    libuv 异步文件读写 一 libuv编译环境 1 可查看另一篇 libuv 介绍与编译 http mp blog csdn net postedit 79193274 程序代码 include
  • libuv 对 uv_loop_new 的未定义引用

    编译后 我尝试运行libuv示例程序 include
  • 在 Node js 8 中打印 libuv 线程池大小

    这个链接纯粹指定libuv提供一个线程池 可用于运行用户代码并在循环线程中获得通知 它的默认大小是 4 但是可以在启动时通过设置来更改它UV THREADPOOL SIZE环境变量为任意值 绝对最大值为 128 So in package
  • Node js:libuv 线程池如何工作?

    我正在学习 Node Js 我了解 Node js 的核心是基于事件循环的反应器模式 当任何事件发生时 它都会进入事件队列 然后在运行任务结束后被堆栈拾取 如果事件是非阻塞事件 则会发生这种情况 但如果它是阻塞请求 则事件循环将其传递给来自
  • JavaScript 内部原理:事件循环以什么间隔运行?

    这是一个关于 JavaScript 内部结构的问题 假设我有 10 个异步任务 全部需要 x 秒才能运行 在等待响应时 脚本处于空闲状态 JavaScript 引擎在后台询问 任务队列中是否有任何内容 据我了解 这是一个循环 因此 事件循环
  • 使用 libwebsockets 编译 libuv

    我正在尝试运行与 LWS 库一起安装的 libwebsockets test server 但它不会运行 因为 lwsts 31616 libuv support not generated in 我检查过 libuv 已安装 1 8 0
  • 正确关闭 libUV 句柄

    我正在尝试找出如何修复使用 Valgrind 运行此程序时遇到的这些内存泄漏 泄漏发生在两个分配中nShell client main 但我不是 确定如何正确释放它们 我尝试在 nShell Connect 处释放它们 但它导致 libUV
  • 两次调用某个回调函数会导致分段错误:Nan

    我正在使用 C 插件编写nbind GitHub 链接 https github com charto nbind对于大多数事情和Nan GitHub 链接 https github com nodejs nan 用于异步调用回调 当我只调
  • 如何在 Node.js 插件中泵送窗口消息?

    在 Windows Nodejs 插件中 我创建了一个窗口来接收消息 Handle
  • libuv 只是 POSIX 系统上 libev 的包装吗?

    我真的很困惑 libev 和 libuv libuv 只是 POSIX 系统上 libev 的包装吗 如果不是的话区别在哪里 不再 从 libuv v0 9 开始 这是libuv github问题 https github com joye

随机推荐

  • C++ Windows 窗体程序入门 - 1.你的第亿个窗体程序

    前言 43 学Windows窗体已经有一段时日了 xff0c 奈何没有什么浅显易懂 amp 便宜 xff01 xff01 的书籍 就想来 算是记笔记吧 顺便还能给你们总结一些经验 注 有许多内容源于我看过的一些视频 比如Chili和Cher
  • CSS替换元素和非替换元素

    根据是否可以通过修改某个属性值更改元素呈现的内容 xff0c 可以分为替换元素和非替换元素 替换元素 以下元素都是可替换元素 xff0c 以及在各种浏览器下的默认display值 xff08 图片来源 CSS世界 张鑫旭 xff09 针对
  • SD-WAN加速保障跨国公司数据传输质量

    很多企业开启国际化业务 xff0c 跨国文件传输越来越频繁 xff0c 而且随着业务的开展 xff0c 公司规模的扩张 xff0c 很多企业都在海外设置了分支机构 不得不说 xff0c 随着经济一体化的进程不断加快 xff0c 企业跨国经营
  • 零基础视觉SLAM(一)

    文章目录 SLAM简介什么是SLAM xff1f 传感器VSLAM架构视觉里程计后端优化 SLAM应用自学参考书预备知识 SLAM简介 什么是SLAM xff1f SLAM从本质上来说它要实现的就是通过传感器去实时地估计自身位置及经过的轨迹
  • 关于Proxmox 5.x的国内有效镜像源

    官网的 http download proxmox com 有多慢我就不提了 xff0c 否则大家也不会看到这篇小文 首先需要分清楚Proxmox VE的镜像构成 1 xff09 Debian自身 这个用国内哪个镜像都可以 xff0c al
  • 多线程是否真的有必要?

    一点疑问 相比大家在投简历 面试等等过程中 xff0c 或多或少会遇到这么一个问题 xff1a 熟悉掌握多线程开发 xff1b 谈谈你对多线程的认识 其实 xff0c 我有这么一个疑问 xff0c 那就是多线程真的有必要么 xff1f 根据
  • stm32无法烧录问题分析

    1 开始能烧录 xff0c 烧录程序后就不能烧录了 原因 xff1a 升级接口IO被代码修改应用 xff0c 导致无法烧录 xff0c 解决办法 xff1a 可以让MCU进入升级模式 xff08 拉高boot0 xff0c 然后复位MCU
  • 【Git】msysgit + TortoiseGit:在 windows 上安装配置版本控制工具 Git 图形化使用

    msysgit 43 TortoiseGit xff1a 在 windows 上安装配置版本控制工具 Git 图形化使用 一 安装说明 Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控
  • Slickedit使用记录

    Slickedit使用记录 一 快捷键二 问题和解决方法 一 快捷键 已经习惯了android studio 中的快捷键 xff0c 在slickedit上也做下修改Tools gt options gt Keyboard and mous
  • 查看.Net源代码vs版本号

    方法 xff1a 用记事本打开vs项目的 sln文件 第2行就是这个源代码包的开发软件vs版本号了 Microsoft Visual Studio Solution File Format Version 9 00 Visual Studi
  • Docker: GUI 应用,Ubuntu 上如何运行呢?

    操作系统 Ubuntu 18 04运行镜像 continuumio anaconda3 based on debian Step 1 安装 Docker span class token comment update the apt pac
  • 网络爬虫详细设计方案

    目录 网络爬虫设计方案 1 网络爬虫简介 2 Java爬虫的开发和使用流程 2 1 下载 2 2 分析 3 单点登陆与Jsoup解析 3 1 单点登陆简介 3 1 1 登陆 3 1 2 注销 3 2 Jsoup网页解析 4 网络爬虫详细设计
  • 安装 python-dev 的时候,缺少依赖关系

    sudo aptitude install python dev报错 xff1a 下列软件包有未满足的依赖关系 xff1a python dev 依赖 libpython dev 61 2 7 5 5ubuntu3 但是它将不会被安装 依赖
  • maven-replacer-plugin 静态资源打包方案js css

    解决问题 xff1a 防止浏览器缓存 xff0c 修改js css后无效 xff0c 需要强刷 两种解决方案 xff1a 1 不依赖插件 xff0c 纯代码实现 1 1 实现拦截处理器 xff1a ModelAndViewIntercept
  • 简单FTP构建及访问

    使用2台RHEL6虚拟机 xff0c 其中一台作为vsftpd服务器 xff08 192 168 4 5 xff09 另外一台作为测试用的Linux客户机 xff08 192 168 4 205 xff09 在RHEL6系统中 xff0c
  • freertos程序死机原因

    一 开机死机原因 1 一般是某任务栈溢出所致 栈溢出一般有两个原因 xff1a 1 此任务函数的代码量太大 或调用了某个比较大的函数 2 此任务的函数内有比较大的局部变量的数组 调试方法 xff1a 1 先关闭所有任务再逐个打开 xff0c
  • 安装在win10环境下的Jenkins添加本地虚拟机centos7作为从机遇到的问题,报错:SSH Connection failed with IOException: "Key exchange

    首先声明我的Jenkins版本是 xff1a 2 31版 因为不同版本页面有所不一样 安装在win10环境下的Jenkins添加本地虚拟机centos7作为从机遇到的问题 xff0c 报错情况如下 xff1a Searching for 1
  • Linux du命令和df命令区别

    1 xff0c 两者区别 du xff0c disk usage 是通过搜索文件来计算每个文件的大小然后累加 xff0c du能看到的文件只是一些当前存在的 xff0c 没有被删除的 他计算的大小就是当前他认为存在的所有文件大小的累加和 d
  • ML大杂烩:**常见机器学习算法公式梳理

    机器学习方法有一个进阶的过程 xff0c 不同的方法族 xff0c 都有其基础和逐渐进化的模型 每一个更新的模型一般是对上一个简单模型的改进 xff0c 比如SVM就直接改进了近邻方法 xff0c 降低了保留的实例个数 本文有大量修改 xf
  • libuv笔记 (一)Threads

    Threads 线程在现代程序开发中会很常见 xff0c 当然Libuv也不能缺席这一块 xff0c 记得你在使用过程中要非常认真的处理 各种原始的同步问题 线程会在内部使用 xff0c 用来在执行系统调用时伪造异步的假象 libuv通过线