Console. log is used to print the required information to the standard output, which is like 1+1=2. But one day I saw a question on the Internet: is console.log asynchronous or synchronous? I’m surprised. Is that still a question? Synchronization, of course. However, I was surprised by the answer to the question, which told me that it should be divided into different situations. According to the situation of process.stdout, asynchronous situations may occur. I scowled and realized that the problem was not as simple as I thought, so I found this prompt in the Node documentation:

Writes may be synchronous depending on what the stream is connected to and whether the system is Windows or POSIX:
	Files: synchronous on Windows and POSIX
	TTYs (Terminals): asynchronous on Windows, synchronous on POSIX
	Pipes (and sockets): synchronous on Windows, asynchronous on POSIX
Copy the code

When I found that I had a blind spot in this knowledge, I quickly went into the kernel to see what was going on. I chose TTYs on POSIX, the most commonly used one, to get a deeper understanding.

Set out from the console. The log

First I found the console.log code in the Node source file from lib/console.js:

Console.prototype.log = function log(... args) { write(this._ignoreErrors, this._stdout, util.format.apply(null, args), this._stdoutErrorHandler, this[kGroupIndent]); };Copy the code

There are some formatting strings in the middle, but the core is clearly in the write function

stream.write(string, errorhandler);
Copy the code

And stream is this._stdout, from the code:

module.exports = new Console(process.stdout, process.stderr);
module.exports.Console = Console;
Copy the code

This._stout is process.stdout, so whether console.log is synchronous or asynchronous really depends.

The realization of the process. The stdout

Now we turned to the process. Stdout, definition of the attribute in the lib/internal/process/stdiso js, through the analysis of the file, we can find that the stream stdout is defined like this:

const tty_wrap = process.binding('tty_wrap');
switch (tty_wrap.guessHandleType(fd)) {
	case 'TTY':
		var tty = require('tty');
		stream = new tty.WriteStream(fd);
		stream._type = 'tty';
	break;

	case 'FILE':
		var fs = require('internal/fs');
		stream = new fs.SyncWriteStream(fd, { autoClose: false });
		stream._type = 'fs';
	break;

	case 'PIPE':
	case 'TCP':
		var net = require('net');
		stream = new net.Socket({
			fd: fd,
			readable: false,
			writable: true
		});
		stream._type = 'pipe';
	break;

	default:
	// Probably an error on in uv_guess_handle()
	throw new errors.Error('ERR_UNKNOWN_STREAM_TYPE');
}

// For supporting legacy API we put the FD here.
stream.fd = fd;

stream._isStdio = true;
Copy the code

The file state is synchronous using fs.syncWritestream, and PIPE is asynchronous using NET.socket on Posix machines. What puzzles me most is the implementation of TTY, where TTY.WriteStream is implemented in lib/ TTY. Js like this:

function WriteStream(fd) { ... net.Socket.call(this, { handle: new TTY(fd, false), readable: false, writable: true }); this._handle.setBlocking(true); . } inherits(WriteStream, net.Socket);Copy the code

As you can see, a stream on TTY inherits from net.socket, and even a new constructor calls its constructor directly. Handle is a TTY object. Did you just say that net.sockets should not be asynchronous? Why is it asynchronous to come here? At this point, I was a little puzzled and wondered how it made STdout synchronous in TTY mode. Net. Socket is a standard Node stream object. It directly inherits stream.Duplex. Write: socket.prototype. _writeGeneric: socket.prototype. _writeGeneric: socket.prototype. _writeGeneric In this function, different stream methods are called depending on the character type:

switch (encoding) {
	case 'latin1':
	case 'binary':
		return handle.writeLatin1String(req, data);

	case 'buffer':
		return handle.writeBuffer(req, data);

	case 'utf8':
	case 'utf-8':
		return handle.writeUtf8String(req, data);

	case 'ascii':
		return handle.writeAsciiString(req, data);

	case 'ucs2':
	case 'ucs-2':
	case 'utf16le':
	case 'utf-16le':
		return handle.writeUcs2String(req, data);

	default:
		return handle.writeBuffer(req, Buffer.from(data, encoding));
}	
Copy the code

In this example, handle is an instance of TTY, and TTY is obtained by process.bingding, so these methods are NODE_BUILTIN_MODULE methods. These methods all call template functions in SRC /stream_base.cc

template <enum encoding enc>
int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args)
Copy the code

As you can see from this function, the encoding differs in the way stack_storage values are generated, but they all pass

err = DoWrite(
    req_wrap,
    &buf,
    1,
    reinterpret_cast<uv_stream_t*>(send_handle));
Copy the code

DoWrite is a pure virtual function. The actual definition of this function is defined in TTYWrap’s base class, a streamBase derived class, in the file SRC /stream_wrap.cc. The libuv method uv_write is used to perform the actual write operations of IO. Then I again turning to file deps/uv/SRC/Unix/stream. The c in this method, which invokes the uv_write2, this is a very classic in libuv asynchronous method, but also has a special case of which the write operation on the actual function uv__write we can see, If the current stream has the UV_STREAM_BLOCKING flag set, it will always be written synchronously and no asynchronous operations will occur. So where did our TTY set this mark? We can go back to this sentence in lib/tty.js:

net.Socket.call(this, {
	handle: new TTY(fd, false),
	readable: false,
	writable: true
});
Copy the code

SRC /tty_wrap.cc void TTYWrap::New(const FunctionCallbackInfo

& args); void TTYWrap::New(const FunctionCallbackInfo

& args)

TTYWrap* wrap = new TTYWrap(env, args.This(), fd, args[1]->IsTrue(), &err);
Copy the code

Generates an instance of TTYWrap, and the constructor of the TTYWrap object passes:

uv_tty_init(env->event_loop(), &handle_, fd, readable);
Copy the code

Initialize the libuv stream Handle on tty. As you can see from the uv_tty_init code, the UV_STREAM_BLOCKING flag will be set to handle when the readable parameter is false. The readable parameter is passed in as the second parameter of new TTY(fd, False), which happens to be false, so process.stdout is naturally synchronized.

conclusion

I always thought I was familiar with Node. When I found that it was a stream operation of Net.socket, I was a little confused, but I also thought that handle might be an instance of TTY and that the write operation would be different. However, I explored the source code step by step and finally found that it was still through libuv uv_write2. UV_STREAM_BLOCKING was ignored, and finally compared uv_tty_init with other stream initializations. UV_STREAM_BLOCKING was set on tty, and the write operation that set the flag was synchronous. Through this incident, I still understand that many things can not be taken for granted. Only by exploring and understanding more can I accumulate more technology. I hope this article can also help you.