并发编程整理完成

This commit is contained in:
Estom
2021-09-07 10:39:42 +08:00
parent 651a2fe58e
commit 3112b989bc
44 changed files with 4291 additions and 18 deletions

View File

@@ -0,0 +1,508 @@
本章介绍了 Boost C++ 库 Asio它是异步输入输出的核心。 名字本身就说明了一切Asio 意即异步输入/输出。 该库可以让 C++ 异步地处理数据,且平台独立。 异步数据处理就是指,任务触发后不需要等待它们完成。 相反Boost.Asio 会在任务完成时触发一个应用。 异步任务的主要优点在于,在等待任务完成时不需要阻塞应用程序,可以去执行其它任务。
异步任务的典型例子是网络应用。 如果数据被发送出去了,比如发送至 Internet通常需要知道数据是否发送成功。 如果没有一个象 Boost.Asio 这样的库,就必须对函数的返回值进行求值。 但是,这样就要求待至所有数据发送完毕,并得到一个确认或是错误代码。 而使用 Boost.Asio这个过程被分为两个单独的步骤第一步是作为一个异步任务开始数据传输。 一旦传输完成,不论成功或是错误,应用程序都会在第二步中得到关于相应的结果通知。 主要的区别在于,应用程序无需阻塞至传输完成,而可以在这段时间里执行其它操作。
7.2. I/O 服务与 I/O 对象
使用 Boost.Asio 进行异步数据处理的应用程序基于两个概念I/O 服务和 I/O 对象。 I/O 服务抽象了操作系统的接口,允许第一时间进行异步数据处理,而 I/O 对象则用于初始化特定的操作。 鉴于 Boost.Asio 只提供了一个名为 boost::asio::io_service 的类作为 I/O 服务,它针对所支持的每一个操作系统都分别实现了优化的类,另外库中还包含了针对不同 I/O 对象的几个类。 其中,类 boost::asio::ip::tcp::socket 用于通过网络发送和接收数据,而类 boost::asio::deadline_timer 则提供了一个计时器,用于测量某个固定时间点到来或是一段指定的时长过去了。 以下第一个例子中就使用了计时器,因为与 Asio 所提供的其它 I/O 对象相比较而言,它不需要任何有关于网络编程的知识。
#include <boost/asio.hpp>
#include <iostream>
void handler(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
int main()
{
boost::asio::io_service io_service;
boost::asio::deadline_timer timer(io_service, boost::posix_time::seconds(5));
timer.async_wait(handler);
io_service.run();
}
函数 main() 首先定义了一个 I/O 服务 io_service用于初始化 I/O 对象 timer。 就象 boost::asio::deadline_timer 那样,所有 I/O 对象通常都需要一个 I/O 服务作为它们的构造函数的第一个参数。 由于 timer 的作用类似于一个闹钟,所以 boost::asio::deadline_timer 的构造函数可以传入第二个参数,用于表示在某个时间点或是在某段时长之后闹钟停止。 以上例子指定了五秒的时长,该闹钟在 timer 被定义之后立即开始计时。
虽然我们可以调用一个在五秒后返回的函数,但是通过调用方法 async_wait() 并传入 handler() 函数的名字作为唯一参数,可以让 Asio 启动一个异步操作。 请留意,我们只是传入了 handler() 函数的名字,而该函数本身并没有被调用。
async_wait() 的好处是,该函数调用会立即返回,而不是等待五秒钟。 一旦闹钟时间到,作为参数所提供的函数就会被相应调用。 因此,应用程序可以在调用了 async_wait() 之后执行其它操作,而不是阻塞在这里。
象 async_wait() 这样的方法被称为是非阻塞式的。 I/O 对象通常还提供了阻塞式的方法,可以让执行流在特定操作完成之前保持阻塞。 例如,可以调用阻塞式的 wait() 方法,取代 boost::asio::deadline_timer 的调用。 由于它会阻塞调用,所以它不需要传入一个函数名,而是在指定时间点或指定时长之后返回。
再看看上面的源代码,可以留意到在调用 async_wait() 之后,又在 I/O 服务之上调用了一个名为 run() 的方法。这是必须的,因为控制权必须被操作系统接管,才能在五秒之后调用 handler() 函数。
async_wait() 会启动一个异步操作并立即返回,而 run() 则是阻塞的。因此调用 run() 后程序执行会停止。 具有讽刺意味的是,许多操作系统只是通过阻塞函数来支持异步操作。 以下例子显示了为什么这个限制通常不会成为问题。
#include <boost/asio.hpp>
#include <iostream>
void handler1(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
void handler2(const boost::system::error_code &ec)
{
std::cout << "10 s." << std::endl;
}
int main()
{
boost::asio::io_service io_service;
boost::asio::deadline_timer timer1(io_service, boost::posix_time::seconds(5));
timer1.async_wait(handler1);
boost::asio::deadline_timer timer2(io_service, boost::posix_time::seconds(10));
timer2.async_wait(handler2);
io_service.run();
}
上面的程序用了两个 boost::asio::deadline_timer 类型的 I/O 对象。 第一个 I/O 对象表示一个五秒后触发的闹钟,而第二个则表示一个十秒后触发的闹钟。 每一段指定时长过去后,都会相应地调用函数 handler1() 和 handler2()。
在 main() 的最后,再次在唯一的 I/O 服务之上调用了 run() 方法。 如前所述,这个函数将阻塞执行,把控制权交给操作系统以接管异步处理。 在操作系统的帮助下handler1() 函数会在五秒后被调用,而 handler2() 函数则在十秒后被调用。
乍一看,你可能会觉得有些奇怪,为什么异步处理还要调用阻塞式的 run() 方法。 然而,由于应用程序必须防止被中止执行,所以这样做实际上不会有任何问题。 如果 run() 不是阻塞的main() 就会结束从而中止该应用程序。 如果应用程序不应被阻塞,那么就应该在一个新的线程内部调用 run(),它自然就会仅仅阻塞那个线程。
一旦特定的 I/O 服务的所有异步操作都完成了,控制权就会返回给 run() 方法,然后它就会返回。 以上两个例子中,应用程序都会在闹钟到时间后马上结束。
7.3. 可扩展性与多线程
用 Boost.Asio 这样的库来开发应用程序,与一般的 C++ 风格不同。 那些可能需要较长时间才返回的函数不再是以顺序的方式来调用。 不再是调用阻塞式的函数Boost.Asio 是启动一个异步操作。 而那些需要在操作结束后调用的函数则实现为相应的句柄。 这种方法的缺点是,本来顺序执行的功能变得在物理上分割开来了,从而令相应的代码更难理解。
象 Boost.Asio 这样的库通常是为了令应用程序具有更高的效率。 应用程序不需要等待特定的函数执行完成,而可以在期间执行其它任务,如开始另一个需要较长时间的操作。
可扩展性是指,一个应用程序从新增资源有效地获得好处的能力。 如果那些执行时间较长的操作不应该阻塞其它操作的话,那么建议使用 Boost.Asio. 由于现今的PC机通常都具有多核处理器所以线程的应用可以进一步提高一个基于 Boost.Asio 的应用程序的可扩展性。
如果在某个 boost::asio::io_service 类型的对象之上调用 run() 方法,则相关联的句柄也会在同一个线程内被执行。 通过使用多线程,应用程序可以同时调用多个 run() 方法。 一旦某个异步操作结束,相应的 I/O 服务就将在这些线程中的某一个之中执行句柄。 如果第二个操作在第一个操作之后很快也结束了,则 I/O 服务可以在另一个线程中执行句柄,而无需等待第一个句柄终止。
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
void handler1(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
void handler2(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
boost::asio::io_service io_service;
void run()
{
io_service.run();
}
int main()
{
boost::asio::deadline_timer timer1(io_service, boost::posix_time::seconds(5));
timer1.async_wait(handler1);
boost::asio::deadline_timer timer2(io_service, boost::posix_time::seconds(5));
timer2.async_wait(handler2);
boost::thread thread1(run);
boost::thread thread2(run);
thread1.join();
thread2.join();
}
上一节中的例子现在变成了一个多线程的应用。 通过使用在 boost/thread.hpp 中定义的 boost::thread 类,它来自于 Boost C++ 库 Thread我们在 main() 中创建了两个线程。 这两个线程均针对同一个 I/O 服务调用了 run() 方法。 这样当异步操作完成时,这个 I/O 服务就可以使用两个线程去执行句柄函数。
这个例子中的两个计时数均被设为在五秒后触发。 由于有两个线程,所以 handler1() 和 handler2() 可以同时执行。 如果第二个计时器触发时第一个仍在执行,则第二个句柄就会在第二个线程中执行。 如果第一个计时器的句柄已经终止,则 I/O 服务可以自由选择任一线程。
线程可以提高应用程序的性能。 因为线程是在处理器内核上执行的,所以创建比内核数更多的线程是没有意义的。 这样可以确保每个线程在其自己的内核上执行,而没有同一内核上的其它线程与之竞争。
要注意,使用线程并不总是值得的。 以上例子的运行会导致不同信息在标准输出流上混合输出,因为这两个句柄可能会并行运行,访问同一个共享资源:标准输出流 std::cout。 这种访问必须被同步,以保证每一条信息在另一个线程可以向标准输出流写出另一条信息之前被完全写出。 在这种情形下使用线程并不能提供多少好处,如果各个独立句柄不能独立地并行运行。
多次调用同一个 I/O 服务的 run() 方法,是为基于 Boost.Asio 的应用程序增加可扩展性的推荐方法。 另外还有一个不同的方法:不要绑定多个线程到单个 I/O 服务,而是创建多个 I/O 服务。 然后每一个 I/O 服务使用一个线程。 如果 I/O 服务的数量与系统的处理器内核数量相匹配,则异步操作都可以在各自的内核上执行。
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
void handler1(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
void handler2(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
boost::asio::io_service io_service1;
boost::asio::io_service io_service2;
void run1()
{
io_service1.run();
}
void run2()
{
io_service2.run();
}
int main()
{
boost::asio::deadline_timer timer1(io_service1, boost::posix_time::seconds(5));
timer1.async_wait(handler1);
boost::asio::deadline_timer timer2(io_service2, boost::posix_time::seconds(5));
timer2.async_wait(handler2);
boost::thread thread1(run1);
boost::thread thread2(run2);
thread1.join();
thread2.join();
}
前面的那个使用两个计时器的例子被重写为使用两个 I/O 服务。 这个应用程序仍然基于两个线程;但是现在每个线程被绑定至不同的 I/O 服务。 此外,两个 I/O 对象 timer1 和 timer2 现在也被绑定至不同的 I/O 服务。
这个应用程序的功能与前一个相同。 在一定条件下使用多个 I/O 服务是有好处的,每个 I/O 服务有自己的线程,最好是运行在各自的处理器内核上,这样每一个异步操作连同它们的句柄就可以局部化执行。 如果没有远端的数据或函数需要访问,那么每一个 I/O 服务就象一个小的自主应用。 这里的局部和远端是指象高速缓存、内存页这样的资源。 由于在确定优化策略之前需要对底层硬件、操作系统、编译器以及潜在的瓶颈有专门的了解,所以应该仅在清楚这些好处的情况下使用多个 I/O 服务。
7.4. 网络编程
虽然 Boost.Asio 是一个可以异步处理任何种类数据的库,但是它主要被用于网络编程。 这是由于,事实上 Boost.Asio 在加入其它 I/O 对象之前很久就已经支持网络功能了。 网络功能是异步处理的一个很好的例子,因为通过网络进行数据传输可能会需要较长时间,从而不能直接获得确认或错误条件。
Boost.Asio 提供了多个 I/O 对象以开发网络应用。 以下例子使用了 boost::asio::ip::tcp::socket 类来建立与中另一台PC的连接并下载 'Highscore' 主页;就象一个浏览器在指向 www.highscore.de 时所要做的。
#include <boost/asio.hpp>
#include <boost/array.hpp>
#include <iostream>
#include <string>
boost::asio::io_service io_service;
boost::asio::ip::tcp::resolver resolver(io_service);
boost::asio::ip::tcp::socket sock(io_service);
boost::array<char, 4096> buffer;
void read_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
{
if (!ec)
{
std::cout << std::string(buffer.data(), bytes_transferred) << std::endl;
sock.async_read_some(boost::asio::buffer(buffer), read_handler);
}
}
void connect_handler(const boost::system::error_code &ec)
{
if (!ec)
{
boost::asio::write(sock, boost::asio::buffer("GET / HTTP 1.1\r\nHost: highscore.de\r\n\r\n"));
sock.async_read_some(boost::asio::buffer(buffer), read_handler);
}
}
void resolve_handler(const boost::system::error_code &ec, boost::asio::ip::tcp::resolver::iterator it)
{
if (!ec)
{
sock.async_connect(*it, connect_handler);
}
}
int main()
{
boost::asio::ip::tcp::resolver::query query("www.highscore.de", "80");
resolver.async_resolve(query, resolve_handler);
io_service.run();
}
这个程序最明显的部分是三个句柄的使用connect_handler() 和 read_handler() 函数会分别在连接被建立后以及接收到数据后被调用。 那么为什么需要 resolve_handler() 函数呢?
互联网使用了所谓的IP地址来标识每台PC。 IP地址实际上只是一长串数字难以记住。 而记住象 www.highscore.de 这样的名字就容易得多。 为了在互联网上使用类似的名字需要通过一个叫作域名解析的过程将它们翻译成相应的IP地址。 这个过程由所谓的域名解析器来完成,对应的 I/O 对象是boost::asio::ip::tcp::resolver。
域名解析也是一个需要连接到互联网的过程。 有些专门的PC被称为DNS服务器其作用就象是电话本它知晓哪个IP地址被赋给了哪台PC。 由于这个过程本身的透明的,只要明白其背后的概念以及为何需要 boost::asio::ip::tcp::resolver I/O 对象就可以了。 由于域名解析不是发生在本地的,所以它也被实现为一个异步操作。 一旦域名解析成功或被某个错误中断resolve_handler() 函数就会被调用。
因为接收数据需要一个成功的连接,进而需要一次成功的域名解析,所以这三个不同的异步操作要以三个不同的句柄来启动。 resolve_handler() 访问 I/O 对象 sock用由迭代器 it 所提供的解析后地址创建一个连接。 而 sock 也在 connect_handler() 的内部被使用,发送 HTTP 请求并启动数据的接收。 因为所有这些操作都是异步的,各个句柄的名字被作为参数传递。 取决于各个句柄,需要相应的其它参数,如指向解析后地址的迭代器 it 或用于保存接收到的数据的缓冲区 buffer。
开始执行后,该应用将创建一个类型为 boost::asio::ip::tcp::resolver::query 的对象 query表示一个查询其中含有名字 www.highscore.de 以及互联网常用的端口80。 这个查询被传递给 async_resolve() 方法以解析该名字。 最后main() 只要调用 I/O 服务的 run() 方法,将控制交给操作系统进行异步操作即可。
当域名解析的过程完成后resolve_handler() 被调用,检查域名是否能被解析。 如果解析成功,则存有错误条件的对象 ec 被设为0。 只有在这种情况下,才会相应地访问 socket 以创建连接。 服务器的地址是通过类型为 boost::asio::ip::tcp::resolver::iterator 的第二个参数来提供的。
调用了 async_connect() 方法之后connect_handler() 会被自动调用。 在该句柄的内部,会访问 ec 对象以检查连接是否已建立。 如果连接是有效的,则对相应的 socket 调用 async_read_some() 方法,启动读数据操作。 为了保存接收到的数据,要提供一个缓冲区作为第一个参数。 在以上例子中,缓冲区的类型是 boost::array它来自 Boost C++ 库 Array定义于 boost/array.hpp.
每当有一个或多个字节被接收并保存至缓冲区时read_handler() 函数就会被调用。 准确的字节数通过 std::size_t 类型的参数 bytes_transferred 给出。 同样的规则,该句柄应该首先看看参数 ec 以检查有没有接收错误。 如果是成功接收,则将数据写出至标准输出流。
请留意read_handler() 在将数据写出至 std::cout 之后,会再次调用 async_read_some() 方法。 这是必需的,因为无法保证仅在一次异步操作中就可以接收到整个网页。 async_read_some() 和 read_handler() 的交替调用只有当连接被破坏时才中止,如当 web 服务器已经传送完整个网页时。 这种情况下,在 read_handler() 内部将报告一个错误,以防止进一步将数据输出至标准输出流,以及进一步对该 socket 调用 async_read() 方法。 这时该例程将停止,因为没有更多的异步操作了。
上个例子是用来取出 www.highscore.de 的网页的,而下一个例子则示范了一个简单的 web 服务器。 其主要差别在于这个应用不会连接至其它PC而是等待连接。
#include <boost/asio.hpp>
#include <string>
boost::asio::io_service io_service;
boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), 80);
boost::asio::ip::tcp::acceptor acceptor(io_service, endpoint);
boost::asio::ip::tcp::socket sock(io_service);
std::string data = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
void write_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
{
}
void accept_handler(const boost::system::error_code &ec)
{
if (!ec)
{
boost::asio::async_write(sock, boost::asio::buffer(data), write_handler);
}
}
int main()
{
acceptor.listen();
acceptor.async_accept(sock, accept_handler);
io_service.run();
}
类型为 boost::asio::ip::tcp::acceptor 的 I/O 对象 acceptor - 被初始化为指定的协议和端口号 - 用于等待从其它PC传入的连接。 初始化工作是通过 endpoint 对象完成的,该对象的类型为 boost::asio::ip::tcp::endpoint将本例子中的接收器配置为使用端口80来等待 IP v4 的传入连接,这是 WWW 通常所使用的端口和协议。
接收器初始化完成后main() 首先调用 listen() 方法将接收器置于接收状态,然后再用 async_accept() 方法等待初始连接。 用于发送和接收数据的 socket 被作为第一个参数传递。
当一个PC试图建立一个连接时accept_handler() 被自动调用。 如果该连接请求成功,就执行自由函数 boost::asio::async_write() 来通过 socket 发送保存在 data 中的信息。 boost::asio::ip::tcp::socket 还有一个名为 async_write_some() 的方法也可以发送数据;不过它会在发送了至少一个字节之后调用相关联的句柄。 该句柄需要计算还剩余多少字节,并反复调用 async_write_some() 直至所有字节发送完毕。 而使用 boost::asio::async_write() 可以避免这些,因为这个异步操作仅在缓冲区的所有字节都被发送后才结束。
在这个例子中,当所有数据发送完毕,空函数 write_handler() 将被调用。 由于所有异步操作都已完成,所以应用程序终止。 与其它PC的连接也被相应关闭。
7.5. 开发 Boost.Asio 扩展
虽然 Boost.Asio 主要是支持网络功能的,但是加入其它 I/O 对象以执行其它的异步操作也非常容易。 本节将介绍 Boost.Asio 扩展的一个总体布局。 虽然这不是必须的,但它为其它扩展提供了一个可行的框架作为起点。
要向 Boost.Asio 中增加新的异步操作,需要实现以下三个类:
一个派生自 boost::asio::basic_io_object 的类,以表示新的 I/O 对象。使用这个新的 Boost.Asio 扩展的开发者将只会看到这个 I/O 对象。
一个派生自 boost::asio::io_service::service 的类,表示一个服务,它被注册为 I/O 服务,可以从 I/O 对象访问它。 服务与 I/O 对象之间的区别是很重要的,因为在任意给定的时间点,每个 I/O 服务只能有一个服务实例,而一个服务可以被多个 I/O 对象访问。
一个不派生自任何其它类的类,表示该服务的具体实现。 由于在任意给定的时间点每个 I/O 服务只能有一个服务实例,所以服务会为每个 I/O 对象创建一个其具体实现的实例。 该实例管理与相应 I/O 对象有关的内部数据。
本节中开发的 Boost.Asio 扩展并不仅仅提供一个框架,而是模拟一个可用的 boost::asio::deadline_timer 对象。 它与原来的 boost::asio::deadline_timer 的区别在于,计时器的时长是作为参数传递给 wait() 或 async_wait() 方法的,而不是传给构造函数。
#include <boost/asio.hpp>
#include <cstddef>
template <typename Service>
class basic_timer
: public boost::asio::basic_io_object<Service>
{
public:
explicit basic_timer(boost::asio::io_service &io_service)
: boost::asio::basic_io_object<Service>(io_service)
{
}
void wait(std::size_t seconds)
{
return this->service.wait(this->implementation, seconds);
}
template <typename Handler>
void async_wait(std::size_t seconds, Handler handler)
{
this->service.async_wait(this->implementation, seconds, handler);
}
};
每个 I/O 对象通常被实现为一个模板类,要求以一个服务来实例化 - 通常就是那个特定为此 I/O 对象开发的服务。 当一个 I/O 对象被实例化时,该服务会通过父类 boost::asio::basic_io_object 自动注册为 I/O 服务,除非它之前已经注册。 这样可确保任何 I/O 对象所使用的服务只会每个 I/O 服务只注册一次。
在 I/O 对象的内部,可以通过 service 引用来访问相应的服务,通常的访问就是将方法调用前转至该服务。 由于服务需要为每一个 I/O 对象保存数据,所以要为每一个使用该服务的 I/O 对象自动创建一个实例。 这还是在父类 boost::asio::basic_io_object 的帮助下实现的。 实际的服务实现被作为一个参数传递给任一方法调用,使得服务可以知道是哪个 I/O 对象启动了这次调用。 服务的具体实现是通过 implementation 属性来访问的。
一般一上谕I/O 对象是相对简单的:服务的安装以及服务实现的创建都是由父类 boost::asio::basic_io_object 来完成的,方法调用则只是前转至相应的服务;以 I/O 对象的实际服务实现作为参数即可。
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/system/error_code.hpp>
template <typename TimerImplementation = timer_impl>
class basic_timer_service
: public boost::asio::io_service::service
{
public:
static boost::asio::io_service::id id;
explicit basic_timer_service(boost::asio::io_service &io_service)
: boost::asio::io_service::service(io_service),
async_work_(new boost::asio::io_service::work(async_io_service_)),
async_thread_(boost::bind(&boost::asio::io_service::run, &async_io_service_))
{
}
~basic_timer_service()
{
async_work_.reset();
async_io_service_.stop();
async_thread_.join();
}
typedef boost::shared_ptr<TimerImplementation> implementation_type;
void construct(implementation_type &impl)
{
impl.reset(new TimerImplementation());
}
void destroy(implementation_type &impl)
{
impl->destroy();
impl.reset();
}
void wait(implementation_type &impl, std::size_t seconds)
{
boost::system::error_code ec;
impl->wait(seconds, ec);
boost::asio::detail::throw_error(ec);
}
template <typename Handler>
class wait_operation
{
public:
wait_operation(implementation_type &impl, boost::asio::io_service &io_service, std::size_t seconds, Handler handler)
: impl_(impl),
io_service_(io_service),
work_(io_service),
seconds_(seconds),
handler_(handler)
{
}
void operator()() const
{
implementation_type impl = impl_.lock();
if (impl)
{
boost::system::error_code ec;
impl->wait(seconds_, ec);
this->io_service_.post(boost::asio::detail::bind_handler(handler_, ec));
}
else
{
this->io_service_.post(boost::asio::detail::bind_handler(handler_, boost::asio::error::operation_aborted));
}
}
private:
boost::weak_ptr<TimerImplementation> impl_;
boost::asio::io_service &io_service_;
boost::asio::io_service::work work_;
std::size_t seconds_;
Handler handler_;
};
template <typename Handler>
void async_wait(implementation_type &impl, std::size_t seconds, Handler handler)
{
this->async_io_service_.post(wait_operation<Handler>(impl, this->get_io_service(), seconds, handler));
}
private:
void shutdown_service()
{
}
boost::asio::io_service async_io_service_;
boost::scoped_ptr<boost::asio::io_service::work> async_work_;
boost::thread async_thread_;
};
template <typename TimerImplementation>
boost::asio::io_service::id basic_timer_service<TimerImplementation>::id;
为了与 Boost.Asio 集成,一个服务必须符合几个要求:
它必须派生自 boost::asio::io_service::service。 构造函数必须接受一个指向 I/O 服务的引用,该 I/O 服务会被相应地传给 boost::asio::io_service::service 的构造函数。
任何服务都必须包含一个类型为 boost::asio::io_service::id 的静态公有属性 id。在 I/O 服务的内部是用该属性来识别服务的。
必须定义两个名为 construct() 和 destruct() 的公有方法,均要求一个类型为 implementation_type 的参数。 implementation_type 通常是该服务的具体实现的类型定义。 正如上面例子所示,在 construct() 中可以很容易地使用一个 boost::shared_ptr 对象来初始化一个服务实现,以及在 destruct() 中相应地析构它。 由于这两个方法都会在一个 I/O 对象被创建或销毁时自动被调用,所以一个服务可以分别使用 construct() 和 destruct() 为每个 I/O 对象创建和销毁服务实现。
必须定义一个名为 shutdown_service() 的方法;不过它可以是私有的。 对于一般的 Boost.Asio 扩展来说,它通常是一个空方法。 只有与 Boost.Asio 集成得非常紧密的服务才会使用它。 但是这个方法必须要有,这样扩展才能编译成功。
为了将方法调用前转至相应的服务,必须为相应的 I/O 对象定义要前转的方法。 这些方法通常具有与 I/O 对象中的方法相似的名字,如上例中的 wait() 和 async_wait()。 同步方法,如 wait(),只是访问该服务的具体实现去调用一个阻塞式的方法,而异步方法,如 async_wait(),则是在一个线程中调用这个阻塞式方法。
在线程的协助下使用异步操作,通常是通过访问一个新的 I/O 服务来完成的。 上述例子中包含了一个名为 async_io_service_ 的属性,其类型为 boost::asio::io_service。 这个 I/O 服务的 run() 方法是在它自己的线程中启动的,而它的线程是在该服务的构造函数内部由类型为 boost::thread 的 async_thread_ 创建的。 第三个属性 async_work_ 的类型为 boost::scoped_ptr<boost::asio::io_service::work>,用于避免 run() 方法立即返回。 否则,这可能会发生,因为已没有其它的异步操作在创建。 创建一个类型为 boost::asio::io_service::work 的对象并将它绑定至该 I/O 服务,这个动作也是发生在该服务的构造函数中,可以防止 run() 方法立即返回。
一个服务也可以无需访问它自身的 I/O 服务来实现 - 单线程就足够的。 为新增的线程使用一个新的 I/O 服务的原因是,这样更简单: 线程间可以用 I/O 服务来非常容易地相互通信。 在这个例子中async_wait() 创建了一个类型为 wait_operation 的函数对象,并通过 post() 方法将它传递给内部的 I/O 服务。 然后,在用于执行这个内部 I/O 服务的 run() 方法的线程内,调用该函数对象的重载 operator()()。 post() 提供了一个简单的方法,在另一个线程中执行一个函数对象。
wait_operation 的重载 operator()() 操作符基本上就是执行了和 wait() 方法相同的工作:调用服务实现中的阻塞式 wait() 方法。 但是,有可能这个 I/O 对象以及它的服务实现在这个线程执行 operator()() 操作符期间被销毁。 如果服务实现是在 destruct() 中销毁的,则 operator()() 操作符将不能再访问它。 这种情形是通过使用一个弱指针来防止的,从第一章中我们知道:如果在调用 lock() 时服务实现仍然存在,则弱指针 impl_ 返回它的一个共享指针否则它将返回0。 在这种情况下operator()() 不会访问这个服务实现,而是以一个 boost::asio::error::operation_aborted 错误来调用句柄。
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <windows.h>
class timer_impl
{
public:
timer_impl()
: handle_(CreateEvent(NULL, FALSE, FALSE, NULL))
{
}
~timer_impl()
{
CloseHandle(handle_);
}
void destroy()
{
SetEvent(handle_);
}
void wait(std::size_t seconds, boost::system::error_code &ec)
{
DWORD res = WaitForSingleObject(handle_, seconds * 1000);
if (res == WAIT_OBJECT_0)
ec = boost::asio::error::operation_aborted;
else
ec = boost::system::error_code();
}
private:
HANDLE handle_;
};
服务实现 timer_impl 使用了 Windows API 函数,只能在 Windows 中编译和使用。 这个例子的目的只是为了说明一种潜在的实现。
timer_impl 提供两个基本方法wait() 用于等待数秒。 destroy() 则用于取消一个等待操作这是必须要有的因为对于异步操作来说wait() 方法是在其自身的线程中调用的。 如果 I/O 对象及其服务实现被销毁,那么阻塞式的 wait() 方法就要尽使用 destroy() 来取消。
这个 Boost.Asio 扩展可以如下使用。
#include <boost/asio.hpp>
#include <iostream>
#include "basic_timer.hpp"
#include "timer_impl.hpp"
#include "basic_timer_service.hpp"
void wait_handler(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
typedef basic_timer<basic_timer_service<> > timer;
int main()
{
boost::asio::io_service io_service;
timer t(io_service);
t.async_wait(5, wait_handler);
io_service.run();
}
与本章开始的例子相比,这个 Boost.Asio 扩展的用法类似于 boost::asio::deadline_timer。 在实践上,应该优先使用 boost::asio::deadline_timer因为它已经集成在 Boost.Asio 中了。 这个扩展的唯一目的就是示范一下 Boost.Asio 是如何扩展新的异步操作的。
目录监视器(Directory Monitor) 是现实中的一个 Boost.Asio 扩展,它提供了一个可以监视目录的 I/O 对象。 如果被监视目录中的某个文件被创建、修改或是删除,就会相应地调用一个句柄。 当前的版本支持 Windows 和 Linux (内核版本 2.6.13 或以上)。
7.6. 练习
You can buy solutions to all exercises in this book as a ZIP file.
修改 第 7.4 节 “网络编程” 中的服务器程序,不在一次请求后即终止,而是可以处理任意多次请求。
扩展 第 7.4 节 “网络编程” 中的客户端程序即时在所接收到的HTML代码中分析某个URL。 如果找到,则同时下载相应的资源。 对于本练习只使用第一个URL。 理想情况下,网站及其资源应被保存在两个文件中而不是同时写出至标准输出流。
创建一个客户端/服务器应用在两台PC间传送文件。 当服务器端启动后它应该显示所有本地接口的IP地址并等待客户端连接。 客户端则应将服务器端的某一个IP地址以及某个本地文件的文件名作为命令行参数。 客户端应将该文件传送给服务器,后者则相应地保存它。 在传送过程中,客户端应向用户提供一些进度的可视显示。

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,21 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 14:59:21
* @LastEditTime: 2020-04-17 17:16:02
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int main()
{
char *version;
version = (char*) uv_version_string();
printf("libuv version is %s\n", version);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,24 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 19:44:48
* @LastEditTime: 2020-04-17 19:52:40
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int main()
{
uv_loop_t *loop = malloc(sizeof(uv_loop_t));
uv_loop_init(loop);
uv_run(loop, UV_RUN_DEFAULT);
printf("quit...\n");
uv_loop_close(loop);
free(loop);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,35 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 19:44:48
* @LastEditTime: 2020-04-18 09:46:21
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int64_t num = 0;
void my_idle_cb(uv_idle_t* handle)
{
num++;
if (num >= 10e6) {
printf("idle stop, num = %ld\n", num);
uv_idle_stop(handle);
}
}
int main()
{
uv_idle_t idler;
uv_idle_init(uv_default_loop(), &idler);
printf("idle start, num = %ld\n", num);
uv_idle_start(&idler, my_idle_cb);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,38 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 19:44:48
* @LastEditTime: 2020-04-18 09:47:00
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int64_t num = 0;
void my_idle_cb(uv_idle_t* handle)
{
num++;
if (num >= 10e6) {
printf("data: %s\n", (char*)handle->data);
printf("idle stop, num = %ld\n", num);
uv_idle_stop(handle);
}
}
int main()
{
uv_idle_t idler;
uv_idle_init(uv_default_loop(), &idler);
idler.data = (void*)"this is a public data...";
printf("idle start, num = %ld\n", num);
uv_idle_start(&idler, my_idle_cb);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,45 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 19:44:48
* @LastEditTime: 2020-04-20 21:20:52
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
typedef struct my_time{
int64_t now;
} my_time_t;
void my_timer_cb(uv_timer_t* handle)
{
my_time_t * update_time;
update_time = (my_time_t*)handle->data;
printf("timer callback running, time = %ld ...\n", update_time->now);
update_time->now = uv_now(uv_default_loop());
}
int main()
{
my_time_t time;
uv_timer_t timer;
// time.now = uv_now(uv_default_loop());
timer.data = (void*)&time;
uv_timer_init(uv_default_loop(), &timer);
uv_timer_start(&timer, my_timer_cb, 0, 1000);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,43 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 19:44:48
* @LastEditTime: 2020-04-20 22:33:29
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int64_t num = 0;
void my_idle_cb(uv_idle_t* handle)
{
num++;
printf("idle callback\n");
if (num >= 5) {
printf("idle stop, num = %ld\n", num);
uv_stop(uv_default_loop());
}
}
void my_prep_cb(uv_prepare_t *handle)
{
printf("prep callback\n");
}
int main()
{
uv_idle_t idler;
uv_prepare_t prep;
uv_idle_init(uv_default_loop(), &idler);
uv_idle_start(&idler, my_idle_cb);
uv_prepare_init(uv_default_loop(), &prep);
uv_prepare_start(&prep, my_prep_cb);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,43 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-17 19:44:48
* @LastEditTime: 2020-04-20 23:11:23
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int64_t num = 0;
void my_idle_cb(uv_idle_t* handle)
{
num++;
printf("idle callback\n");
if (num >= 5) {
printf("idle stop, num = %ld\n", num);
uv_stop(uv_default_loop());
}
}
void my_check_cb(uv_check_t *handle)
{
printf("check callback\n");
}
int main()
{
uv_idle_t idler;
uv_check_t check;
uv_idle_init(uv_default_loop(), &idler);
uv_idle_start(&idler, my_idle_cb);
uv_check_init(uv_default_loop(), &check);
uv_check_start(&check, my_check_cb);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,39 @@
#include <stdio.h>
#include <unistd.h>
#include <uv.h>
void hare_entry(void *arg)
{
int track_len = *((int *) arg);
while (track_len) {
track_len--;
sleep(1);
printf("hare ran another step\n");
}
printf("hare done running!\n");
}
void tortoise_entry(void *arg)
{
int track_len = *((int *) arg);
while (track_len)
{
track_len--;
printf("tortoise ran another step\n");
sleep(3);
}
printf("tortoise done running!\n");
}
int main() {
int track_len = 10;
uv_thread_t hare;
uv_thread_t tortoise;
uv_thread_create(&hare, hare_entry, &track_len);
uv_thread_create(&tortoise, tortoise_entry, &track_len);
uv_thread_join(&hare);
uv_thread_join(&tortoise);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,43 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-22 20:22:38
* @LastEditTime: 2020-04-22 21:34:25
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <unistd.h>
#include <uv.h>
void wake_entry(void *arg)
{
sleep(5);
printf("wake_entry running, wake async!\n");
uv_async_send((uv_async_t*)arg);
uv_stop(uv_default_loop());
}
void my_async_cb(uv_async_t* handle)
{
printf("my async running!\n");
}
int main()
{
uv_thread_t wake;
uv_async_t async;
uv_async_init(uv_default_loop(), &async, my_async_cb);
uv_thread_create(&wake, wake_entry, &async);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
uv_thread_join(&wake);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,47 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-22 20:22:38
* @LastEditTime: 2020-04-23 16:26:46
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>
void signal_handler(uv_signal_t *handle, int signum)
{
printf("signal received: %d\n", signum);
uv_signal_stop(handle);
}
void thread1_entry(void *userp)
{
sleep(2);
kill(0, SIGUSR1);
}
void thread2_entry(void *userp)
{
uv_signal_t signal;
uv_signal_init(uv_default_loop(), &signal);
uv_signal_start(&signal, signal_handler, SIGUSR1);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
}
int main()
{
uv_thread_t thread1, thread2;
uv_thread_create(&thread1, thread1_entry, NULL);
uv_thread_create(&thread2, thread2_entry, NULL);
uv_thread_join(&thread1);
uv_thread_join(&thread2);
return 0;
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,46 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-22 20:22:38
* @LastEditTime: 2020-04-25 01:29:52
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_process_t child_req;
uv_process_options_t options;
void process_on_exit(uv_process_t *req, int64_t exit_status, int term_signal) {
printf("process exited with status %ld, signal %d\n", exit_status, term_signal);
uv_close((uv_handle_t*) req, NULL);
}
int main()
{
int r;
char* args[3];
args[0] = "mkdir";
args[1] = "dir";
args[2] = NULL;
loop = uv_default_loop();
options.exit_cb = process_on_exit;
options.file = "mkdir";
options.args = args;
if ((r = uv_spawn(loop, &child_req, &options))) {
printf("%s\n", uv_strerror(r));
return 1;
} else {
printf("Launched process with ID %d\n", child_req.pid);
}
return uv_run(loop, UV_RUN_DEFAULT);
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,44 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-22 20:22:38
* @LastEditTime: 2020-04-29 00:15:55
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
int main(void)
{
uv_write_t req;
uv_buf_t buf;
uv_loop_t *loop;
uv_tty_t tty;
loop = uv_default_loop();
uv_tty_init(loop, &tty, STDOUT_FILENO, 0);
uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL);
if (uv_guess_handle(STDOUT_FILENO) == UV_TTY) {
buf.base = "\033[41;37m";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
}
buf.base = "Hello TTY\n";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
uv_tty_reset_mode();
return uv_run(loop, UV_RUN_DEFAULT);
}

View File

@@ -0,0 +1,25 @@
CC=gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude -lpthread -luv -lcurl
TARGET = targets
$(TARGET):$(OBJS)
$(CC) -o $@ $^ $(FLAG)
ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif
%.o:%.c
$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
clean:
rm -rf $(TARGET) $(OBJS)
distclean:
rm -rf $(DEP_FILES)
.PHONY:clean

View File

@@ -0,0 +1,93 @@
/*
* @Author: jiejie
* @Github: https://github.com/jiejieTop
* @Date: 2020-04-22 20:22:38
* @LastEditTime: 2020-05-22 20:33:16
* @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#define DEFAULT_PORT 6666
#define DEFAULT_BACKLOG 128
uv_loop_t *loop;
struct sockaddr_in addr;
typedef struct {
uv_write_t req;
uv_buf_t buf;
} write_req_t;
void free_write_req(uv_write_t *req) {
write_req_t *wr = (write_req_t*) req;
free(wr->buf.base);
free(wr);
}
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = (char*) malloc(suggested_size);
buf->len = suggested_size;
}
void echo_write(uv_write_t *req, int status) {
if (status) {
fprintf(stderr, "Write error %s\n", uv_strerror(status));
}
free_write_req(req);
}
void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
if (nread > 0) {
write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t));
req->buf = uv_buf_init(buf->base, nread);
uv_write((uv_write_t*) req, client, &req->buf, 1, echo_write);
return;
}
if (nread < 0) {
if (nread != UV_EOF)
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*) client, NULL);
}
free(buf->base);
}
void on_new_connection(uv_stream_t *server, int status) {
if (status < 0) {
fprintf(stderr, "New connection error %s\n", uv_strerror(status));
// error!
return;
}
uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if (uv_accept(server, (uv_stream_t*) client) == 0) {
printf("client connect success ...\n");
uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
}
else {
uv_close((uv_handle_t*) client, NULL);
}
}
int main() {
loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);
if (r) {
fprintf(stderr, "Listen error %s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}

View File

@@ -0,0 +1,17 @@
# libuv-learning-code
libuv-learning-code
| 状态 | 目录 | 说明 | 博客教程 |
| -- | -- | -- | -- |
| ☑ | 001 | 打印libuv版本 | [【libuv高效编程】libuv学习超详细教程1——libuv的编译与安装](https://blog.csdn.net/jiejiemcu/article/details/105586752) |
| ☑ | 002 | uv_loop_t 事件循环 | [【libuv高效编程】libuv学习超详细教程3——libuv事件循环](https://blog.csdn.net/jiejiemcu/article/details/105622428) |
| ☑ | 003 | idle 空闲句柄 | [【libuv高效编程】libuv学习超详细教程4——idle handle解读](https://blog.csdn.net/jiejiemcu/article/details/105670311) |
| ☑ | 004 | 句柄数据域data | - |
| ☑ | 005 | timer 定时器句柄 | - |
| ☑ | 006 | prepare 准备句柄 | [【libuv高效编程】libuv学习超详细教程5——prepare handle解读](https://jiejie.blog.csdn.net/article/details/105681010) |
| ☑ | 007 | check 检查句柄 | [【libuv高效编程】libuv学习超详细教程6——check handle解读](https://blog.csdn.net/jiejiemcu/article/details/105681625) |
| ☑ | 008 | uv_thread_t libuv线程 | [【libuv高效编程】libuv学习超详细教程7——libuv线程解读](https://blog.csdn.net/jiejiemcu/article/details/105687789) |
| ☑ | 009 | async 异步句柄 | [【libuv高效编程】libuv学习超详细教程9——libuv async异步句柄解读](https://blog.csdn.net/jiejiemcu/article/details/105738484) |
| ☑ | 010 | signal 信号句柄 | [【libuv高效编程】libuv学习超详细教程8——libuv signal 信号句柄解读](https://blog.csdn.net/jiejiemcu/article/details/105717692) |
| ☑ | 011 | process 进程句柄 | - |

View File

@@ -0,0 +1,28 @@
# libuv中文教程
翻译自[《An Introduction to libuv》](https://github.com/nikhilm/uvbook).
会持续关注原教程并更新中文版本教程基于libuv的v1.3.0。
## 在线阅读
目前关闭gitbook在线阅读大家直接在github上查看md文件即可。
## 目录
1. [简介](./source/introduction.md)
2. [libuv基础](./source/basics_of_libuv.md)
3. [文件系统](./source/filesystem.md)
4. [网络](./source/networking.md)
5. [线程](./source/threads.md)
6. [进程](./source/processes.md)
7. [高级事件循环](./source/advanced-event-loops.md)
8. [实用工具](./source/utilities.md)
9. [关于](./source/about.md)
## 翻译人员
翻译人员[在这](https://github.com/luohaha/Chinese-uvbook/graphs/contributors)。
## 辅助阅读
1. [libuv官方文档](http://docs.libuv.org/en/v1.x/)-由于教程有些知识点讲解得不够深入,需要我们自行阅读官方文档,来加强理解。
2. [教程的完整代码](https://github.com/nikhilm/uvbook/tree/master/code)-教程中展示的代码并不完整,对于一些复杂的程序,需要阅读完整的实例代码。
## 说明
在翻译的过程中对于一些个人觉得可能不是那么容易理解的知识点我都会附上自己收集的说明资料的链接以方便学习。由于个人的英文水平有限如果大家发现翻译出错或者不合适的地方欢迎PR修改master分支下source文件夹下的md文件即可

View File

@@ -0,0 +1,11 @@
# About
[Nikhil Marathe](http://nikhilism.com)在某一个下午(June 16, 2012)开始写这本书。当他在写[node-taglib](https://github.com/nikhilm/node-taglib)的时候苦于没有好的libuv文档。虽然已经有了官方文档但是没有好理解的教程。本书正是应需求而生并且努力变得准确。也就是说本书中可能会有错误。所以鼓励大家Pull requests。你当然可以直接给他发[email](nsm.nikhil%40gmail.com),告诉他错误。
Nikhil从Marc Lehmann的关于libev的[手册](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod)中讲解libev和libuv的不同点的部分获取了不少的灵感。
本书的中文翻译者在[这里](https://github.com/luohaha/Chinese-uvbook/graphs/contributors)。同样欢迎您的Pull requests来改进本书的翻译工作。
### Licensing
The contents of this book are licensed as [Creative Commons - Attribution](http://creativecommons.org/licenses/by/3.0/). All code is in the public domain.

View File

@@ -0,0 +1,78 @@
# Advanced event loops
libuv提供了非常多的控制event-loop的方法你能通过使用多loop来实现很多有趣的功能。你还可以将libuv的event loop嵌入到其它基于event-loop的库中。比如想象着一个基于Qt的UI然后Qt的event-loop是由libuv驱动的做着加强级的系统任务。
## Stopping an event loop
`uv_stop()`用来终止event loop。loop会停止的最早时间点是在下次循环的时候或者稍晚些的时候。这也就意味着在本次循环中已经准备被处理的事件依然会被处理`uv_stop`不会起到作用。当`uv_stop`被调用在当前的循环中loop不会被IO操作阻塞。上面这些说得有点玄乎还是让我们看下`uv_run()`的代码:
#### src/unix/core.c - uv_run
```c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop);
ran_pending = uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
```
`stop_flag``uv_stop`设置。现在所有的libuv回调函数都是在一次loop循环中被调用的因此调用`uv_stop`并不能中止本次循环。首先libuv会更新定时器然后运行接下来的定时器空转和准备回调调用任何准备好的IO回调函数。如果你在它们之间的任何一个时间里调用`uv_stop()``stop_flag`会被设置为1。这会导致`uv_backend_timeout()`返回0这也就是为什么loop不会阻塞在IO上。从另外的角度来说你在任何一个检查handler中调用`uv_stop`此时I/O已经完成所以也没有影响。
在已经得到结果,或是发生错误的时候,`uv_stop()`可以用来关闭一个loop而且不需要保证handler停止的顺序。
下面是一个简单的例子它演示了loop的停止以及当前的循环依旧在执行。
#### uvstop/main.c
```c
#include <stdio.h>
#include <uv.h>
int64_t counter = 0;
void idle_cb(uv_idle_t *handle) {
printf("Idle callback\n");
counter++;
if (counter >= 5) {
uv_stop(uv_default_loop());
printf("uv_stop() called\n");
}
}
void prep_cb(uv_prepare_t *handle) {
printf("Prep callback\n");
}
int main() {
uv_idle_t idler;
uv_prepare_t prep;
uv_idle_init(uv_default_loop(), &idler);
uv_idle_start(&idler, idle_cb);
uv_prepare_init(uv_default_loop(), &prep);
uv_prepare_start(&prep, prep_cb);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}
```

View File

@@ -0,0 +1,170 @@
# Basics of libuv
libuv强制使用异步和事件驱动的编程风格。它的核心工作是提供一个event-loop还有基于I/O和其它事件通知的回调函数。libuv还提供了一些核心工具例如定时器非阻塞的网络支持异步文件系统访问子进程等。
## Event loops
在事件驱动编程中程序会关注每一个事件并且对每一个事件的发生做出反应。libuv会负责将来自操作系统的事件收集起来或者监视其他来源的事件。这样用户就可以注册回调函数回调函数会在事件发生的时候被调用。event-loop会一直保持运行状态。用伪代码描述如下
```c
while there are still events to process:
e = get the next event
if there is a callback associated with e:
call the callback
```
举几个事件的例子:
>* 准备好被写入的文件。
>* 包含准备被读取的数据的socket。
>* 超时的定时器。
event-loop最终会被`uv_run()`启动当使用libuv时最后都会调用的函数。
系统编程中最经常处理的一般是输入和输出,而不是一大堆的数据处理。问题在于传统的输入/输出函数(例如`read``fprintf`)都是阻塞式的。实际上向文件写入数据从网络读取数据所花的时间对比CPU的处理速度差得太多。任务没有完成函数是不会返回的所以你的程序在这段时间内什么也做不了。对于需要高性能的的程序来说这是一个主要的障碍因为其他活动和I/O操作都在保持等待。
其中一个标准的解决方案是使用多线程。每一个阻塞的I/O操作都会被分配到各个线程中或者是使用线程池。当某个线程一旦阻塞处理器就可以调度处理其他需要cpu资源的线程。
但是libuv使用了另外一个解决方案那就是异步非阻塞风格。大多数的现代操作系统提供了基于事件通知的子系统。例如一个正常的socket上的`read`调用会发生阻塞直到发送方把信息发送过来。但是实际上程序可以请求操作系统监视socket事件的到来并将这个事件通知放到事件队列中。这样程序就可以很简单地检查事件是否到来可能此时正在使用cpu做数值处理的运算并及时地获取数据。说libuv是异步的是因为程序可以在一头表达对某一事件的兴趣并在另一头获取到数据对于时间或是空间来说。它是非阻塞是因为应用程序无需在请求数据后等待可以自由地做其他的事。libuv的事件循环方式很好地与该模型匹配, 因为操作系统事件可以视为另外一种libuv事件. 非阻塞方式可以保证在其他事件到来时被尽快处理(当然还要考虑硬件的能力)。
##### Note
>我们不需要关心I/O在后台是如何工作的但是由于我们的计算机硬件的工作方式线程是处理器最基本的执行单元libuv和操作系统通常会运行后台/工作者线程, 或者采用非阻塞方式来轮流执行任务。
Bert Belder一个libuv的核心开发者通过一个短视频向我们解释了libuv的架构和它的后台工作方式。如果你之前没有接触过类似libuvlibev这个视频会非常有用。视频的网址是https://youtu.be/nGn60vDSxQ4 。
包含了libuv的event-loop的更多详细信息的[文档](http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop)。
## HELLO WORLD
让我们开始写第一个libuv程序吧它什么都没做只是开启了一个loop然后很快地退出了。
#### helloworld/main.c
```c
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int main() {
uv_loop_t *loop = malloc(sizeof(uv_loop_t));
uv_loop_init(loop);
printf("Now quitting.\n");
uv_run(loop, UV_RUN_DEFAULT);
uv_loop_close(loop);
free(loop);
return 0;
}
```
这个程序会很快就退出了因为没有可以很处理的事件。我们可以使用各种API函数来告诉event-loop我们要监视的事件。
从libuv的1.0版本开始,用户就可以在使用`uv_loop_init`初始化loop之前给其分配相应的内存。这就允许你植入自定义的内存管理方法。记住要使用`uv_loop_close(uv_loop_t *)`关闭loop然后再回收内存空间。在例子中程序退出的时候会关闭loop系统也会自动回收内存。对于长时间运行的程序来说合理释放内存很重要。
### Default loop
可以使用`uv_default_loop`获取libuv提供的默认loop。如果你只需要一个loop的话可以使用这个。
##### Note
>nodejs中使用了默认的loop作为自己的主loop。如果你在编写nodejs的绑定你应该注意一下。
## Error handling
初始化函数或者同步执行的函数,会在执行失败后返回代表错误的负数。但是对于异步执行的函数,会在执行失败的时候,给它们的回调函数传递一个状态参数。错误信息被定义为`UV_E*`[常量](http://docs.libuv.org/en/v1.x/errors.html#error-constants)。
你可以使用`uv_strerror(int)``uv_err_name(int)`分别获取`const char *`格式的错误信息和错误名字。
I/O函数的回调函数例如文件和socket等会被传递一个`nread`参数。如果`nread`小于0就代表出现了错误当然UV_EOF是读取到文件末端的错误你要特殊处理
##Handles and Requests
libuv的工作建立在用户表达对特定事件的兴趣。这通常通过创造对应I/O设备定时器进程等的handle来实现。handle是不透明的数据结构其中对应的类型`uv_TYPE_t`中的type指定了handle的使用目的。
#### libuv watchers
```c
/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;
/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;
/* None of the above. */
typedef struct uv_cpu_info_s uv_cpu_info_t;
typedef struct uv_interface_address_s uv_interface_address_t;
typedef struct uv_dirent_s uv_dirent_t;
```
handle代表了持久性对象。在异步的操作中相应的handle上有许多与之关联的request。request是短暂性对象通常只维持在一个回调函数的时间通常对映着handle上的一个I/O操作。Requests用来在初始函数和回调函数之间传递上下文。例如uv_udp_t代表了一个udp的socket然而对于每一个向socket的写入的完成后都会向回调函数传递一个`uv_udp_send_t`
handle可以通过下面的函数设置
```c
uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)
```
回调函数是libuv所关注的事件发生后所调用的函数。应用程序的特定逻辑会在回调函数中实现。例如一个IO监视器的回调函数会接收到从文件读取到的数据一个定时器的回调函数会在超时后被触发等等。
### Idling
下面有一个使用空转handle的例子。回调函数在每一个循环中都会被调用。在Utilities这部分会讲到一些空转handle的使用场景。现在让我们使用一个空转监视器然后来观察它的生命周期接着看`uv_run`调用会造成阻塞。当达到事先规定好的计数后,空转监视器会退出。因为`uv_run`已经找不到活着的事件监视器了,所以`uv_run()`也退出。
#### idle-basic/main.c
```c
#include <stdio.h>
#include <uv.h>
int64_t counter = 0;
void wait_for_a_while(uv_idle_t* handle) {
counter++;
if (counter >= 10e6)
uv_idle_stop(handle);
}
int main() {
uv_idle_t idler;
uv_idle_init(uv_default_loop(), &idler);
uv_idle_start(&idler, wait_for_a_while);
printf("Idling...\n");
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
uv_loop_close(uv_default_loop());
return 0;
}
```
### Storing context
在基于回调函数的编程风格中你可能会需要在调用处和回调函数之间传递一些上下文等特定的应用信息。所有的handle和request都有一个`data`可以用来存储信息并传递。这是一个c语言库中很常见的模式。即使是`uv_loop_t`也有一个相似的`data`域。

View File

@@ -0,0 +1,436 @@
# Filesystem
简单的文件读写是通过```uv_fs_*```函数族和与之相关的```uv_fs_t```结构体完成的。
#### note
>libuv 提供的文件操作和 socket operations 并不相同。套接字操作使用了操作系统本身提供了非阻塞操作,而文件操作内部使用了阻塞函数,但是 libuv 是在线程池中调用这些函数,并在应用程序需要交互时通知在事件循环中注册的监视器。
所有的文件操作函数都有两种形式 - 同步**(synchronous)** 和 异步**( asynchronous)**。
同步方式如果没有指定回调函数则会被自动调用( 并阻塞),函数的返回值是[libuv error code](http://docs.libuv.org/en/v1.x/guide/basics.html#libuv-error-handling) 。但以上通常只对同步调用有意义。而异步方式则会在传入回调函数时被调用, 并且返回 0。
## Reading/Writing files
文件描述符可以采用如下方式获得:
```c
int uv_fs_open(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags, int mode, uv_fs_cb cb)
```
参数```flags```与```mode```和标准的 Unix flags 相同。libuv 会小心地处理 Windows 环境下的相关标志位(flags)的转换, 所以编写跨平台程序时你不用担心不同平台上文件打开的标志位不同。
关闭文件描述符可以使用:
```c
int uv_fs_close(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_fs_cb cb)
```
文件系统的回调函数有如下的形式:
```c
void callback(uv_fs_t* req);
```
让我们看一下一个简单的```cat```命令的实现。我们通过当文件被打开时注册一个回调函数来开始:
#### uvcat/main.c - opening a file
```c
void on_open(uv_fs_t *req) {
// The request passed to the callback is the same as the one the call setup
// function was passed.
assert(req == &open_req);
if (req->result >= 0) {
iov = uv_buf_init(buffer, sizeof(buffer));
uv_fs_read(uv_default_loop(), &read_req, req->result,
&iov, 1, -1, on_read);
}
else {
fprintf(stderr, "error opening file: %s\n", uv_strerror((int)req->result));
}
}
```
`uv_fs_t`的`result`域保存了`uv_fs_open`回调函数**打开的文件描述符**。如果文件被正确地打开,我们可以开始读取了。
#### uvcat/main.c - read callback
```c
void on_read(uv_fs_t *req) {
if (req->result < 0) {
fprintf(stderr, "Read error: %s\n", uv_strerror(req->result));
}
else if (req->result == 0) {
uv_fs_t close_req;
// synchronous
uv_fs_close(uv_default_loop(), &close_req, open_req.result, NULL);
}
else if (req->result > 0) {
iov.len = req->result;
uv_fs_write(uv_default_loop(), &write_req, 1, &iov, 1, -1, on_write);
}
}
```
在调用读取函数的时候,你必须传递一个已经初始化的缓冲区,在```on_read()```被触发后,缓冲区被写入数据。```uv_fs_*```系列的函数是和POSIX的函数对应的所以当读到文件的末尾时(EOF)result返回0。在使用streams或者pipe的情况下使用的是libuv自定义的```UV_EOF```。
现在你看到类似的异步编程的模式。但是```uv_fs_close()```是同步的。一般来说一次性的开始的或者关闭的部分都是同步的因为我们一般关心的主要是任务和多路I/O的快速I/O。所以在这些对性能微不足道的地方都是使用同步的这样代码还会简单一些。
文件系统的写入使用 ```uv_fs_write()```,当写入完成时会触发回调函数,在这个例子中回调函数会触发下一次的读取。
#### uvcat/main.c - write callback
```c
void on_write(uv_fs_t *req) {
if (req->result < 0) {
fprintf(stderr, "Write error: %s\n", uv_strerror((int)req->result));
}
else {
uv_fs_read(uv_default_loop(), &read_req, open_req.result, &iov, 1, -1, on_read);
}
}
```
##### Warning
>由于文件系统和磁盘的调度策略,写入成功的数据不一定就存在磁盘上。
我们开始在main中推动多米诺骨牌
#### uvcat/main.c
```c
int main(int argc, char **argv) {
uv_fs_open(uv_default_loop(), &open_req, argv[1], O_RDONLY, 0, on_open);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
uv_fs_req_cleanup(&open_req);
uv_fs_req_cleanup(&read_req);
uv_fs_req_cleanup(&write_req);
return 0;
}
```
##### Warning
>函数uv_fs_req_cleanup()在文件系统操作结束后必须要被调用,用来回收在读写中分配的内存。
##Filesystem operations
所有像 ``unlink``, ``rmdir``, ``stat`` 这样的标准文件操作都是支持异步的并且使用方法和上述类似。下面的各个函数的使用方法和read/write/open类似在``uv_fs_t.result``中保存返回值。所有的函数如下所示:
译者注返回的result值<0表示出错其他值表示成功。但>=0的值在不同的函数中表示的意义不一样比如在```uv_fs_read```或者```uv_fs_write```中,它代表读取或写入的数据总量,但在```uv_fs_open```中表示打开的文件描述符。)
```c
UV_EXTERN int uv_fs_close(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
uv_fs_cb cb);
UV_EXTERN int uv_fs_open(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int flags,
int mode,
uv_fs_cb cb);
UV_EXTERN int uv_fs_read(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
const uv_buf_t bufs[],
unsigned int nbufs,
int64_t offset,
uv_fs_cb cb);
UV_EXTERN int uv_fs_unlink(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
uv_fs_cb cb);
UV_EXTERN int uv_fs_write(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
const uv_buf_t bufs[],
unsigned int nbufs,
int64_t offset,
uv_fs_cb cb);
UV_EXTERN int uv_fs_mkdir(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int mode,
uv_fs_cb cb);
UV_EXTERN int uv_fs_mkdtemp(uv_loop_t* loop,
uv_fs_t* req,
const char* tpl,
uv_fs_cb cb);
UV_EXTERN int uv_fs_rmdir(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
uv_fs_cb cb);
UV_EXTERN int uv_fs_scandir(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int flags,
uv_fs_cb cb);
UV_EXTERN int uv_fs_scandir_next(uv_fs_t* req,
uv_dirent_t* ent);
UV_EXTERN int uv_fs_stat(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
uv_fs_cb cb);
UV_EXTERN int uv_fs_fstat(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
uv_fs_cb cb);
UV_EXTERN int uv_fs_rename(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
const char* new_path,
uv_fs_cb cb);
UV_EXTERN int uv_fs_fsync(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
uv_fs_cb cb);
UV_EXTERN int uv_fs_fdatasync(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
uv_fs_cb cb);
UV_EXTERN int uv_fs_ftruncate(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
int64_t offset,
uv_fs_cb cb);
UV_EXTERN int uv_fs_sendfile(uv_loop_t* loop,
uv_fs_t* req,
uv_file out_fd,
uv_file in_fd,
int64_t in_offset,
size_t length,
uv_fs_cb cb);
UV_EXTERN int uv_fs_access(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int mode,
uv_fs_cb cb);
UV_EXTERN int uv_fs_chmod(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int mode,
uv_fs_cb cb);
UV_EXTERN int uv_fs_utime(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
double atime,
double mtime,
uv_fs_cb cb);
UV_EXTERN int uv_fs_futime(uv_loop_t* loop,
uv_fs_t* req,
uv_file file,
double atime,
double mtime,
uv_fs_cb cb);
UV_EXTERN int uv_fs_lstat(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
uv_fs_cb cb);
UV_EXTERN int uv_fs_link(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
const char* new_path,
uv_fs_cb cb);
```
## Buffers and Streams
在libuv中最基础的I/O操作是流stream(``uv_stream_t``)。TCP套接字UDP套接字管道对于文件I/O和IPC来说都可以看成是流stream(``uv_stream_t``)的子类。
上面提到的各个流的子类都有各自的初始化函数,然后可以使用下面的函数操作:
```c
int uv_read_start(uv_stream_t*, uv_alloc_cb alloc_cb, uv_read_cb read_cb);
int uv_read_stop(uv_stream_t*);
int uv_write(uv_write_t* req, uv_stream_t* handle,
const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb);
```
可以看出,流操作要比上述的文件操作要简单一些,而且当``uv_read_start()``一旦被调用libuv会保持从流中持续地读取数据直到``uv_read_stop()``被调用。
数据的离散单元是buffer-``uv_buffer_t``。它包含了指向数据的开始地址的指针(``uv_buf_t.base``)和buffer的长度(``uv_buf_t.len``)这两个信息。``uv_buf_t``很轻量级,使用值传递。我们需要管理的只是实际的数据,即程序必须自己分配和回收内存。
**ERROR**
THIS PROGRAM DOES NOT ALWAYS WORK, NEED SOMETHING BETTER
为了更好地演示流stream我们将会使用``uv_pipe_t``。它可以将本地文件转换为流stream的形态。接下来的这个是使用libuv实现的一个简单的tee工具如果不是很了解请看[维基百科](https://en.wikipedia.org/wiki/Tee_(command))。所有的操作都是异步的这也正是事件驱动I/O的威力所在。两个输出操作不会相互阻塞但是我们也必须要注意确保一块缓冲区不会在还没有写入之前就提前被回收了。
这个程序执行命令如下
```
./uvtee <output_file>
```
在使用pipe打开文件时libuv会默认地以可读和可写的方式打开文件。
#### uvtee/main.c - read on pipes
```c
int main(int argc, char **argv) {
loop = uv_default_loop();
uv_pipe_init(loop, &stdin_pipe, 0);
uv_pipe_open(&stdin_pipe, 0);
uv_pipe_init(loop, &stdout_pipe, 0);
uv_pipe_open(&stdout_pipe, 1);
uv_fs_t file_req;
int fd = uv_fs_open(loop, &file_req, argv[1], O_CREAT | O_RDWR, 0644, NULL);
uv_pipe_init(loop, &file_pipe, 0);
uv_pipe_open(&file_pipe, fd);
uv_read_start((uv_stream_t*)&stdin_pipe, alloc_buffer, read_stdin);
uv_run(loop, UV_RUN_DEFAULT);
return 0;
}
```
当需要使用具名管道的时候(**译注:匿名管道 是Unix最初的IPC形式但是由于匿名管道的局限性后来出现了具名管道 FIFO这种管道由于可以在文件系统中创建一个名字所以可以被没有亲缘关系的进程访问**``uv_pipe_init()``的第三个参数应该被设置为1。这部分会在Process进程的这一章节说明。``uv_pipe_open()``函数把管道和文件描述符关联起来,在上面的代码中表示把管道``stdin_pipe``和标准输入关联起来(**译者注:``0``代表标准输入,``1``代表标准输出,``2``代表标准错误输出**)。
当调用``uv_read_start()``后,我们开始监听``stdin``当需要新的缓冲区来存储数据时调用alloc_buffer在函数``read_stdin()``中可以定义缓冲区中的数据处理操作。
#### uvtee/main.c - reading buffers
```c
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
*buf = uv_buf_init((char*) malloc(suggested_size), suggested_size);
}
void read_stdin(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
if (nread < 0){
if (nread == UV_EOF){
// end of file
uv_close((uv_handle_t *)&stdin_pipe, NULL);
uv_close((uv_handle_t *)&stdout_pipe, NULL);
uv_close((uv_handle_t *)&file_pipe, NULL);
}
} else if (nread > 0) {
write_data((uv_stream_t *)&stdout_pipe, nread, *buf, on_stdout_write);
write_data((uv_stream_t *)&file_pipe, nread, *buf, on_file_write);
}
if (buf->base)
free(buf->base);
}
```
标准的``malloc``是非常高效的方法但是你依然可以使用其它的内存分配的策略。比如nodejs使用自己的内存分配方法```Smalloc```它将buffer用v8的对象关联起来具体的可以查看[nodejs的官方文档](https://nodejs.org/docs/v0.11.5/api/smalloc.html)。
当回调函数```read_stdin()```的nread参数小于0时表示错误发生了。其中一种可能的错误是EOF(**读到文件的尾部**),这时我们可以使用函数```uv_close()```关闭流了。除此之外当nread大于0时nread代表我们可以向输出流中写入的字节数目。最后注意缓冲区要由我们手动回收。
当分配函数```alloc_buf()```返回一个长度为0的缓冲区时代表它分配内存失败。在这种情况下读取的回调函数会被错误```UV_ENOBUFS```唤醒。libuv同时也会继续尝试从流中读取数据所以如果你想要停止的话必须明确地调用```uv_close()```.
当nread为0时代表已经没有可读的了大多数的程序会自动忽略这个。
#### uvtee/main.c - Write to pipe
```c
typedef struct {
uv_write_t req;
uv_buf_t buf;
} write_req_t;
void free_write_req(uv_write_t *req) {
write_req_t *wr = (write_req_t*) req;
free(wr->buf.base);
free(wr);
}
void on_stdout_write(uv_write_t *req, int status) {
free_write_req(req);
}
void on_file_write(uv_write_t *req, int status) {
free_write_req(req);
}
void write_data(uv_stream_t *dest, size_t size, uv_buf_t buf, uv_write_cb cb) {
write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t));
req->buf = uv_buf_init((char*) malloc(size), size);
memcpy(req->buf.base, buf.base, size);
uv_write((uv_write_t*) req, (uv_stream_t*)dest, &req->buf, 1, cb);
}
```
`write_data()`开辟了一块地址空间存储从缓冲区读取出来的数据。这块缓存不会被释放,直到与``uv_write()``绑定的回调函数执行。为了实现它,我们用结构体``write_req_t``包裹一个write request和一个buffer然后在回调函数中展开它。因为我们复制了一份缓存所以我们可以在两个``write_data()``中独立释放两个缓存。 我们之所以这样做是因为,两个调用`write_data()`是相互独立的。为了保证它们不会因为读取速度的原因,由于共享一片缓冲区而损失掉独立性,所以才开辟了新的两块区域。当然这只是一个简单的例子,你可以使用更聪明的内存管理方法来实现它,比如引用计数或者缓冲区池等。
##### WARNING
>你的程序在被其他的程序调用的过程中有意无意地会向pipe写入数据这样的话它会很容易被信号SIGPIPE终止掉你最好在初始化程序的时候加入这句
>`signal(SIGPIPE, SIG_IGN)`。
## File change events
所有的现代操作系统都会提供相应的API来监视文件和文件夹的变化(**如Linux的inotifyDarwin的FSEventsBSD的kqueueWindows的ReadDirectoryChangesW Solaris的event ports**)。libuv同样包括了这样的文件监视库。这是libuv中很不协调的部分因为在跨平台的前提上实现这个功能很难。为了更好地说明我们现在来写一个监视文件变化的命令
```
./onchange <command> <file1> [file2] ...
```
实现这个监视器,要从```uv_fs_event_init()```开始:
#### onchange/main.c - The setup
```c
int main(int argc, char **argv) {
if (argc <= 2) {
fprintf(stderr, "Usage: %s <command> <file1> [file2 ...]\n", argv[0]);
return 1;
}
loop = uv_default_loop();
command = argv[1];
while (argc-- > 2) {
fprintf(stderr, "Adding watch on %s\n", argv[argc]);
uv_fs_event_t *fs_event_req = malloc(sizeof(uv_fs_event_t));
uv_fs_event_init(loop, fs_event_req);
// The recursive flag watches subdirectories too.
uv_fs_event_start(fs_event_req, run_command, argv[argc], UV_FS_EVENT_RECURSIVE);
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
函数```uv_fs_event_start()```的第三个参数是要监视的文件或文件夹。最后一个参数,```flags```,可以是:
```
UV_FS_EVENT_WATCH_ENTRY = 1,
UV_FS_EVENT_STAT = 2,
UV_FS_EVENT_RECURSIVE = 4
```
`UV_FS_EVENT_WATCH_ENTRY`和`UV_FS_EVENT_STAT`不做任何事情(至少目前是这样)`UV_FS_EVENT_RECURSIVE`可以在支持的系统平台上递归地监视子文件夹。
在回调函数`run_command()`中,接收的参数如下:
>1.`uv_fs_event_t *handle`-句柄。里面的path保存了发生改变的文件的地址。
>2.`const char *filename`-如果目录被监视它代表发生改变的文件名。只在Linux和Windows上不为null在其他平台上可能为null。
>3.`int flags` -`UV_RENAME`名字改变,`UV_CHANGE`内容改变之一,或者他们两者的按位或的结果(`|`)。
>4.`int status`当前为0。
在我们的例子中,只是简单地打印参数和调用`system()`运行command.
#### onchange/main.c - file change notification callback
```c
void run_command(uv_fs_event_t *handle, const char *filename, int events, int status) {
char path[1024];
size_t size = 1023;
// Does not handle error if path is longer than 1023.
uv_fs_event_getpath(handle, path, &size);
path[size] = '\0';
fprintf(stderr, "Change detected in %s: ", path);
if (events & UV_RENAME)
fprintf(stderr, "renamed");
if (events & UV_CHANGE)
fprintf(stderr, "changed");
fprintf(stderr, " %s\n", filename ? filename : "");
system(command);
}
```
----

View File

@@ -0,0 +1,28 @@
# Introduction
本书由很多的libuv教程组成libuv是一个高性能的事件驱动的I/O库并且提供了跨平台如windows, linux的API。
本书会涵盖libuv的主要部分但是不会详细地讲解每一个函数和数据结构。[官方文档](http://docs.libuv.org/en/v1.x/)中可以查阅到完整的内容。
本书依然在不断完善中,所以有些章节会不完整,但我希望你能喜欢它。
## Who this book is for
如果你正在读此书,你或许是:
>1. 系统程序员会编写一些底层的程序例如守护进程或者网络服务器客户端。你也许发现了event-loop很适合于你的应用场景然后你决定使用libuv。
>2. 一个node.js的模块开发人员决定使用C/C++封装系统平台某些同步或者异步API并将其暴露给Javascript。你可以在node.js中只使用libuv。但你也需要参考其他资源因为本书并没有包括v8/node.js相关的内容。
本书假设你对c语言有一定的了解。
## Background
[node.js](https://nodejs.org/en/)最初开始于2009年是一个可以让Javascript代码离开浏览器的执行环境也可以执行的项目。 node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js将事件驱动的I/O模型与适合该模型的编程语言(Javascript)融合在了一起。随着node.js的日益流行node.js需要同时支持windows, 但是libev只能在Unix环境下运行。Windows 平台上与kqueue(FreeBSD)或者(e)poll(Linux)等内核事件通知相应的机制是IOCP。libuv提供了一个跨平台的抽象由平台决定使用libev或IOCP。在node-v0.9.0版本中libuv移除了libev的内容。
随着libuv的日益成熟它成为了拥有卓越性能的系统编程库。除了node.js以外包括Mozilla的[Rust](http://rust-lang.org)编程语言和许多的语言都开始使用libuv。
本书基于libuv的v1.3.0。
## Code
本书中的实例代码都可以在[Github](https://github.com/nikhilm/uvbook/tree/master/code)上找到。

View File

@@ -0,0 +1,287 @@
# Networking
在 libuv 中,网络编程与直接使用 BSD socket 区别不大有些地方还更简单概念保持不变的同时libuv 上所有接口都是非阻塞的。它还提供了很多工具函数,抽象了恼人、啰嗦的底层任务,如使用 BSD socket 结构体设置 socket 、DNS 查找以及调整各种 socket 参数。
在网络I/O中会使用到```uv_tcp_t```和```uv_udp_t```。
##### note
>本章中的代码片段仅用于展示 libuv API ,并不是优质代码的范例,常有内存泄露和未关闭的连接。
## TCP
TCP是面向连接的字节流协议因此基于libuv的stream实现。
#### server
服务器端的建立流程如下:
1.```uv_tcp_init```建立tcp句柄。
2.```uv_tcp_bind```绑定。
3.```uv_listen```建立监听,当有新的连接到来时,激活调用回调函数。
4.```uv_accept```接收链接。
5.使用stream操作来和客户端通信。
#### tcp-echo-server/main.c - The listen socket
```c
int main() {
loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);
if (r) {
fprintf(stderr, "Listen error %s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
你可以调用```uv_ip4_addr()```函数来将ip地址和端口号转换为sockaddr_in结构这样就可以被BSD的socket使用了。要想完成逆转换的话可以调用```uv_ip4_name()```。
##### note
>对应ipv6有类似的uv_ip6_*
大多数的设置函数是同步的因为它们毕竟不是io操作。到了```uv_listen```这句,我们再次回到回调函数的风格上来。第二个参数是待处理的连接请求队列-最大长度的请求连接队列。
当客户端开始建立连接的时候,回调函数```on_new_connection```需要使用```uv_accept```去建立一个与客户端socket通信的句柄。同时我们也要开始从流中读取数据。
#### tcp-echo-server/main.c - Accepting the client
```c
void on_new_connection(uv_stream_t *server, int status) {
if (status < 0) {
fprintf(stderr, "New connection error %s\n", uv_strerror(status));
// error!
return;
}
uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if (uv_accept(server, (uv_stream_t*) client) == 0) {
uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
}
else {
uv_close((uv_handle_t*) client, NULL);
}
}
```
上述的函数集和stream的例子类似在code文件夹中可以找到更多的例子。记得在socket不需要后调用uv_close。如果你不需要接受连接你甚至可以在uv_listen的回调函数中调用uv_close。
#### client
当你在服务器端完成绑定/监听/接收的操作后,在客户端只要简单地调用```uv_tcp_connect```,它的回调函数和上面类似,具体例子如下:
```c
uv_tcp_t* socket = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);
uv_connect_t* connect = (uv_connect_t*)malloc(sizeof(uv_connect_t));
struct sockaddr_in dest;
uv_ip4_addr("127.0.0.1", 80, &dest);
uv_tcp_connect(connect, socket, dest, on_connect);
```
当建立连接后,回调函数```on_connect```会被调用。回调函数会接收到一个uv_connect_t结构的数据它的```handle```指向通信的socket。
## UDP
用户数据报协议(User Datagram Protocol)提供无连接的不可靠的网络通信。因此libuv不会提供一个stream实现的形式而是提供了一个```uv_udp_t```句柄(接收端),和一个```uv_udp_send_t```句柄发送端还有相关的函数。也就是说实际的读写api与正常的流读取类似。下面的例子展示了一个从DCHP服务器获取ip的例子。
##### note
>你必须以管理员的权限运行udp-dhcp因为它的端口号低于1024
#### udp-dhcp/main.c - Setup and send UDP packets
```c
uv_loop_t *loop;
uv_udp_t send_socket;
uv_udp_t recv_socket;
int main() {
loop = uv_default_loop();
uv_udp_init(loop, &recv_socket);
struct sockaddr_in recv_addr;
uv_ip4_addr("0.0.0.0", 68, &recv_addr);
uv_udp_bind(&recv_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR);
uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);
uv_udp_init(loop, &send_socket);
struct sockaddr_in broadcast_addr;
uv_ip4_addr("0.0.0.0", 0, &broadcast_addr);
uv_udp_bind(&send_socket, (const struct sockaddr *)&broadcast_addr, 0);
uv_udp_set_broadcast(&send_socket, 1);
uv_udp_send_t send_req;
uv_buf_t discover_msg = make_discover_msg();
struct sockaddr_in send_addr;
uv_ip4_addr("255.255.255.255", 67, &send_addr);
uv_udp_send(&send_req, &send_socket, &discover_msg, 1, (const struct sockaddr *)&send_addr, on_send);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
##### note
>ip地址为0.0.0.0用来绑定所有的接口。255.255.255.255是一个广播地址这也意味着数据报将往所有的子网接口中发送。端口号为0代表着由操作系统随机分配一个端口。
首先我们设置了一个用于接收socket绑定了全部网卡端口号为68作为DHCP客户端然后开始从中读取数据。它会接收所有来自DHCP服务器的返回数据。我们设置了```UV_UDP_REUSEADDR```标记,用来和其他共享端口的 DHCP客户端和平共处。接着我们设置了一个类似的发送socket然后使用```uv_udp_send```向DHCP服务器在67端口发送广播。
设置广播发送是非常必要的,否则你会接收到`EACCES`[错误](http://beej.us/guide/bgnet/output/html/multipage/advanced.html#broadcast)。和此前一样,如果在读写中出错,返回码<0。
因为UDP不会建立连接因此回调函数会接收到关于发送者的额外的信息。
当没有可读数据后nread等于0。如果`addr`是`null`它代表了没有可读数据回调函数不会做任何处理。如果不为null则说明了从addr中接收到一个空的数据报。如果flag为```UV_UDP_PARTIAL```,则代表了内存分配的空间不够存放接收到的数据了,在这种情形下,操作系统会丢弃存不下的数据。
#### udp-dhcp/main.c - Reading packets
```c
void on_read(uv_udp_t *req, ssize_t nread, const uv_buf_t *buf, const struct sockaddr *addr, unsigned flags) {
if (nread < 0) {
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*) req, NULL);
free(buf->base);
return;
}
char sender[17] = { 0 };
uv_ip4_name((const struct sockaddr_in*) addr, sender, 16);
fprintf(stderr, "Recv from %s\n", sender);
// ... DHCP specific code
unsigned int *as_integer = (unsigned int*)buf->base;
unsigned int ipbin = ntohl(as_integer[4]);
unsigned char ip[4] = {0};
int i;
for (i = 0; i < 4; i++)
ip[i] = (ipbin >> i*8) & 0xff;
fprintf(stderr, "Offered IP %d.%d.%d.%d\n", ip[3], ip[2], ip[1], ip[0]);
free(buf->base);
uv_udp_recv_stop(req);
}
```
#### UDP Options
生存时间Time-to-live
>可以通过`uv_udp_set_ttl`更改生存时间。
只允许IPV6协议栈
>在调用`uv_udp_bind`时,设置`UV_UDP_IPV6ONLY`标示可以强制只使用ipv6。
组播
>socket也支持组播可以这么使用
```c
UV_EXTERN int uv_udp_set_membership(uv_udp_t* handle,
const char* multicast_addr,
const char* interface_addr,
uv_membership membership);
```
其中`membership`可以为`UV_JOIN_GROUP`和`UV_LEAVE_GROUP`。
这里有一篇很好的关于组播的[文章](http://www.tldp.org/HOWTO/Multicast-HOWTO-2.html)。
可以使用`uv_udp_set_multicast_loop`修改本地的组播。
同样可以使用`uv_udp_set_multicast_ttl`修改组播数据报的生存时间。(设定生存时间可以防止数据报由于环路的原因,会出现无限循环的问题)。
## Querying DNS
libuv提供了一个异步的DNS解决方案。它提供了自己的`getaddrinfo`。在回调函数中你可以像使用正常的socket操作一样。让我们来看一下例子
#### dns/main.c
```c
int main() {
loop = uv_default_loop();
struct addrinfo hints;
hints.ai_family = PF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = 0;
uv_getaddrinfo_t resolver;
fprintf(stderr, "irc.freenode.net is... ");
int r = uv_getaddrinfo(loop, &resolver, on_resolved, "irc.freenode.net", "6667", &hints);
if (r) {
fprintf(stderr, "getaddrinfo call error %s\n", uv_err_name(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
如果`uv_getaddrinfo`返回非零值说明设置错误了因此也不会激发回调函数。在函数返回后所有的参数将会被回收和释放。主机地址请求服务器地址还有hints的结构都可以在[这里](http://nikhilm.github.io/uvbook/getaddrinfo)找到详细的说明。如果想使用同步请求可以将回调函数设置为NULL。
在回调函数on_resolved中你可以从`struct addrinfo(s)`链表中获取返回的IP最后需要调用`uv_freeaddrinfo`回收掉链表。下面的例子演示了回调函数的内容。
#### dns/main.c
```c
void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) {
if (status < 0) {
fprintf(stderr, "getaddrinfo callback error %s\n", uv_err_name(status));
return;
}
char addr[17] = {'\0'};
uv_ip4_name((struct sockaddr_in*) res->ai_addr, addr, 16);
fprintf(stderr, "%s\n", addr);
uv_connect_t *connect_req = (uv_connect_t*) malloc(sizeof(uv_connect_t));
uv_tcp_t *socket = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);
uv_tcp_connect(connect_req, socket, (const struct sockaddr*) res->ai_addr, on_connect);
uv_freeaddrinfo(res);
}
```
libuv同样提供了DNS逆解析的函数[uv_getnameinfo](http://docs.libuv.org/en/v1.x/dns.html#c.uv_getnameinfo])。
## Network interfaces
可以调用`uv_interface_addresses`获得系统的网络接口信息。下面这个简单的例子打印出所有可以获取的信息。这在服务器开始准备绑定IP地址的时候很有用。
#### interfaces/main.c
```c
#include <stdio.h>
#include <uv.h>
int main() {
char buf[512];
uv_interface_address_t *info;
int count, i;
uv_interface_addresses(&info, &count);
i = count;
printf("Number of interfaces: %d\n", count);
while (i--) {
uv_interface_address_t interface = info[i];
printf("Name: %s\n", interface.name);
printf("Internal? %s\n", interface.is_internal ? "Yes" : "No");
if (interface.address.address4.sin_family == AF_INET) {
uv_ip4_name(&interface.address.address4, buf, sizeof(buf));
printf("IPv4 address: %s\n", buf);
}
else if (interface.address.address4.sin_family == AF_INET6) {
uv_ip6_name(&interface.address.address6, buf, sizeof(buf));
printf("IPv6 address: %s\n", buf);
}
printf("\n");
}
uv_free_interface_addresses(info, count);
return 0;
}
```
`is_internal`可以用来表示是否是内部的IP。由于一个物理接口会有多个IP地址所以每一次while循环的时候都会打印一次。

View File

@@ -0,0 +1,551 @@
# Processes
libuv提供了相当多的子进程管理函数并且是跨平台的还允许使用stream或者说pipe完成进程间通信。
在UNIX中有一个共识就是进程只做一件事并把它做好。因此进程通常通过创建子进程来完成不同的任务例如在shell中使用pipe。 一个多进程的,通过消息通信的模型,总比多线程的,共享内存的模型要容易理解得多。
当前一个比较常见的反对事件驱动编程的原因在于其不能很好地利用现代多核计算机的优势。一个多线程的程序内核可以将线程调度到不同的cpu核心中执行以提高性能。但是一个event-loop的程序只有一个线程。实际上工作区可以被分配到多进程上每一个进程执行一个event-loop然后每一个进程被分配到不同的cpu核心中执行。
## Spawning child processes
一个最简单的用途是,你想要开始一个进程,然后知道它什么时候终止。需要使用`uv_spawn`完成任务:
#### spawn/main.c
```c
uv_loop_t *loop;
uv_process_t child_req;
uv_process_options_t options;
int main() {
loop = uv_default_loop();
char* args[3];
args[0] = "mkdir";
args[1] = "test-dir";
args[2] = NULL;
options.exit_cb = on_exit;
options.file = "mkdir";
options.args = args;
int r;
if ((r = uv_spawn(loop, &child_req, &options))) {
fprintf(stderr, "%s\n", uv_strerror(r));
return 1;
} else {
fprintf(stderr, "Launched process with ID %d\n", child_req.pid);
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
##### Note
>由于上述的options是全局变量因此被初始化为0。如果你在局部变量中定义options请记得将所有没用的域设为0
```c
uv_process_options_t options = {0};
```
`uv_process_t`只是作为句柄,所有的选择项都通过`uv_process_options_t`设置为了简单地开始一个进程你只需要设置file和argsfile是要执行的程序args是所需的参数和c语言中main函数的传入参数类似。因为`uv_spawn`在内部使用了[execvp](http://man7.org/linux/man-pages/man3/exec.3.html),所以不需要提供绝对地址。遵从惯例,**实际传入参数的数目要比需要的参数多一个因为最后一个参数会被设为NULL**。
在函数`uv_spawn`被调用之后,`uv_process_t.pid`会包含子进程的id。
回调函数`on_exit()`会在被调用的时候传入exit状态和导致exit的信号。
#### spawn/main.c
```c
void on_exit(uv_process_t *req, int64_t exit_status, int term_signal) {
fprintf(stderr, "Process exited with status %" PRId64 ", signal %d\n", exit_status, term_signal);
uv_close((uv_handle_t*) req, NULL);
```
在进程关闭后需要回收handler。
## Changing process parameters
在子进程开始执行前,你可以通过使用`uv_process_options_t`设置运行环境。
### Change execution directory
设置`uv_process_options_t.cwd`,更改相应的目录。
### Set environment variables
`uv_process_options_t.env`的格式是以null为结尾的字符串数组其中每一个字符串的形式都是`VAR=VALUE`。这些值用来设置进程的环境变量。如果子进程想要继承父进程的环境变量,就将`uv_process_options_t.env`设为null。
### Option flags
通过使用下面标识的按位或的值设置`uv_process_options_t.flags`的值,可以定义子进程的行为:
>* `UV_PROCESS_SETUID`-将子进程的执行用户idUID设置为`uv_process_options_t.uid`中的值。
* `UV_PROCESS_SETGID`-将子进程的执行组id(GID)设置为`uv_process_options_t.gid`中的值。
只有在unix系的操作系统中支持设置用户id和组id在windows下设置会失败`uv_spawn`会返回`UV_ENOTSUP`
* `UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS`-在windows上`uv_process_options_t.args`参数不要用引号包裹。此标记对unix无效。
* `UV_PROCESS_DETACHED`-在新会话(session)中启动子进程,这样子进程就可以在父进程退出后继续进行。请看下面的例子:
## Detaching processes
使用标识`UV_PROCESS_DETACHED`可以启动守护进程(daemon),或者是使得子进程从父进程中独立出来,这样父进程的退出就不会影响到它。
#### detach/main.c
```c
int main() {
loop = uv_default_loop();
char* args[3];
args[0] = "sleep";
args[1] = "100";
args[2] = NULL;
options.exit_cb = NULL;
options.file = "sleep";
options.args = args;
options.flags = UV_PROCESS_DETACHED;
int r;
if ((r = uv_spawn(loop, &child_req, &options))) {
fprintf(stderr, "%s\n", uv_strerror(r));
return 1;
}
fprintf(stderr, "Launched sleep with PID %d\n", child_req.pid);
uv_unref((uv_handle_t*) &child_req);
return uv_run(loop, UV_RUN_DEFAULT);
```
记住一点就是handle会始终监视着子进程所以你的程序不会退出。`uv_unref()`会解除handle。
## Sending signals to processes
libuv打包了unix标准的`kill(2)`系统调用并且在windows上实现了一个类似用法的调用但要注意所有的`SIGTERM``SIGINT``SIGKILL`都会导致进程的中断。`uv_kill`函数如下所示:
```c
uv_err_t uv_kill(int pid, int signum);
```
对于用libuv启动的进程应该使用`uv_process_kill`终止,它会以`uv_process_t`作为第一个参数而不是pid。当使用`uv_process_kill`后,记得使用`uv_close`关闭`uv_process_t`
## Signals
libuv对unix信号和一些[windows下类似的机制](http://docs.libuv.org/en/v1.x/signal.html#signal),做了很好的打包。
使用`uv_signal_init`初始化handle`uv_signal_t `然后将它与loop关联。为了使用handle监听特定的信号使用`uv_signal_start()`函数。每一个handle只能与一个信号关联后续的`uv_signal_start`会覆盖前面的关联。使用`uv_signal_stop`终止监听。下面的这个小例子展示了各种用法:
#### signal/main.c
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t* create_loop()
{
uv_loop_t *loop = malloc(sizeof(uv_loop_t));
if (loop) {
uv_loop_init(loop);
}
return loop;
}
void signal_handler(uv_signal_t *handle, int signum)
{
printf("Signal received: %d\n", signum);
uv_signal_stop(handle);
}
// two signal handlers in one loop
void thread1_worker(void *userp)
{
uv_loop_t *loop1 = create_loop();
uv_signal_t sig1a, sig1b;
uv_signal_init(loop1, &sig1a);
uv_signal_start(&sig1a, signal_handler, SIGUSR1);
uv_signal_init(loop1, &sig1b);
uv_signal_start(&sig1b, signal_handler, SIGUSR1);
uv_run(loop1, UV_RUN_DEFAULT);
}
// two signal handlers, each in its own loop
void thread2_worker(void *userp)
{
uv_loop_t *loop2 = create_loop();
uv_loop_t *loop3 = create_loop();
uv_signal_t sig2;
uv_signal_init(loop2, &sig2);
uv_signal_start(&sig2, signal_handler, SIGUSR1);
uv_signal_t sig3;
uv_signal_init(loop3, &sig3);
uv_signal_start(&sig3, signal_handler, SIGUSR1);
while (uv_run(loop2, UV_RUN_NOWAIT) || uv_run(loop3, UV_RUN_NOWAIT)) {
}
}
int main()
{
printf("PID %d\n", getpid());
uv_thread_t thread1, thread2;
uv_thread_create(&thread1, thread1_worker, 0);
uv_thread_create(&thread2, thread2_worker, 0);
uv_thread_join(&thread1);
uv_thread_join(&thread2);
return 0;
}
```
##### Note
>`uv_run(loop, UV_RUN_NOWAIT)``uv_run(loop, UV_RUN_ONCE)`非常像因为它们都只处理一个事件。但是不同在于UV_RUN_ONCE会在没有任务的时候阻塞但是UV_RUN_NOWAIT会立刻返回。我们使用`NOWAIT`这样才使得一个loop不会因为另外一个loop没有要处理的事件而挨饿。
当向进程发送`SIGUSR1`你会发现signal_handler函数被激发了4次每次都对应一个`uv_signal_t`。然后signal_handler调用uv_signal_stop终止了每一个`uv_signal_t`最终程序退出。对每个handler函数来说任务的分配很重要。一个使用了多个event-loop的服务器程序只要简单地给每一个进程添加信号SIGINT监视器就可以保证程序在中断退出前数据能够安全地保存。
## Child Process I/O
一个正常的新产生的进程都有自己的一套文件描述符映射表例如012分别对应`stdin``stdout``stderr`。有时候父进程想要将自己的文件描述符映射表分享给子进程。例如你的程序启动了一个子命令并且把所有的错误信息输出到log文件中但是不能使用`stdout`。因此,你想要使得你的子进程和父进程一样,拥有`stderr`。在这种情形下libuv提供了继承文件描述符的功能。在下面的例子中我们会调用这么一个测试程序
#### proc-streams/test.c
```c
#include <stdio.h>
int main()
{
fprintf(stderr, "This is stderr\n");
printf("This is stdout\n");
return 0;
}
```
实际的执行程序` proc-streams`在运行的时候,只向子进程分享`stderr`。使用`uv_process_options_t``stdio`域设置子进程的文件描述符。首先设置`stdio_count`,定义文件描述符的个数。`uv_process_options_t.stdio`是一个`uv_stdio_container_t`数组。定义如下:
```c
typedef struct uv_stdio_container_s {
uv_stdio_flags flags;
union {
uv_stream_t* stream;
int fd;
} data;
} uv_stdio_container_t;
```
上边的flag值可取多种。比如如果你不打算使用可以设置为`UV_IGNORE`。如果与stdio中对应的前三个文件描述符被标记为`UV_IGNORE`,那么它们会被重定向到`/dev/null`
因为我们想要传递一个已经存在的文件描述符,所以使用`UV_INHERIT_FD`。因此fd被设为stderr。
#### proc-streams/main.c
```c
int main() {
loop = uv_default_loop();
/* ... */
options.stdio_count = 3;
uv_stdio_container_t child_stdio[3];
child_stdio[0].flags = UV_IGNORE;
child_stdio[1].flags = UV_IGNORE;
child_stdio[2].flags = UV_INHERIT_FD;
child_stdio[2].data.fd = 2;
options.stdio = child_stdio;
options.exit_cb = on_exit;
options.file = args[0];
options.args = args;
int r;
if ((r = uv_spawn(loop, &child_req, &options))) {
fprintf(stderr, "%s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
这时你启动proc-streams也就是在main中产生一个执行test的子进程你只会看到“This is stderr”。你可以试着设置stdout也继承父进程。
同样可以把上述方法用于流的重定向。比如把flag设为`UV_INHERIT_STREAM`,然后再设置父进程中的`data.stream`这时子进程只会把这个stream当成是标准的I/O。这可以用来实现例如[CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface)。
一个简单的CGI脚本的例子如下
#### cgi/tick.c
```c
#include <stdio.h>
#include <unistd.h>
int main() {
int i;
for (i = 0; i < 10; i++) {
printf("tick\n");
fflush(stdout);
sleep(1);
}
printf("BOOM!\n");
return 0;
}
```
CGI服务器用到了这章和[网络](http://luohaha.github.io/Chinese-uvbook/source/networking.html)那章的知识所以每一个client都会被发送10个tick然后被中断连接。
#### cgi/main.c
```c
void on_new_connection(uv_stream_t *server, int status) {
if (status == -1) {
// error!
return;
}
uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if (uv_accept(server, (uv_stream_t*) client) == 0) {
invoke_cgi_script(client);
}
else {
uv_close((uv_handle_t*) client, NULL);
}
```
上述代码中我们接受了连接并把socket传递给`invoke_cgi_script`
#### cgi/main.c
```c
args[1] = NULL;
/* ... finding the executable path and setting up arguments ... */
options.stdio_count = 3;
uv_stdio_container_t child_stdio[3];
child_stdio[0].flags = UV_IGNORE;
child_stdio[1].flags = UV_INHERIT_STREAM;
child_stdio[1].data.stream = (uv_stream_t*) client;
child_stdio[2].flags = UV_IGNORE;
options.stdio = child_stdio;
options.exit_cb = cleanup_handles;
options.file = args[0];
options.args = args;
// Set this so we can close the socket after the child process exits.
child_req.data = (void*) client;
int r;
if ((r = uv_spawn(loop, &child_req, &options))) {
fprintf(stderr, "%s\n", uv_strerror(r));
```
cgi的`stdout`被绑定到socket上所以无论tick脚本程序打印什么都会发送到client端。通过使用进程我们能够很好地处理读写并发操作而且用起来也很方便。但是要记得这么做是很浪费资源的。
## Pipes
libuv的`uv_pipe_t`结构可能会让一些unix程序员产生困惑因为它像魔术般变幻出`|`和[`pipe(7)`](http://man7.org/linux/man-pages/man7/pipe.7.html)。但这里的`uv_pipe_t`并不是IPC机制里的 匿名管道在IPC里pipe是 匿名管道只允许父子进程之间通信。FIFO则允许没有亲戚关系的进程间通信显然llibuv里的`uv_pipe_t`不是第一种)。`uv_pipe_t`背后有[unix本地socket](http://man7.org/linux/man-pages/man7/unix.7.html)或者[windows 具名管道](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365590.aspx)的支持,可以实现多进程间的通信。下面会具体讨论。
#### Parent-child IPC
父进程与子进程可以通过单工或者双工管道通信,获得管道可以通过设置`uv_stdio_container_t.flags``UV_CREATE_PIPE``UV_READABLE_PIPE`或者`UV_WRITABLE_PIPE`的按位或的值。上述的读/写标记是对于子进程而言的。
#### Arbitrary process IPC
既然本地socket具有确定的名称而且是以文件系统上的位置来标示的例如unix中socket是文件的一种存在形式那么它就可以用来在不相关的进程间完成通信任务。被开源桌面环境使用的[`D-BUS`系统](http://www.freedesktop.org/wiki/Software/dbus/)也是使用了本地socket来作为事件通知的例如当消息来到或者检测到硬件的时候各种应用程序会被通知到。mysql服务器也运行着一个本地socket等待客户端的访问。
当使用本地socket的时候客户端服务器模型通常和之前类似。在完成初始化后发送和接受消息的方法和之前的tcp类似接下来我们同样适用echo服务器的例子来说明。
#### pipe-echo-server/main.c
```c
int main() {
loop = uv_default_loop();
uv_pipe_t server;
uv_pipe_init(loop, &server, 0);
signal(SIGINT, remove_sock);
int r;
if ((r = uv_pipe_bind(&server, "echo.sock"))) {
fprintf(stderr, "Bind error %s\n", uv_err_name(r));
return 1;
}
if ((r = uv_listen((uv_stream_t*) &server, 128, on_new_connection))) {
fprintf(stderr, "Listen error %s\n", uv_err_name(r));
return 2;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
我们把socket命名为echo.sock意味着它将会在本地文件夹中被创造。对于stream API来说本地socekt表现得和tcp的socket差不多。你可以使用[socat](http://www.dest-unreach.org/socat/)测试一下服务器:
```
$ socat - /path/to/socket
```
客户端如果想要和服务器端连接的话,应该使用:
```c
void uv_pipe_connect(uv_connect_t *req, uv_pipe_t *handle, const char *name, uv_connect_cb cb);
```
上述函数name应该为echo.sock。
#### Sending file descriptors over pipes
最酷的事情是本地socket可以传递文件描述符也就是说进程间可以交换文件描述符。这样就允许进程将它们的I/O传递给其他进程。它的应用场景包括负载均衡服务器分派工作进程等各种可以使得cpu使用最优化的应用。libuv当前只支持通过管道传输**TCP sockets或者其他的pipes**。
为了展示这个功能我们将来实现一个由循环中的工人进程处理client端请求的这么一个echo服务器程序。这个程序有一些复杂在教程中只截取了部分的片段为了更好地理解我推荐你去读下完整的[代码](https://github.com/nikhilm/uvbook/tree/master/code/multi-echo-server)。
工人进程很简单,文件描述符将从主进程传递给它。
#### multi-echo-server/worker.c
```c
uv_loop_t *loop;
uv_pipe_t queue;
int main() {
loop = uv_default_loop();
uv_pipe_init(loop, &queue, 1 /* ipc */);
uv_pipe_open(&queue, 0);
uv_read_start((uv_stream_t*)&queue, alloc_buffer, on_new_connection);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
`queue`是另一端连接上主进程的管道,因此,文件描述符可以传送过来。在`uv_pipe_init`中将`ipc`参数设置为1很关键因为它标明了这个管道将被用来做进程间通信。因为主进程需要把文件handle赋给了工人进程作为标准输入因此我们使用`uv_pipe_open`把stdin作为pipe别忘了0代表stdin
#### multi-echo-server/worker.c
```c
void on_new_connection(uv_stream_t *q, ssize_t nread, const uv_buf_t *buf) {
if (nread < 0) {
if (nread != UV_EOF)
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*) q, NULL);
return;
}
uv_pipe_t *pipe = (uv_pipe_t*) q;
if (!uv_pipe_pending_count(pipe)) {
fprintf(stderr, "No pending count\n");
return;
}
uv_handle_type pending = uv_pipe_pending_type(pipe);
assert(pending == UV_TCP);
uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if (uv_accept(q, (uv_stream_t*) client) == 0) {
uv_os_fd_t fd;
uv_fileno((const uv_handle_t*) client, &fd);
fprintf(stderr, "Worker %d: Accepted fd %d\n", getpid(), fd);
uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
}
else {
uv_close((uv_handle_t*) client, NULL);
}
}
```
首先,我们调用`uv_pipe_pending_count`来确定从handle中可以读取出数据。如果你的程序能够处理不同类型的handle这时`uv_pipe_pending_type`就可以用来决定当前的类型。虽然在这里使用`accept`看起来很怪,但实际上是讲得通的。`accept`最常见的用途是从其他的文件描述符监听的socket获取文件描述符client端。这从原理上说和我们现在要做的是一样的从queue中获取文件描述符client。接下来worker可以执行标准的echo服务器的工作了。
我们再来看看主进程观察如何启动worker来达到负载均衡。
#### multi-echo-server/main.c
```c
struct child_worker {
uv_process_t req;
uv_process_options_t options;
uv_pipe_t pipe;
} *workers;
```
`child_worker`结构包裹着进程,和连接主进程和各个独立进程的管道。
#### multi-echo-server/main.c
```c
void setup_workers() {
round_robin_counter = 0;
// ...
// launch same number of workers as number of CPUs
uv_cpu_info_t *info;
int cpu_count;
uv_cpu_info(&info, &cpu_count);
uv_free_cpu_info(info, cpu_count);
child_worker_count = cpu_count;
workers = calloc(sizeof(struct child_worker), cpu_count);
while (cpu_count--) {
struct child_worker *worker = &workers[cpu_count];
uv_pipe_init(loop, &worker->pipe, 1);
uv_stdio_container_t child_stdio[3];
child_stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE;
child_stdio[0].data.stream = (uv_stream_t*) &worker->pipe;
child_stdio[1].flags = UV_IGNORE;
child_stdio[2].flags = UV_INHERIT_FD;
child_stdio[2].data.fd = 2;
worker->options.stdio = child_stdio;
worker->options.stdio_count = 3;
worker->options.exit_cb = close_process_handle;
worker->options.file = args[0];
worker->options.args = args;
uv_spawn(loop, &worker->req, &worker->options);
fprintf(stderr, "Started worker %d\n", worker->req.pid);
}
}
```
首先,我们使用酷炫的`uv_cpu_info`函数获取到当前的cpu的核心个数所以我们也能启动一样数目的worker进程。再次强调一下务必将`uv_pipe_init`的ipc参数设置为1。接下来我们指定子进程的`stdin`是一个可读的管道从子进程的角度来说。接下来的一切就很直观了worker进程被启动等待着文件描述符被写入到他们的标准输入中。
在主进程的`on_new_connection`我们接收了client端的socket然后把它传递给worker环中的下一个可用的worker进程。
#### multi-echo-server/main.c
```c
void on_new_connection(uv_stream_t *server, int status) {
if (status == -1) {
// error!
return;
}
uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if (uv_accept(server, (uv_stream_t*) client) == 0) {
uv_write_t *write_req = (uv_write_t*) malloc(sizeof(uv_write_t));
dummy_buf = uv_buf_init("a", 1);
struct child_worker *worker = &workers[round_robin_counter];
uv_write2(write_req, (uv_stream_t*) &worker->pipe, &dummy_buf, 1, (uv_stream_t*) client, NULL);
round_robin_counter = (round_robin_counter + 1) % child_worker_count;
}
else {
uv_close((uv_handle_t*) client, NULL);
}
}
```
`uv_write2`能够在所有的情形上做了一个很好的抽象我们只需要将client作为一个参数即可完成传输。现在我们的多进程echo服务器已经可以运转起来啦。
感谢Kyle指出了`uv_write2`需要一个不为空的buffer。

View File

@@ -0,0 +1,401 @@
# Threads
等一下为什么我们要聊线程事件循环event loop不应该是用来做web编程的方法吗(如果你对event loop, 不是很了解,可以看[这里](http://www.ruanyifeng.com/blog/2014/10/event-loop.html))。哦,不不。线程依旧是处理器完成任务的重要手段。线程因此有可能会派上用场,虽然会使得你不得不艰难地应对各种原始的同步问题。
线程会在内部使用用来在执行系统调用时伪造异步的假象。libuv通过线程还可以使得程序异步地执行一个阻塞的任务。方法就是大量地生成新线程然后收集线程执行返回的结果。
当下有两个占主导地位的线程库windows下的线程实现和POSIX的[pthread](http://man7.org/linux/man-pages/man7/pthreads.7.html)。libuv的线程API与pthread的API在使用方法和语义上很接近。
值得注意的是libuv的线程模块是自成一体的。比如其他的功能模块都需要依赖于event loop和回调的原则但是线程并不是这样。它们是不受约束的会在需要的时候阻塞通过返回值产生信号错误还有像接下来的这个例子所演示的这样不需要在event loop中执行。
因为线程API在不同的系统平台上句法和语义表现得都不太相似在支持程度上也各不相同。考虑到libuv的跨平台特性libuv支持的线程API个数很有限。
最后要强调一句只有一个主线程主线程上只有一个event loop。不会有其他与主线程交互的线程了。除非使用`uv_async_send`)。
## Core thread operations
下面这个例子不会很复杂,你可以使用`uv_thread_create()`开始一个线程,再使用`uv_thread_join()`等待其结束。
#### thread-create/main.c
```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
```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](#inter_thread_communication-pane)。
## Synchronization Primitives
因为本教程重点不在线程所以我只罗列了libuv API中一些神奇的地方。剩下的你可以自行阅读pthreads的手册。
#### Mutexes
libuv上的互斥量函数与pthread上存在一一映射。如果对pthread上的mutex不是很了解可以看[这里](https://computing.llnl.gov/tutorials/pthreads/)。
#### libuv mutex functions
```c
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_init`与`uv_mutex_trylock`在成功执行后返回0或者在错误时返回错误码。
如果libuv在编译的时候开启了调试模式`uv_mutex_destroy()`, `uv_mutex_lock()` 和 `uv_mutex_unlock()`会在出错的地方调用`abort()`中断。类似的,`uv_mutex_trylock()`也同样会在错误发生时中断,而不是返回`EAGAIN`和`EBUSY`。
递归地调用互斥量函数在某些系统平台上是支持的但是你不能太过度依赖。因为例如在BSD上递归地调用互斥量函数会返回错误比如你准备使用互斥量函数给一个已经上锁的临界区再次上锁的时候就会出错。比如像下面这个例子
```c
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()`, 在调试模式下会导致程序崩溃,或者是返回错误。
##### NOTE
>在linux中是支持递归上锁的但是在libuv的API中并未实现。
#### Lock
读写锁是更细粒度的实现机制。两个读者线程可以同时从共享区中读取数据。当读者以读模式占有读写锁时,写者不能再占有它。当写者以写模式占有这个锁时,其他的写者或者读者都不能占有它。读写锁在数据库操作中非常常见,下面是一个玩具式的例子:
#### locks/main.c - simple rwlocks
```c
#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;
}
```
试着来执行一下上面的程序,看读者有多少次会同步执行。在有多个写者的时候,调度器会给予他们高优先级。因此,如果你加入两个读者,你会看到所有的读者趋向于在读者得到加锁机会前结束。
在上面的例子中,我们也使用了屏障。因此主线程来等待所有的线程都已经结束,最后再将屏障和锁一块回收。
#### Others
libuv同样支持[信号量](https://en.wikipedia.org/wiki/Semaphore_programming)[条件变量](https://en.wikipedia.org/wiki/Monitor_synchronization#Waiting_and_signaling)和[屏障](https://en.wikipedia.org/wiki/Barrier_computer_science)而且API的使用方法和pthread中的用法很类似。如果你对上面的三个名词还不是很熟可以看[这里](http://www.wuzesheng.com/?p=1668)[这里](http://name5566.com/4535.html)[这里](http://www.cnblogs.com/panhao/p/4653623.html))。
还有libuv提供了一个简单易用的函数`uv_once()`。多个线程调用这个函数参数可以使用一个uv_once_t和一个指向特定函数的指针**最终只有一个线程能够执行这个特定函数,并且这个特定函数只会被调用一次**
```c
/* 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](http://baike.baidu.com/view/598128.htm)的[API](http://docs.libuv.org/en/v1.x/threading.html#thread-local-storage)使用方法同样和pthread类似。
##libuv work queue
`uv_queue_work()`是一个便利的函数它使得一个应用程序能够在不同的线程运行任务当任务完成后回调函数将会被触发。它看起来好像很简单但是它真正吸引人的地方在于它能够使得任何第三方的库都能以event-loop的方式执行。当使用event-loop的时候最重要的是不能让loop线程阻塞或者是执行高cpu占用的程序因为这样会使得loop慢下来loop event的高效特性也不能得到很好地发挥。
然而,很多带有阻塞的特性的程序(比如最常见的I/O使用开辟新线程来响应新请求(最经典的‘一个客户,一个线程‘模型)。使用event-loop可以提供另一种实现的方式。libuv提供了一个很好的抽象使得你能够很好地使用它。
下面有一个很好的例子,灵感来自<<[nodejs is cancer](http://teddziuba.github.io/2011/10/node-js-is-cancer.html)>>。我们将要执行fibonacci数列并且睡眠一段时间但是将阻塞和cpu占用时间长的任务分配到不同的线程使得其不会阻塞event loop上的其他任务。
#### queue-work/main.c - lazy fibonacci
```c
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
```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](http://nikhilm.github.io/uvbook/utilities.html#baton)来交换数据。
从libuv 0.9.4版后,添加了函数`uv_cancel()`。它可以用来取消工作队列中的任务。只有还未开始的任务可以被取消,如果任务已经开始执行或者已经执行完毕,`uv_cancel()`调用会失败。
当用户想要终止程序的时候,`uv_cancel()`可以用来清理任务队列中的等待执行的任务。例如,一个音乐播放器可以以歌手的名字对歌曲进行排序,如果这个时候用户想要退出这个程序,`uv_cancel()`就可以做到快速退出,而不用等待执行完任务队列后,再退出。
让我们对上述程序做一些修改,用来演示`uv_cancel()`的用法。首先让我们注册一个处理中断的函数。
#### queue-cancel/main.c
```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
```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
```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_t`和`uv_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
```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
```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
```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
```c
void after(uv_work_t *req, int status) {
fprintf(stderr, "Download complete\n");
uv_close((uv_handle_t*) &async, NULL);
}
```
在例子的最后,我们要说下`data`域的滥用,[bnoordhuis](https://github.com/bnoordhuis)指出使用`data`域可能会存在线程安全问题,`uv_async_send()`事实上只是唤醒了event-loop。可以使用互斥量或者读写锁来保证执行顺序的正确性。
##### Note
>互斥量和读写锁不能在信号处理函数中正确工作,但是`uv_async_send`可以。
一种需要使用`uv_async_send`的场景是当调用需要线程交互的库时。例如举一个在node.js中V8引擎的例子上下文和对象都是与v8引擎的线程绑定的从另一个线程中直接向v8请求数据会导致返回不确定的结果。但是考虑到现在很多nodejs的模块都是和第三方库绑定的可以像下面一样解决这个问题
>1.在node中第三方库会建立javascript的回调函数以便回调函数被调用时能够返回更多的信息。
```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的功能以便其完成任务

View File

@@ -0,0 +1,558 @@
# Utilities
本章介绍的工具和技术对于常见的任务非常的实用。libuv吸收了[libev用户手册页](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#COMMON_OR_USEFUL_IDIOMS_OR_BOTH)中所涵盖的一些模式并在此基础上对API做了少许的改动。本章还包含了一些无需用完整的一章来介绍的libuv API。
## Timers
在定时器启动后的特定时间后定时器会调用回调函数。libuv的定时器还可以设定为按时间间隔定时启动而不是只启动一次。
可以简单地使用超时时间`timeout`作为参数初始化一个定时器,还有一个可选参数`repeat`。定时器能在任何时间被终止。
```c
uv_timer_t timer_req;
uv_timer_init(loop, &timer_req);
uv_timer_start(&timer_req, callback, 5000, 2000);
```
上述操作会启动一个循环定时器repeating timer它会在调用`uv_timer_start`5秒timeout启动回调函数然后每隔2秒repeat循环启动回调函数。你可以使用
```c
uv_timer_stop(&timer_req);
```
来停止定时器。这个函数也可以在回调函数中安全地使用。
循环的间隔也可以随时定义,使用:
```c
uv_timer_set_repeat(uv_timer_t *timer, int64_t repeat);
```
它会在**可能的时候**发挥作用。如果上述函数是在定时器回调函数中调用的,这意味着:
>* 如果定时器未设置为循环,这意味着定时器已经停止。需要先用`uv_timer_start`重新启动。
* 如果定时器被设置为循环,那么下一次超时的时间已经被规划好了,所以在切换到新的间隔之前,旧的间隔还会发挥一次作用。
函数:
```c
int uv_timer_again(uv_timer_t *)
```
**只适用于循环定时器**,相当于停止定时器,然后把原先的`timeout``repeat`值都设置为之前的`repeat`值,启动定时器。如果当该函数调用时,定时器未启动,则调用失败(错误码为`UV_EINVAL`并且返回1。
下面的一节会出现使用定时器的例子。
## Event loop reference count
event-loop在没有了活跃的handle之后便会终止。整套系统的工作方式是在handle增加时event-loop的引用计数加1在handle停止时引用计数减少1。当然libuv也允许手动地更改引用计数通过使用
```c
void uv_ref(uv_handle_t*);
void uv_unref(uv_handle_t*);
```
这样就可以达到允许loop即使在有正在活动的定时器时仍然能够推出。或者是使用自定义的uv_handle_t对象来使得loop保持工作。
第二个函数可以和间隔循环定时器结合使用。你会有一个每隔x秒执行一次的垃圾回收器或者是你的网络服务器会每隔一段时间向其他人发送一次心跳信号但是你不想只有在所有垃圾回收完或者出现错误时才能停止他们。如果你想要在你其他的监视器都退出后终止程序。这时你就可以立即unref定时器即便定时器这时是loop上唯一还在运行的监视器你依旧可以停止`uv_run()`
它们同样会出现在node.js中如js的API中封装的libuv方法。每一个js的对象产生一个`uv_handle_t`所有监视器的超类同样可以被uv_ref和uv_unref。
#### ref-timer/main.c
```c
uv_loop_t *loop;
uv_timer_t gc_req;
uv_timer_t fake_job_req;
int main() {
loop = uv_default_loop();
uv_timer_init(loop, &gc_req);
uv_unref((uv_handle_t*) &gc_req);
uv_timer_start(&gc_req, gc, 0, 2000);
// could actually be a TCP download or something
uv_timer_init(loop, &fake_job_req);
uv_timer_start(&fake_job_req, fake_job, 9000, 0);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
首先初始化垃圾回收器的定时器,然后在立刻`unref`它。注意观察9秒之后此时fake_job完成程序会自动退出即使垃圾回收器还在运行。
## Idler pattern
空转的回调函数会在每一次的event-loop循环激发一次。空转的回调函数可以用来执行一些优先级较低的活动。比如你可以向开发者发送应用程序的每日性能表现情况以便于分析或者是使用用户应用cpu时间来做[SETI](http://www.seti.org)运算:)。空转程序还可以用于GUI应用。比如你在使用event-loop来下载文件如果tcp连接未中断而且当前并没有其他的事件则你的event-loop会阻塞这也就意味着你的下载进度条会停滞用户会面对一个无响应的程序。面对这种情况空转监视器可以保持UI可操作。
#### idle-compute/main.c
```c
uv_loop_t *loop;
uv_fs_t stdin_watcher;
uv_idle_t idler;
char buffer[1024];
int main() {
loop = uv_default_loop();
uv_idle_init(loop, &idler);
uv_buf_t buf = uv_buf_init(buffer, 1024);
uv_fs_read(loop, &stdin_watcher, 0, &buf, 1, -1, on_type);
uv_idle_start(&idler, crunch_away);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
上述程序中,我们将空转监视器和我们真正关心的事件排在一起。`crunch_away`会被循环地调用,直到输入字符并回车。然后程序会被中断很短的时间,用来处理数据读取,然后在接着调用空转的回调函数。
#### idle-compute/main.c
```c
void crunch_away(uv_idle_t* handle) {
// Compute extra-terrestrial life
// fold proteins
// computer another digit of PI
// or similar
fprintf(stderr, "Computing PI...\n");
// just to avoid overwhelming your terminal emulator
uv_idle_stop(handle);
}
```
## Passing data to worker thread
在使用`uv_queue_work`的时候你通常需要给工作线程传递复杂的数据。解决方案是自定义struct然后使用`uv_work_t.data`指向它。一个稍微的不同是必须让`uv_work_t`作为这个自定义struct的成员之一把这叫做接力棒。这么做就可以使得同时回收数据和`uv_wortk_t`
```c
struct ftp_baton {
uv_work_t req;
char *host;
int port;
char *username;
char *password;
}
```
```c
ftp_baton *baton = (ftp_baton*) malloc(sizeof(ftp_baton));
baton->req.data = (void*) baton;
baton->host = strdup("my.webhost.com");
baton->port = 21;
// ...
uv_queue_work(loop, &baton->req, ftp_session, ftp_cleanup);
```
现在我们创建完了接力棒,并把它排入了队列中。
现在就可以随性所欲地获取自己想要的数据啦。
```c
void ftp_session(uv_work_t *req) {
ftp_baton *baton = (ftp_baton*) req->data;
fprintf(stderr, "Connecting to %s\n", baton->host);
}
void ftp_cleanup(uv_work_t *req) {
ftp_baton *baton = (ftp_baton*) req->data;
free(baton->host);
// ...
free(baton);
}
```
我们既回收了接力棒,同时也回收了监视器。
## External I/O with polling
通常在使用第三方库的时候需要应对他们自己的IO还有保持监视他们的socket和内部文件。在此情形下不可能使用标准的IO流操作但第三方库仍然能整合进event-loop中。所有这些需要的就是第三方库就必须允许你访问它的底层文件描述符并且提供可以处理有用户定义的细微任务的函数。但是一些第三库并不允许你这么做他们只提供了一个标准的阻塞IO函数此函数会完成所有的工作并返回。在event-loop的线程直接使用它们是不明智的而是应该使用libuv的工作线程。当然这也意味着失去了对第三方库的颗粒化控制。
libuv的`uv_poll`简单地监视了使用了操作系统的监控机制的文件描述符。从某方面说libuv实现的所有的IO操作的背后均有`uv_poll`的支持。无论操作系统何时监视到文件描述符的改变libuv都会调用响应的回调函数。
现在我们简单地实现一个下载管理程序,它会通过[libcurl](http://curl.haxx.se/libcurl/)来下载文件。我们不会直接控制libcurl而是使用libuv的event-loop通过非阻塞的异步的[多重接口](http://curl.haxx.se/libcurl/c/libcurl-multi.html)来处理下载与此同时libuv会监控IO的就绪状态。
#### uvwget/main.c - The setup
```c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
#include <curl/curl.h>
uv_loop_t *loop;
CURLM *curl_handle;
uv_timer_t timeout;
}
int main(int argc, char **argv) {
loop = uv_default_loop();
if (argc <= 1)
return 0;
if (curl_global_init(CURL_GLOBAL_ALL)) {
fprintf(stderr, "Could not init cURL\n");
return 1;
}
uv_timer_init(loop, &timeout);
curl_handle = curl_multi_init();
curl_multi_setopt(curl_handle, CURLMOPT_SOCKETFUNCTION, handle_socket);
curl_multi_setopt(curl_handle, CURLMOPT_TIMERFUNCTION, start_timeout);
while (argc-- > 1) {
add_download(argv[argc], argc);
}
uv_run(loop, UV_RUN_DEFAULT);
curl_multi_cleanup(curl_handle);
return 0;
}
```
每种库整合进libuv的方式都是不同的。以libcurl的例子来说我们注册了两个回调函数。socket回调函数`handle_socket`会在socket状态改变的时候被触发因此我们不得不开始轮询它。`start_timeout`是libcurl用来告知我们下一次的超时间隔的之后我们就应该不管当前IO状态驱动libcurl向前。这些也就是libcurl能处理错误或驱动下载进度向前的原因。
可以这么调用下载器:
```
$ ./uvwget [url1] [url2] ...
```
我们可以把url当成参数传入程序。
#### uvwget/main.c - Adding urls
```c
void add_download(const char *url, int num) {
char filename[50];
sprintf(filename, "%d.download", num);
FILE *file;
file = fopen(filename, "w");
if (file == NULL) {
fprintf(stderr, "Error opening %s\n", filename);
return;
}
CURL *handle = curl_easy_init();
curl_easy_setopt(handle, CURLOPT_WRITEDATA, file);
curl_easy_setopt(handle, CURLOPT_URL, url);
curl_multi_add_handle(curl_handle, handle);
fprintf(stderr, "Added download %s -> %s\n", url, filename);
}
```
我们允许libcurl直接向文件写入数据。
`start_timeout`会被libcurl立即调用。它会启动一个libuv的定时器使用`CURL_SOCKET_TIMEOUT`驱动`curl_multi_socket_action`,当其超时时,调用它。`curl_multi_socket_action`会驱动libcurl也会在socket状态改变的时候被调用。但在我们深入讲解它之前我们需要轮询监听socket等待`handle_socket`被调用。
#### uvwget/main.c - Setting up polling
```c
void start_timeout(CURLM *multi, long timeout_ms, void *userp) {
if (timeout_ms <= 0)
timeout_ms = 1; /* 0 means directly call socket_action, but we'll do it in a bit */
uv_timer_start(&timeout, on_timeout, timeout_ms, 0);
}
int handle_socket(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp) {
curl_context_t *curl_context;
if (action == CURL_POLL_IN || action == CURL_POLL_OUT) {
if (socketp) {
curl_context = (curl_context_t*) socketp;
}
else {
curl_context = create_curl_context(s);
curl_multi_assign(curl_handle, s, (void *) curl_context);
}
}
switch (action) {
case CURL_POLL_IN:
uv_poll_start(&curl_context->poll_handle, UV_READABLE, curl_perform);
break;
case CURL_POLL_OUT:
uv_poll_start(&curl_context->poll_handle, UV_WRITABLE, curl_perform);
break;
case CURL_POLL_REMOVE:
if (socketp) {
uv_poll_stop(&((curl_context_t*)socketp)->poll_handle);
destroy_curl_context((curl_context_t*) socketp);
curl_multi_assign(curl_handle, s, NULL);
}
break;
default:
abort();
}
return 0;
}
```
我们关心的是socket的文件描述符s还有action。对应每一个socket我们都创造了`uv_poll_t`,并用`curl_multi_assign`把它们关联起来。每当回调函数被调用时,`socketp`都会指向它。
在下载完成或失败后libcurl需要移除poll。所以我们停止并回收了poll的handle。
我们使用`UV_READABLE``UV_WRITABLE`开始轮询基于libcurl想要监视的事件。当socket已经准备好读或写后libuv会调用轮询的回调函数。在相同的handle上调用多次`uv_poll_start`是被允许的,这么做可以更新事件的参数。`curl_perform`是整个程序的关键。
#### uvwget/main.c - Driving libcurl.
```c
void curl_perform(uv_poll_t *req, int status, int events) {
uv_timer_stop(&timeout);
int running_handles;
int flags = 0;
if (status < 0) flags = CURL_CSELECT_ERR;
if (!status && events & UV_READABLE) flags |= CURL_CSELECT_IN;
if (!status && events & UV_WRITABLE) flags |= CURL_CSELECT_OUT;
curl_context_t *context;
context = (curl_context_t*)req;
curl_multi_socket_action(curl_handle, context->sockfd, flags, &running_handles);
check_multi_info();
}
```
首先我们要做的是停止定时器因为内部还有其他要做的事。接下来我们我们依据触发回调函数的事件来设置flag。然后我们使用上述socket和flag作为参数来调用`curl_multi_socket_action`。在此刻libcurl会在内部完成所有的工作然后尽快地返回事件驱动程序在主线程中急需的数据。libcurl会在自己的队列中将传输进度的消息排队。对于我们来说我们只关心是否传输完成这类消息。所以我们将这类消息提取出来并将传输完成的handle回收。
#### uvwget/main.c - Reading transfer status.
```c
void check_multi_info(void) {
char *done_url;
CURLMsg *message;
int pending;
while ((message = curl_multi_info_read(curl_handle, &pending))) {
switch (message->msg) {
case CURLMSG_DONE:
curl_easy_getinfo(message->easy_handle, CURLINFO_EFFECTIVE_URL,
&done_url);
printf("%s DONE\n", done_url);
curl_multi_remove_handle(curl_handle, message->easy_handle);
curl_easy_cleanup(message->easy_handle);
break;
default:
fprintf(stderr, "CURLMSG default\n");
abort();
}
}
}
```
## Loading libraries
libuv提供了一个跨平台的API来加载[共享库shared libraries](http://liaoph.com/linux-shared-libary/)。这就可以用来实现你自己的插件扩展模块系统它们可以被nodejs通过`require()`调用。只要你的库输出的是正确的符号,用起来还是很简单的。在载入第三方库的时候,要注意错误和安全检查,否则你的程序就会表现出不可预测的行为。下面这个例子实现了一个简单的插件,它只是打印出了自己的名字。
首先看下提供给插件作者的接口。
#### plugin/plugin.h
```c
#ifndef UVBOOK_PLUGIN_SYSTEM
#define UVBOOK_PLUGIN_SYSTEM
// Plugin authors should use this to register their plugins with mfp.
void mfp_register(const char *name);
#endif
```
你可以在你的程序中给插件添加更多有用的功能mfp is My Fancy Plugin。使用了这个api的插件的例子
#### plugin/hello.c
```c
#include "plugin.h"
void initialize() {
mfp_register("Hello World!");
}
```
我们的接口定义了,所有的插件都应该有一个能被程序调用的`initialize`函数。这个插件被编译成了共享库,因此可以被我们的程序在运行的时候载入。
```
$ ./plugin libhello.dylib
Loading libhello.dylib
Registered plugin "Hello World!"
```
##### Note
>共享库的后缀名在不同平台上是不一样的。在Linux上是libhello.so。
使用`uv_dlopen`首先载入了共享库`libhello.dylib`。再使用`uv_dlsym`获取了该插件的`initialize`函数,最后在调用它。
#### plugin/main.c
```c
#include "plugin.h"
typedef void (*init_plugin_function)();
void mfp_register(const char *name) {
fprintf(stderr, "Registered plugin \"%s\"\n", name);
}
int main(int argc, char **argv) {
if (argc == 1) {
fprintf(stderr, "Usage: %s [plugin1] [plugin2] ...\n", argv[0]);
return 0;
}
uv_lib_t *lib = (uv_lib_t*) malloc(sizeof(uv_lib_t));
while (--argc) {
fprintf(stderr, "Loading %s\n", argv[argc]);
if (uv_dlopen(argv[argc], lib)) {
fprintf(stderr, "Error: %s\n", uv_dlerror(lib));
continue;
}
init_plugin_function init_plugin;
if (uv_dlsym(lib, "initialize", (void **) &init_plugin)) {
fprintf(stderr, "dlsym error: %s\n", uv_dlerror(lib));
continue;
}
init_plugin();
}
return 0;
}
```
函数`uv_dlopen`需要传入一个共享库的路径作为参数。当它成功时返回0出错时返回1。使用`uv_dlerror`可以获取出错的消息。
`uv_dlsym`的第三个参数保存了一个指向第二个参数所保存的函数的指针。`init_plugin_function`是一个函数的指针,它指向了我们所需要的程序插件的函数。
## TTY
文字终端长期支持非常标准化的[控制序列](https://en.wikipedia.org/wiki/ANSI_escape_code)。它经常被用来增强终端输出的可读性。例如`grep --colour`。libuv提供了跨平台的`uv_tty_t`抽象stream和相关的处理ANSI escape codes 的函数。这也就是说libuv同样在Windows上实现了对等的ANSI codes并且提供了获取终端信息的函数。
首先要做的是,使用读/写文件描述符来初始化`uv_tty_t`。如下:
```c
int uv_tty_init(uv_loop_t*, uv_tty_t*, uv_file fd, int readable)
```
设置`readable`为true意味着你打算使用`uv_read_start`从stream从中读取数据。
最好还要使用`uv_tty_set_mode`来设置其为正常模式。也就是运行大多数的TTY格式流控制和其他的设置。其他的模式还有[这些](http://docs.libuv.org/en/v1.x/tty.html#c.uv_tty_mode_t)。
记得当你的程序退出后,要使用`uv_tty_reset_mode`恢复终端的状态。这才是礼貌的做法。另外要注意礼貌的地方是关心重定向。如果使用者将你的命令的输出重定向到文件控制序列不应该被重写因为这会阻碍可读性和grep。为了保证文件描述符确实是TTY可以使用`uv_guess_handle`函数,比较返回值是否为`UV_TTY`
下面是一个把白字打印到红色背景上的例子。
#### tty/main.c
```c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_tty_t tty;
int main() {
loop = uv_default_loop();
uv_tty_init(loop, &tty, 1, 0);
uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL);
if (uv_guess_handle(1) == UV_TTY) {
uv_write_t req;
uv_buf_t buf;
buf.base = "\033[41;37m";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
}
uv_write_t req;
uv_buf_t buf;
buf.base = "Hello TTY\n";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
uv_tty_reset_mode();
return uv_run(loop, UV_RUN_DEFAULT);
}
```
最后要说的是`uv_tty_get_winsize()`它能获取到终端的宽和长当成功获取后返回0。下面这个小程序实现了一个动画的效果。
#### tty-gravity/main.c
```c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_tty_t tty;
uv_timer_t tick;
uv_write_t write_req;
int width, height;
int pos = 0;
char *message = " Hello TTY ";
void update(uv_timer_t *req) {
char data[500];
uv_buf_t buf;
buf.base = data;
buf.len = sprintf(data, "\033[2J\033[H\033[%dB\033[%luC\033[42;37m%s",
pos,
(unsigned long) (width-strlen(message))/2,
message);
uv_write(&write_req, (uv_stream_t*) &tty, &buf, 1, NULL);
pos++;
if (pos > height) {
uv_tty_reset_mode();
uv_timer_stop(&tick);
}
}
int main() {
loop = uv_default_loop();
uv_tty_init(loop, &tty, 1, 0);
uv_tty_set_mode(&tty, 0);
if (uv_tty_get_winsize(&tty, &width, &height)) {
fprintf(stderr, "Could not get TTY information\n");
uv_tty_reset_mode();
return 1;
}
fprintf(stderr, "Width %d, height %d\n", width, height);
uv_timer_init(loop, &tick);
uv_timer_start(&tick, update, 200, 200);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
escape codes的对应表如下
代码 | 意义
------------ | -------------
2 J | Clear part of the screen, 2 is entire screen
H | Moves cursor to certain position, default top-left
n B | Moves cursor down by n lines
n C | Moves cursor right by n columns
m | Obeys string of display settings, in this case green background (40+2), white text (30+7)
正如你所见,它能输出酷炫的效果,你甚至可以发挥想象,用它来制作电子游戏。更有趣的输出,可以使用`http://www.gnu.org/software/ncurses/ncurses.html`

0
C++/web开发/beast.md Normal file
View File

View File

@@ -1,20 +1,320 @@
# java并发控制
## 锁
> 参考文献
> * [并发编程](https://www.cnblogs.com/flashsun/p/10776168.html)
> * [java高并发编程](https://blog.csdn.net/cx105200/article/details/80220937)
synchronized 关键字
可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。可能锁对象包括: this 临界资源对象Class 类对象
## 信号量
同步方法
同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时, 需同步执行。
public synchronized void test(){
System.out.println("测试一下");
}
1
2
3
同步代码块
同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。
锁定临界对象
同步代码块在执行时,是锁定 object 对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。
public void test(){
synchronized(o){
System.out.println("测试一下");
}
}
1
2
3
4
5
锁定当前对象
## 条件变量
public void test(){
synchronized(this){
System.out.println("测试一下");
}
}
1
2
3
4
5
锁的底层实现
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步方法 并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。
对象内存简图
对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志类型指针指向对象的类元数据JVM 通过这个指针确定该对象是哪个类的实例等信息。
实例变量:存放类的属性数据信息,包括父类的属性信息
填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁) 的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
在 Java 虚拟机(HotSpot)中monitor 是由 ObjectMonitor 实现的。
ObjectMonitor 中有两个队列_WaitSet 和 _EntryList以及_Owner 标记。其中_WaitSet 是用于管理等待队列(wait)线程的_EntryList 是用于管理锁池阻塞线程的_Owner 标记用于记录当前执行线程。线程状态图如下:
这里写图片描述
## ThreadLocal
当多线程并发访问同一个同步代码时首先会进入_EntryList当线程获取锁标记后
monitor 中的_Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1代表锁定其他线程在_EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行赋值为 0 计算并将_Owner 标记赋值为 null代表放弃锁执行线程进如_WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法_WaitSet 中的线程被唤醒进入_EntryList 中阻塞等待获取锁标记。若执行线程的同步代码执行结束同样会释放锁标记monitor 中的_Owner 标记赋值为 null且计数器赋值为 0 计算。
锁的种类
Java 中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。
锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。
锁只能升级,不能降级。
重量级锁
在锁的底层实现中解释的就是重量级锁。
偏向锁
是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用锁标记的形式记录锁状态。在 Monitor 中有变量 ACC_SYNCHRONIZED。当变量值使用的时候 代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。
轻量级锁
过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记 ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。
两个线程也可能出现重量级锁。
自旋锁
是一个过渡锁,是偏向锁和轻量级锁的过渡。
当获取锁的过程中未获取到。为了提高效率JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。
volatile 关键字
变量的线程可见性。在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其他线程修改,可能造成数据不一致的情况,从而导致结果错误。而 volatile 修饰的变量是线程可见的,当 JVM 解释 volatile 修饰的变量时,会通知 CPU在计算过程中 每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓存中的数据,可以保证计算结果的正确。
volatile 只是通知底层计算时CPU 检查内存数据,而不是让一个变量在多个线程中同步。
volatile int count = 0;
1
wait&notify
AtomicXxx 类型组
原子类型。
在 concurrent.atomic 包中定义了若干原子类型,这些类型中的每个方法都是保证了原子操作的。多线程并发访问原子类型对象中的方法,不会出现数据错误。在多线程开发中,如果某数据需要多个线程同时操作,且要求计算原子性,可以考虑使用原子类型对象。
AtomicInteger count = new AtomicInteger(0);
void m(){
count.incrementAndGet();
}
1
2
3
4
注意:原子类型中的方法是保证了原子操作,但多个方法之间是没有原子性的。如:
AtomicInteger i = new AtomicInteger(0);
if(i.get() != 5){
i.incrementAndGet();
}
1
2
3
4
在上述代码中get 方法和 incrementAndGet 方法都是原子操作,但复合使用时,无法保证原子性,仍旧可能出现数据错误。
CountDownLatch 门闩
门闩是 concurrent 包中定义的一个类型,是用于多线程通讯的一个辅助类型。
门闩相当于在一个门上加多个锁,当线程调用 await 方法时,会检查门闩数量,如果门
闩数量大于 0线程会阻塞等待。当线程调用 countDown 时,会递减门闩的数量,当门闩数量为 0 时await 阻塞线程可执行。
CountDownLatch latch = new CountDownLatch(5);
void m1(){
try {
latch.await();// 等待门闩开放。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1() method");
}
void m2(){
for(int i = 0; i < 10; i++){
if(latch.getCount() != 0){
System.out.println("latch count : " + latch.getCount());
latch.countDown(); // 减门闩上的锁。
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("m2() method : " + i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
锁的重入
在 Java 中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对同一个对象加锁时才可重入。
当线程持有锁时,会在 monitor 的计数器中执行递增计算若当前线程调用其他同步代码且同步代码的锁对象相同时monitor 中的计数器继续递增。每个同步代码执行结束,
monitor 中的计数器都会递减直至所有同步代码执行结束monitor 中的计数器为 0 时释放锁标记_Owner 标记赋值为 null。
ReentrantLock
重入锁,建议应用的同步方式。相对效率比 synchronized 高。量级较轻。
synchronized 在 JDK1.5 版本开始,尝试优化。到 JDK1.7 版本后,优化效率已经非常好了。在绝对效率上,不比 reentrantLock 差多少。
使用重入锁,必须必须必须手工释放锁标记。一般都是在 finally 代码块中定义释放锁标记的 unlock 方法。
公平锁
这里写图片描述
private static ReentrantLock lock = new ReentrantLock(true);
public void run(){
for(int i = 0; i < 5; i++){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " get lock");
}finally{
lock.unlock();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
8ThreadLocal
remove 问题
这里写图片描述
同步容器
解决并发情况下的容器线程安全问题的。给多线程环境准备一个线程安全的容器对象。线程安全的容器对象: Vector, Hashtable。线程安全容器对象都是使用 synchronized
方法实现的。
concurrent 包中的同步容器,大多数是使用系统底层技术实现的线程安全。类似 native。
Java8 中使用 CAS。
Map/Set
ConcurrentHashMap/ConcurrentHashSet
底层哈希实现的同步 Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。量级较 synchronized 低。key 和 value 不能为 null。
ConcurrentSkipListMap/ConcurrentSkipListSet
底层跳表SkipList实现的同步 Map(Set)。有序,效率比 ConcurrentHashMap 稍低。
这里写图片描述
List
CopyOnWriteArrayList
写时复制集合。写入效率低,读取效率高。每次写入数据,都会创建一个新的底层数组。
Queue
ConcurrentLinkedQueue
基础链表同步队列。
LinkedBlockingQueue
阻塞队列,队列容量不足自动阻塞,队列容量为 0 自动阻塞。
ArrayBlockingQueue
底层数组实现的有界队列。自动阻塞。根据调用 APIadd/put/offer不同有不同特性。
当容量不足的时候,有阻塞能力。
add 方法在容量不足的时候,抛出异常。
put 方法在容量不足的时候,阻塞等待。
offer 方法,
单参数 offer 方法,不阻塞。容量不足的时候,返回 false。当前新增数据操作放弃。三参数 offer 方法offer(value,times,timeunit)),容量不足的时候,阻塞 times 时长(单
位为 timeunit如果在阻塞时长内有容量空闲新增数据返回 true。如果阻塞时长范围
内,无容量空闲,放弃新增数据,返回 false。
DelayQueue
延时队列。根据比较机制,实现自定义处理顺序的队列。常用于定时任务。如:定时关机。
LinkedTransferQueue
转移队列,使用 transfer 方法,实现数据的即时处理。没有消费者,就阻塞。
SynchronusQueue
同步队列,是一个容量为 0 的队列。是一个特殊的 TransferQueue。必须现有消费线程等待才能使用的队列。
add 方法,无阻塞。若没有消费线程阻塞等待数据,则抛出异常。
put 方法,有阻塞。若没有消费线程阻塞等待数据,则阻塞。
ThreadPool&Executor
Executor
线程池顶级接口。
常用方法 - void execute(Runnable)
作用是: 启动线程任务的。
ExecutorService
Executor 接口的子接口。
常见方法 - Future submit(Callable) Future submit(Runnable)
Future
未来结果,代表线程任务执行结束后的结果。
Callable
可执行接口。
接口方法 Object call();相当于 Runnable 接口中的 run 方法。区别为此方法有返回值。不能抛出已检查异常。
和 Runnable 接口的选择 - 需要返回值或需要抛出异常时,使用 Callable其他情况可任意选择。
Executors
工具类型。为 Executor 线程池提供工具方法。类似 ArraysCollections 等工具类型的功用。
FixedThreadPool
容量固定的线程池
queued tasks - 任务队列
completed tasks - 结束任务队列
CachedThreadPool
缓存的线程池。容量不限Integer.MAX_VALUE。自动扩容。默认线程空闲 60 秒,自动销毁。
ScheduledThreadPool
计划任务线程池。可以根据计划自动执行任务的线程池。
SingleThreadExceutor
单一容量的线程池。
ForkJoinPool
分支合并线程池mapduce 类似的设计思想)。适合用于处理复杂任务。初始化线程容量与 CPU 核心数相关。
线程池中运行的内容必须是 ForkJoinTask 的子类型RecursiveTask,RecursiveAction
WorkStealingPool
JDK1.8 新增的线程池。工作窃取线程池。当线程池中有空闲连接时,自动到等待队列中窃取未完成任务,自动执行。
初始化线程容量与 CPU 核心数相关。此线程池中维护的是精灵线程。
ExecutorService.newWorkStealingPool();
ThreadPoolExecutor
线程池底层实现。除 ForkJoinPool 外,其他常用线程池底层都是使用 ThreadPoolExecutor
实现的。
public ThreadPoolExecutor
(int corePoolSize, // 核心容量
int maximumPoolSize, // 最大容量
long keepAliveTime, // 生命周期0 为永久
TimeUnit unit, // 生命周期单位
BlockingQueue workQueue // 任务队列,阻塞队列。
);

View File

View File

@@ -0,0 +1,13 @@
## 多进程
## 多线程
## 多协程
## 异步IO/事件驱动IO/IO多路复用

View File

View File

@@ -4,20 +4,21 @@
* [x] 并发通信
* [x] C++并发机制
* [ ] asio、libuv、muduo异步IO
* [ ] Beast web开发
* [ ] Function回调机
* [ ] C++并发控制
* [x] asio、libuv、muduo异步IO
* [x] Beast web开发
* [x] C++并发控
* [x] java并发机制
* [ ] java并发控制
* [x] java并发控制
* [ ] Python并发机制
* [ ] Python并发控制
* [ ] Linux并发机制总结
* [ ] Linux并发控制总结
* [x] Python并发机制
* [x] Python并发控制
* [x] Linux并发机制总结
* [x] Linux并发控制总结
* [x] Windows并发机制总结
* [x] Windows并发控制总结
* [x] 网络编程机制、设备IO机制总结
* [ ] java网络编程
* [ ] C++网络编程
* [ ] Python网络编程
* [x] java网络编程
* [x] C++网络编程
* [x] Python网络编程