Opening Multiple Console Windows One For Each Thread

I'm playing around with threads and rather than designing a class to manage output to a single console i.e., a single stdout, I figured to use this [1] approach to have each thread write to its own console.

Each process has only 1 stdin and stdout, and since threads within a single process share the same stdin and stdout, you'd need to redirect stdin and stdout to point to the console the thread owns each time that thread wants to output to stdout or read from stdin. Even if that was possible, Window's process management API only allow you specify stdin and stdout handles when you create a process, not after the console process has already been created (which means it wouldn't be possible to redirect a thread's stdin and stdout).

The technique of using multiple console windows seem unfeasible because all threads share a single stdin and stdout. But are there other ways to implement this technique?

What about using namedpipes? Each thread has its own namepipe that it writes to. Each namepipe is tied to a different Windows Console. Instead of writing to stdin, each thread would send send bytes/data to its namedpipe. Would this work?

[1] https://cplusplus.com/forum/lounge/17371/
I'm playing around with threads and rather than designing a class to manage output to a single console i.e., a single stdout

If you're using the standard library, see std::osyncstream.
The standard streams (stdin, stdout and stderr) are per process, not per thread. Hence all threads of a process shared the same stdin, stdout and stderr. On Windows, the stdout and stderr streams of a "console" process are, by default, initialized as wrappers/aliases for the $CONOUT handle, whereas the stdin of a "console" process is, by default, initialized as a wrapper/alias for the $CONIN handle.

Furthermore, Windows allocates a new terminal window to a "console" process when it is started. It is actually possible for a "console" process to inherit its parent's terminal window, so multiple processes may be attached to the same terminal window (which makes sense). But I'm not aware of any way how a single process could be attached to more than one terminal window. If this was possible, then how would the process write to its multiple terminals separately, when it just has a single stdout stream (i.e. $CONOUT handle) available?

I think one possible way would be to start a number of separate processes, each of which has its own separate terminal window, and that act as "consumers" for the single "producer" (main) process. When you spawn the "consumer" processes from the "producer" (main) process, you have to use the CREATE_NEW_CONSOLE flag, so that they will not inherit the paren's terminal window. The communication between "producer" (main) process and its "consumer" processes could indeed be realized via a number of named pipes – using one named pipe for each sub-process – so that the output can be sent to each "consumer" process (terminal window) separately, as needed.

Another option would be to create a GUI application and "emulate" the terminal window(s) with your own GUI code. This way you could easily create as many "output" windows as you like, in the same process. The communication between those "output" windows (that probably all live in the "main" GUI thread) and the multiple "worker" threads could then be implemented via window messages, I suppose.

Yet another (more simple, but also more limited) option would be to use OutputDebugString() in combination with DebugView:
https://learn.microsoft.com/de-de/sysinternals/downloads/debugview
Last edited on
In fact, we don't need named pipes! 😊

Here is a full example that is using only anonymous pipes:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <process.h>
#include <Windows.h>

// ============================================================
// Child Process (Consumer)
// ============================================================

#define BUFFSIZE 1024U

static int child_process(void)
{
	puts("Hello from the \"child\" process!\n");

	char buffer[BUFFSIZE];
	for (;;) {
		if (fgets(buffer, BUFFSIZE, stdin) != NULL) {
			printf("> %s", buffer);
		}
		else {
			return EXIT_SUCCESS;
		}
	}
}

// ============================================================
// Main Process (Producer)
// ============================================================

#define THREAD_COUNT 5U

static unsigned _stdcall thread_function(void *data)
{
	FILE *const output = (FILE*)data;
	const DWORD id = GetCurrentThreadId();

	Sleep(3000U);

	DWORD counter = 0U;
	for (size_t i = 0U; i < 32U; ++i) {
		if (i > 0U) {
			Sleep(1000U);
		}
		fprintf(output, "Message #%u from worker thread 0x%X is here!\n", counter++, id);
		fflush(output);
	}

	return 0U;
}

static FILE *wrap_handle(const HANDLE handle, const BOOL write)
{
	const int fdesc = _open_osfhandle((intptr_t)handle, write ? _O_WRONLY : _O_RDONLY);
	if (fdesc >= 0) {
		FILE *const stream = _fdopen(fdesc, write ? "w" : "r");
		if (!stream) {
			_close(fdesc);
		}
		return stream;
	}

	return NULL;
}

// ---------------------------------------------------
// Main function
// ---------------------------------------------------

static int main_process(void)
{
	puts("Hello from the \"main\" process!\n");

	// ---------------------------------------------------
	// Build the command-line
	// ---------------------------------------------------

	wchar_t filename[BUFFSIZE];
	DWORD result = GetModuleFileNameW(NULL, filename, BUFFSIZE);
	if (!((result > 0U) && (result < BUFFSIZE))) {
		fputs("Failed to get executable file path!\n", stderr);
		return EXIT_FAILURE;
	}

	wchar_t commandline[BUFFSIZE];
	if (_snwprintf_s(commandline, BUFFSIZE, _TRUNCATE, L"\"%s\" --child", filename) < 0) {
		fputs("Failed to build the command-line!\n", stderr);
		return EXIT_FAILURE;
	}

	// ---------------------------------------------------
	// Create child processes
	// ---------------------------------------------------

	HANDLE child_process[THREAD_COUNT];
	FILE *console[THREAD_COUNT]; /* the "write" handles of the pipe(s), already wrapped as FILE stream(s) */

	SecureZeroMemory(child_process, sizeof(child_process));
	SecureZeroMemory(console, sizeof(console));

	puts("Creating child processes, please wait...");

	for (size_t i = 0U; i < THREAD_COUNT; ++i) {
		HANDLE hPipeRead = INVALID_HANDLE_VALUE, hPipeWrite = INVALID_HANDLE_VALUE;
		if (!CreatePipe(&hPipeRead, &hPipeWrite, NULL, 0U)) {
			fputs("Failed to create new pipe for child-process!\n", stderr);
			return EXIT_FAILURE;
		}

		if (!SetHandleInformation(hPipeRead, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) {
			fputs("Failed to update handle information!\n", stderr);
			return EXIT_FAILURE;
		}

		if (!(console[i] = wrap_handle(hPipeWrite, TRUE))) {
			fputs("Failed to wrap the console \"write\" handle!!\n", stderr);
		}

		STARTUPINFOW startup;
		SecureZeroMemory(&startup, sizeof(STARTUPINFOW));
		startup.cb = sizeof(STARTUPINFOW);
		startup.dwFlags = STARTF_USESTDHANDLES;
		startup.hStdInput = hPipeRead;

		PROCESS_INFORMATION info;
		SecureZeroMemory(&info, sizeof(PROCESS_INFORMATION));

		if (!CreateProcessW(NULL, commandline, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &startup, &info)) {
			fputs("Failed to create child process!\n", stderr);
			return EXIT_FAILURE;
		}

		child_process[i] = info.hProcess;
		CloseHandle(hPipeRead);
		CloseHandle(info.hThread);
	}

	// ---------------------------------------------------
	// Create worker threads
	// ---------------------------------------------------

	puts("Done.\n\nCreating worker threads, please wait...");

	HANDLE threads[THREAD_COUNT];
	for (size_t i = 0U; i < THREAD_COUNT; ++i) {
		if (!(threads[i] = (HANDLE)_beginthreadex(NULL, 0U, thread_function, console[i], 0U, NULL))) {
			fputs("Failed to create child process!\n", stderr);
			return EXIT_FAILURE;
		}
	}

	// ---------------------------------------------------
	// Wait for completion
	// ---------------------------------------------------

	puts("Done.\n\nWaiting for worker threads to finish, please wait...");

	WaitForMultipleObjects(THREAD_COUNT, threads, TRUE, INFINITE);

	for (size_t i = 0U; i < THREAD_COUNT; ++i) {
		CloseHandle(threads[i]);
	}

	// ---------------------------------------------------
	// Final clean-up
	// ---------------------------------------------------

	puts("Done.\n\nPress any key...");
	getchar();

	for (size_t i = 0U; i < THREAD_COUNT; ++i) {
		fclose(console[i]);
	}

	puts("\nWaiting for child processes to exit, please wait...");

	WaitForMultipleObjects(THREAD_COUNT, child_process, TRUE, INFINITE);

	for (size_t i = 0U; i < THREAD_COUNT; ++i) {
		CloseHandle(child_process);
	}

	puts("Done.\n");
	return EXIT_SUCCESS;
}

// ============================================================
// Entry point
// ============================================================

int wmain(int argc, wchar_t *argv[])
{
	if (argc > 1) {
		if (_wcsicmp(argv[1], L"--child") == 0) {
			return child_process();
		}
		else {
			fputs("Error: Bad arguments detected !!!", stderr);
			return EXIT_FAILURE;
		}
	}

	return main_process();
}
Last edited on
mbozzi wrote:
std::osyncstream

Unfortunately, I only have access to C++11/14, not C++20. I will eventually use this technique at work, where I don't have the luxury of just using the latest language features.

kigar64551 wrote:
But I'm not aware of any way how a single process could be attached to more than one terminal window. If this was possible, then how would the process write to its multiple terminals separately, when it just has a single stdout stream (i.e. $CONOUT handle) available?

I just realized that the named pipe approach wouldn't work. Sure you can have each thread own a different named pipe, but all threads would still need to tie the same stdin and stdout to each of their named pipe ends - which is not what we want.

Is it possible to define additional input and output streams (one for each thread) and have each thread connect their respective streams to the pipe ends? Is it possible to define a handle to the thread's output stream, and pass that handle as the hStdInput to the newly created Windows terminal console process?

I'll try this after work and see if I get anywhere.

kigar64551 wrote:
I think one possible way would be to start a number of separate processes, each of which has its own separate terminal window, and that act as "consumers" for the single "producer" (main) process ...The communication between "producer" (main) process and its "consumer" processes could indeed be realized via a number of named pipes – using one named pipe for each sub-process – so that the output can be sent to each "consumer" process (terminal window) separately, as needed.

Interesting technique but unfortunately, I'm looking for a thread-based solution. Multiple processes = multiple stdin/stdout and that would easily solve the problem.
Last edited on
Interesting technique but unfortunately, I'm looking for a thread-based solution. Multiple processes = multiple stdin/stdout and that would easily solve the problem.

You can still have multiple threads in your "main" process to do the actual work 😎

You just use the additional "child" (consumer) processes as helper processes to display the output from the threads.

https://i.imgur.com/bx1XUM9.png

(See example code above for more details!)

Is it possible to define additional input and output streams (one for each thread) and have each thread connect their respective streams to the pipe ends? Is it possible to define a handle to the thread's output stream, and pass that handle as the hStdInput to the newly created Windows terminal console process?

Sure, you can create as many pipes as you like, only limited by the system's resources. The "read" and/or "write" handles of those pipes can then be passed to your threads as desired. For example, you can create one pipe per thread and then pass the pipe's "write" handle to the respective thread. The thread would then write to its own "write" handle (i.e. the "write side" of the thread's own pipe), instead of writing to the process' global stdout. And that is precisely what I do in the above example!

But: You still have to decide who will read (consume) the data, which was written by the threads, from all those pipes 😏

No matter what you do, only a single handle can be set up as the stdin handle when creating a new process. That is the reason why, in my above example, I create one sub-process (consumer process) for each thread/pipe for consuming (displaying) the data.

Of course, there are many other possibilities how the data from the "per-thread" pipes could be consumed. It is not a necessity to create sub-processes for that task! For example, you could start a dedicated "consumer" thread that collects the outputs of your other threads.

(The reason I used separate sub-processes is that each sub-process can have its own terminal window for dispalying stuff)

I just realized that the named pipe approach wouldn't work. Sure you can have each thread own a different named pipe, but all threads would still need to tie the same stdin and stdout to each of their named pipe ends

Why would they "need" to?
Last edited on
Thanks, Kigar. I ran your code and did see those thread messages appearing in 5 different Console Windows. Not sure if it was only my system but MSVC compiler emitted compiler error on Line 33:

static int _stdcall thread_function(void *data)

Something to do with the thread_function needing to return an unsigned int. Changing it to the following fixes it:

static unsigned int _stdcall thread_function(void *data)

I'm diving into your code sample now. Lots of interesting things to see.
Not sure if it was only my system but MSVC compiler emitted compiler error on Line 33

Should be fixed now.

(I was compiling with MSVC in pure "C" mode, which is a bit more forgiving in such things than "C++" mode)
Last edited on
Registered users can post here. Sign in or register to post.