Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/WolfireGames/overgrowth.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Compat/processpool.cpp')
-rw-r--r--Source/Compat/processpool.cpp725
1 files changed, 725 insertions, 0 deletions
diff --git a/Source/Compat/processpool.cpp b/Source/Compat/processpool.cpp
new file mode 100644
index 00000000..c18ee8d5
--- /dev/null
+++ b/Source/Compat/processpool.cpp
@@ -0,0 +1,725 @@
+//-----------------------------------------------------------------------------
+// Name: processpool.cpp
+// Developer: Wolfire Games LLC
+// Description:
+// License: Read below
+//-----------------------------------------------------------------------------
+//
+// Copyright 2022 Wolfire Games LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//-----------------------------------------------------------------------------
+#include "processpool.h"
+
+#include "Logging/logdata.h"
+
+#include <iostream>
+#include <sstream>
+#include <cstring>
+#include <cstdlib>
+#include <vector>
+#include <queue>
+
+#ifndef _WIN32
+#include <pthread.h>
+#endif
+
+using std::string;
+using std::wstring;
+using std::endl;
+using std::cout;
+using std::vector;
+using std::queue;
+using std::swap;
+
+namespace {
+const char *kWorkerProcessString = "ProcessPool::IAmAWorkerProcess";
+const char *kChildInitString = "ACK_INIT: Child process reporting for duty";
+}
+
+namespace {
+#ifdef _WIN32
+ void ErrorExit(const string &msg){
+ MessageBox(NULL, msg.c_str(), TEXT("Error"), MB_OK);
+ ExitProcess(1);
+ }
+#else
+ void ErrorExit(const string &msg){
+ LOGE << msg << endl;
+ exit(1);
+ }
+#endif
+}
+
+void ProcessPool::Resize( int _size ) {
+ int old_size = (int)processes_.size();
+ for(int i=_size; i<old_size; ++i){
+ delete processes_[i];
+ }
+ processes_.resize(_size, NULL);
+ for(int i=old_size; i<_size; ++i){
+ processes_[i] = new ProcessHandle(worker_path_, this);
+ }
+}
+
+int ProcessPool::GetIdleProcessIndex() {
+ int num_processes = (int)processes_.size();
+ for(int i=0; i<num_processes; ++i){
+ if(processes_[i]->idle()){
+ return i;
+ }
+ }
+ return -1;
+}
+
+ProcessPool::Error ProcessPool::ProcessFirstTaskInQueue() {
+ if(tasks_.empty()){
+ int num_processes = (int)processes_.size();
+ int num_idle = 0;
+ for(int i=0; i<num_processes; ++i){
+ if(processes_[i]->idle()){
+ ++num_idle;
+ }
+ }
+ if(num_idle == num_processes){
+ #ifdef _WIN32
+ SetEvent(idle_event_);
+ #else
+ if(pthread_mutex_lock(&idle_event_mutex_)){
+ ErrorExit("Error locking mutex");
+ }
+ idle_event_bool_ = true;
+ if(pthread_cond_signal(&idle_event_cond_)){
+ ErrorExit("Error signalling condition");
+ }
+ if(pthread_mutex_unlock(&idle_event_mutex_)){
+ ErrorExit("Error unlocking mutex");
+ }
+ #endif
+ }
+ return ProcessPool::NO_TASK_IN_QUEUE;
+ } else {
+#ifdef _WIN32
+ ResetEvent(idle_event_);
+#else
+ if(pthread_mutex_lock(&idle_event_mutex_)){
+ ErrorExit("Error locking mutex");
+ }
+ idle_event_bool_ = false;
+ if(pthread_mutex_unlock(&idle_event_mutex_)){
+ ErrorExit("Error unlocking mutex");
+ }
+#endif
+ }
+ int idle_process = GetIdleProcessIndex();
+ if(idle_process == -1){
+ return ProcessPool::NO_IDLE_PROCESS;
+ }
+ processes_[idle_process]->Process(tasks_.front());
+ tasks_.pop();
+ return ProcessPool::SUCCESS;
+}
+
+bool ProcessPool::AmIAWorkerProcess( int argc, char* argv[] ) {
+ return (argc >= 2 && strcmp(argv[1],kWorkerProcessString) == 0);
+}
+
+ProcessPool::ProcessPool( const string &worker_path, int size ):
+ worker_path_(worker_path)
+{
+ #ifdef _WIN32
+ mutex_ = CreateMutex(NULL, false, NULL);
+ idle_event_ = CreateEvent(NULL, true, true, NULL);
+ #else
+ pthread_mutex_init(&mutex_, NULL);
+ pthread_mutex_init(&idle_event_mutex_, NULL);
+ pthread_cond_init(&idle_event_cond_, NULL);
+ idle_event_bool_ = true;
+ #endif
+ Resize(size);
+}
+
+ProcessPool::~ProcessPool() {
+ Resize(0);
+ #ifdef _WIN32
+ CloseHandle(mutex_);
+ CloseHandle(idle_event_);
+ #else
+ pthread_mutex_destroy(&mutex_);
+ pthread_mutex_destroy(&idle_event_mutex_);
+ pthread_cond_destroy(&idle_event_cond_);
+ #endif
+}
+
+void ProcessPool::Schedule( const string& task ) {
+#ifdef _WIN32
+ WaitForSingleObject(mutex_,INFINITE);
+#else
+ if(pthread_mutex_lock(&mutex_)){
+ ErrorExit("Error locking mutex");
+ }
+#endif
+ tasks_.push(task);
+ ProcessFirstTaskInQueue();
+#ifdef _WIN32
+ if(!ReleaseMutex(mutex_)){
+ ErrorExit("Error releasing mutex");
+ }
+#else
+ if(pthread_mutex_unlock(&mutex_)){
+ ErrorExit("Error releasing mutex");
+ }
+#endif
+}
+
+ProcessHandle::ProcessHandle( const string &worker_path, ProcessPool* parent_process_pool )
+ : idle_(true), os_process_(worker_path), parent_process_pool_(parent_process_pool)
+{}
+
+bool ProcessHandle::ProcessMessageFromChild(const string& msg){
+ size_t divider = (int)msg.find(": ");
+ if(divider == string::npos){
+ ErrorExit("No divider in message from child: "+msg);
+ }
+ string type = msg.substr(0, divider);
+ string body = msg.substr(divider+2, msg.size()-(divider+2));
+ if(type == "PRINT"){
+ cout << body << endl;
+ }
+ if(type == "STATE"){
+ if(body == "IDLE"){
+ cout << "Task complete!" << endl;
+ return true;
+ }
+ }
+ return false;
+}
+
+void ProcessHandle::ProcessInBackground( ) {
+ while(!ProcessMessageFromChild(os_process_.WaitForChildMessage())){}
+ idle_ = true;
+ parent_process_pool_->NotifyTaskComplete();
+}
+
+namespace {
+#ifdef _WIN32
+ DWORD WINAPI ThreadFunc(LPVOID process_handle_ptr){
+ ((ProcessHandle*)process_handle_ptr)->ProcessInBackground();
+ return 0;
+ }
+#else
+ void* ThreadFunc(void* process_handle_ptr){
+ ((ProcessHandle*)process_handle_ptr)->ProcessInBackground();
+ return NULL;
+ }
+#endif
+}
+
+void ProcessHandle::Process( const string& task ) {
+ os_process_.SendMessageToChild("TASK: "+task);
+ idle_ = false;
+#ifdef _WIN32
+ HANDLE thread = CreateThread(NULL, 0, ThreadFunc, this, 0, NULL);
+ CloseHandle(thread);
+#else
+ pthread_t thread;
+ pthread_create(&thread, NULL, ThreadFunc, this);
+#endif
+}
+
+namespace {
+const int kPathBufferSize = 1024;
+const int kPipeBufSize = 256;
+#ifdef _WIN32
+wstring WStrFromCStr(const char* cstr){
+ int cstr_len = (int)strlen(cstr);
+ wstring wstr;
+ wstr.resize(cstr_len);
+ for(int i=0; i<cstr_len; ++i){
+ wstr[i] = cstr[i];
+ }
+ return wstr;
+}
+#endif
+
+#ifdef _WIN32
+void ReadMessageFromPipe(const HANDLE &pipe_handle, string *msg){
+ DWORD dwRead;
+ CHAR chBuf[kPipeBufSize];
+ BOOL bSuccess = FALSE;
+ string &message = *msg;
+ message.clear();
+
+ int offset = 0;
+ bool continue_reading = true;
+ while(continue_reading){
+ if(!ReadFile( pipe_handle, chBuf, 1, &dwRead, NULL)){
+ ExitProcess(1); // Other process was probably killed
+ }
+ if(chBuf[0] == '\0'){
+ continue_reading = false;
+ } else {
+ message += chBuf[0];
+ }
+ }
+}
+#else
+void ReadMessageFromPipe(int pipe_handle, string *msg){
+ int bytes_read;
+ char buf[1];
+ string &message = *msg;
+ message.clear();
+
+ bool continue_reading = true;
+ while(continue_reading){
+ bytes_read = read(pipe_handle, buf, 1);
+ if(bytes_read <= 0){
+ exit(1); // Other process was probably killed
+ }
+ if(buf[0] == '\0'){
+ continue_reading = false;
+ } else {
+ message += buf[0];
+ }
+ }
+}
+#endif
+}
+
+
+#ifdef _WIN32
+void WriteMessageToPipe(const HANDLE& pipe_handle, const string &msg){
+ int msg_length = (int)msg.length();
+ int total_sent = 0;
+ DWORD num_written;
+ while (total_sent < msg_length){
+ if(!WriteFile(pipe_handle, &msg[0], (DWORD)msg.length(), &num_written, NULL)){
+ ExitProcess(1); // Other process was probably killed
+ }
+ total_sent += num_written;
+ }
+ CHAR end_str[] = {'\0'};
+ if(!WriteFile(pipe_handle, end_str, 1, &num_written, NULL) || num_written != 1){
+ ExitProcess(1); // Other process was probably killed
+ }
+}
+#else
+void WriteMessageToPipe(int pipe_handle, const string &msg){
+ int msg_length = (int)msg.length();
+ int total_sent = 0;
+ int bytes_written;
+ while (total_sent < msg_length){
+ bytes_written = write(pipe_handle, &msg[0], (int)msg.length());
+ if(bytes_written<0){
+ exit(1); // Other process was probably killed
+ }
+ total_sent += bytes_written;
+ }
+ char end_str[] = {'\0'};
+ bytes_written = write(pipe_handle, end_str, 1);
+ if(bytes_written != 1){
+ exit(1); // Other process was probably killed
+ }
+}
+#endif
+
+string OSProcess::WaitForChildMessage() {
+ string msg;
+ ReadMessageFromPipe(read_pipe_, &msg);
+ return msg;
+}
+
+#ifdef _WIN32
+OSProcess::OSProcess(const string &worker_path) {
+ LOGI << "Creating OS Process" << endl;
+ wchar_t path_utf16_buf[kPathBufferSize];
+ GetModuleFileNameW( NULL, path_utf16_buf, kPathBufferSize);
+ int last_slash_pos = -1;
+ for(int i=0; i<kPathBufferSize; ++i){
+ if(path_utf16_buf[i] == '\\'){
+ path_utf16_buf[i] = '/';
+ last_slash_pos = i;
+ } else if(path_utf16_buf[i] == '\0'){
+ break;
+ }
+ }
+
+ if(!MultiByteToWideChar(CP_UTF8, 0, worker_path.c_str(), -1, &path_utf16_buf[last_slash_pos+1], kPathBufferSize-(last_slash_pos+1))){
+ ErrorExit("Error converting worker path utf8 string to utf16: " + worker_path);
+ }
+
+ // This uses UTF16 for the first arg and UTF8 for subsequent args
+ wstring param;
+ param.reserve(kPathBufferSize);
+ param += '\"';
+ param += path_utf16_buf;
+ param += '\"';
+ param += L" ";
+ param += WStrFromCStr(kWorkerProcessString);
+
+ STARTUPINFOW startup_info;
+ memset(&startup_info, 0, sizeof(startup_info));
+ memset(&process_info_, 0, sizeof(process_info_));
+ startup_info.cb = sizeof(startup_info);
+ startup_info.dwFlags |= STARTF_USESTDHANDLES;
+
+ SECURITY_ATTRIBUTES inheritable;
+ inheritable.nLength = sizeof(SECURITY_ATTRIBUTES);
+ inheritable.bInheritHandle = TRUE;
+ inheritable.lpSecurityDescriptor = NULL;
+
+ // Create pipes for communication with child
+ if(!CreatePipe(&read_pipe_, &startup_info.hStdOutput, &inheritable, 0)){
+ ErrorExit("Error creating first child process pipe");
+ }
+ if(!CreatePipe(&startup_info.hStdInput, &write_pipe_, &inheritable, 0)){
+ ErrorExit("Error creating second child process pipe");
+ }
+ startup_info.hStdError = GetStdHandle(STD_OUTPUT_HANDLE);
+
+ // Set parent end of pipes to not be inheritable
+ if(!SetHandleInformation(read_pipe_, HANDLE_FLAG_INHERIT, 0)){
+ ErrorExit("Error setting inherit properties of first child process pipe");
+ }
+ if(!SetHandleInformation(write_pipe_, HANDLE_FLAG_INHERIT, 0)){
+ ErrorExit("Error setting inherit properties of second child process pipe");
+ }
+
+ if(!CreateProcessW(path_utf16_buf,
+ &param[0],
+ NULL, NULL, true,
+ NORMAL_PRIORITY_CLASS,
+ NULL, NULL, &startup_info,
+ &process_info_))
+ {
+ ErrorExit("Could not open process: "+worker_path);
+ }
+
+ if(WaitForChildMessage() != kChildInitString){
+ ErrorExit("Invalid initial acknowledgement from child process");
+ }
+}
+
+OSProcess::~OSProcess() {
+ CloseHandle(process_info_.hThread);
+ TerminateProcess(process_info_.hProcess, 0);
+ CloseHandle(process_info_.hProcess);
+ CloseHandle(read_pipe_);
+ CloseHandle(write_pipe_);
+}
+#else
+#ifdef __APPLE__
+#include <mach-o/dyld.h>
+#endif
+OSProcess::OSProcess(const string& worker_path) {
+ LOGI << "Creating OS Process" << endl;
+
+ int pipe_in[2], pipe_out[2];
+ if(pipe(pipe_in) == -1){
+ ErrorExit("Error creating first pipe");
+ }
+ if(pipe(pipe_out) == -1){
+ ErrorExit("Error creating second pipe");
+ }
+
+#ifdef __APPLE__
+ string path;
+ {
+ char path_buf[1024];
+ uint32_t size = sizeof(path_buf);
+ if(_NSGetExecutablePath(path_buf, &size) == 0){
+ path = path_buf;
+ } else {
+ ErrorExit("Could not get application path");
+ }
+ }
+#else
+ string path;
+ char buf[1024];
+ ssize_t len = ::readlink("/proc/self/exe", buf, sizeof(buf)-1);
+ if (len != -1) {
+ buf[len] = '\0';
+ path = buf;
+ } else {
+ ErrorExit("Could not get application path");
+ }
+#endif
+ size_t last_slash = path.rfind('/');
+ if(last_slash == string::npos){
+ path.clear();
+ } else {
+ path = path.substr(0,last_slash+1);
+ }
+ path += worker_path;
+
+
+ int pid = fork();
+ if(pid == 0){
+ if(dup2(pipe_in[0], STDIN_FILENO) == -1){
+ ErrorExit("Error assigning child STDIN pipe");
+ }
+ if(dup2(pipe_out[1], STDOUT_FILENO) == -1){
+ ErrorExit("Error assigning child STDOUT pipe");
+ }
+ if(dup2(pipe_out[1], STDERR_FILENO) == -1){
+ ErrorExit("Error assigning child STDERR pipe");
+ }
+ if(close(pipe_in[0]) == -1){
+ ErrorExit("Error closing pipe a");
+ }
+ if(close(pipe_in[1]) == -1){
+ ErrorExit("Error closing pipe b");
+ }
+ if(close(pipe_out[0]) == -1){
+ ErrorExit("Error closing pipe c");
+ }
+ if(close(pipe_out[1]) == -1){
+ ErrorExit("Error closing pipe d");
+ }
+
+ vector<char*> args;
+ args.push_back((char*)path.c_str());
+ args.push_back((char*)kWorkerProcessString);
+ args.push_back(NULL);
+ if(execv(path.c_str(), &args[0]) == -1){
+ ErrorExit("Could not exec: "+path);
+ }
+ } else if(pid == -1){
+ ErrorExit("Problem with fork()");
+ }
+
+ if(close(pipe_in[0]) == -1){
+ ErrorExit("Error closing pipe e");
+ }
+ if(close(pipe_out[1]) == -1){
+ ErrorExit("Error closing pipe f");
+ }
+
+ process_info_ = pid;
+ read_pipe_ = pipe_out[0];
+ write_pipe_ = pipe_in[1];
+
+ if(WaitForChildMessage() != kChildInitString){
+ ErrorExit("Invalid initial acknowledgement from child process");
+ }
+}
+
+#include <signal.h>
+OSProcess::~OSProcess() {
+ kill(process_info_, SIGKILL);
+ if(close(read_pipe_) == -1){
+ ErrorExit("Error closing pipe e");
+ }
+ if(close(write_pipe_) == -1){
+ ErrorExit("Error closing pipe f");
+ }
+}
+#endif
+
+
+void OSProcess::SendMessageToChild( const string &msg ) {
+ WriteMessageToPipe(write_pipe_, msg);
+}
+
+namespace {
+
+void SendMessageToParent(const string &msg){
+#ifdef _WIN32
+ WriteMessageToPipe(GetStdHandle(STD_OUTPUT_HANDLE), msg);
+#else
+ WriteMessageToPipe(STDOUT_FILENO, msg);
+#endif
+}
+string WaitForParentMessage(){
+ string msg;
+#ifdef _WIN32
+ ReadMessageFromPipe(GetStdHandle(STD_INPUT_HANDLE), &msg);
+#else
+ ReadMessageFromPipe(STDIN_FILENO, &msg);
+#endif
+ return msg;
+}
+
+int ChildProcessMessage(const string& msg, const ProcessPool::JobMap &job_map){
+ size_t divider = (int)msg.find(": ");
+ if(divider == string::npos){
+ ErrorExit("No divider in message from parent: "+msg);
+ }
+ string type = msg.substr(0, divider);
+ string body = msg.substr(divider+2, msg.size()-(divider+2));
+ if(type == "TASK"){
+ SendMessageToParent("PRINT: Starting task \""+body+"\"");
+ size_t space = (int)body.find(' ');
+ string job;
+ string params;
+ if(space == string::npos){
+ job = body;
+ } else {
+ job = body.substr(0, space);
+ params = body.substr(space + 1, body.size()-(space+1));
+ }
+ ProcessPool::JobMap::const_iterator iter = job_map.find(job);
+ if(iter == job_map.end()){
+ ErrorExit("No job named: "+job);
+ }
+ ProcessPool::JobFunctionPtr func = iter->second;
+ vector<string> params_separated;
+ while(!params.empty()){
+ bool in_quotes = false;
+ int quote_start = -1;
+ int quote_end = -1;
+ size_t space = string::npos;
+ int num_params = (int)params.size();
+ for(int i=0; i<num_params; ++i){
+ if(params[i] == '\"'){
+ if(!in_quotes){
+ quote_start = i;
+ } else {
+ quote_end = i;
+ }
+ in_quotes = !in_quotes;
+ }
+ if(!in_quotes && params[i] == ' '){
+ space = i;
+ break;
+ }
+ }
+ if(quote_start != -1 && quote_end != -1){
+ params_separated.push_back(params.substr(quote_start+1, quote_end-quote_start-1));
+ } else if(space == string::npos){
+ params_separated.push_back(params);
+ } else {
+ params_separated.push_back(params.substr(0, space));
+ }
+ if(space == string::npos){
+ params.clear();
+ } else {
+ params = params.substr(space + 1, params.size()-(space+1));
+ }
+ }
+ int argc = (int)params_separated.size();
+ vector<const char*> argv;
+ for(int i=0; i<argc; ++i){
+ argv.push_back(params_separated[i].c_str());
+ }
+#ifdef _WIN32
+ HANDLE temp_pipe_read, temp_pipe_write;
+ if(!CreatePipe(&temp_pipe_read, &temp_pipe_write, NULL, 0)){
+ ErrorExit("Error creating temporary process pipe");
+ }
+ HANDLE parent_communicate = GetStdHandle(STD_OUTPUT_HANDLE);
+ if(!SetStdHandle(STD_OUTPUT_HANDLE, temp_pipe_write)){
+ ErrorExit("Error assigning STD_OUTPUT_HANDLE to temporary process pipe");
+ }
+ if(!SetStdHandle(STD_ERROR_HANDLE, temp_pipe_write)){
+ ErrorExit("Error assigning STD_ERROR_HANDLE to temporary process pipe");
+ }
+ int ret_val = func(argc, &argv[0]);
+ SetStdHandle(STD_OUTPUT_HANDLE, parent_communicate);
+ SetStdHandle(STD_ERROR_HANDLE, parent_communicate);
+ CloseHandle(temp_pipe_read);
+ CloseHandle(temp_pipe_write);
+#else
+ int pipe_old = dup(STDOUT_FILENO);
+ int pipe_temp[2];
+ if(pipe(pipe_temp) == -1){
+ ErrorExit("Error creating temp pipe");
+ }
+ if(dup2(pipe_temp[1], STDERR_FILENO) == -1){
+ ErrorExit("Error redirecting child STDERR pipe");
+ }
+ if(dup2(pipe_temp[1], STDOUT_FILENO) == -1){
+ ErrorExit("Error redirecting child STDOUT pipe");
+ }
+ int ret_val = func(argc, &argv[0]);
+ if(dup2(pipe_old, STDERR_FILENO) == -1){
+ ErrorExit("Error redirecting child STDERR pipe");
+ }
+ if(dup2(pipe_old, STDOUT_FILENO) == -1){
+ ErrorExit("Error redirecting child STDOUT pipe");
+ }
+ if(close(pipe_temp[0]) == -1){
+ ErrorExit("Error closing pipe a");
+ }
+ if(close(pipe_temp[1]) == -1){
+ ErrorExit("Error closing pipe b");
+ }
+ if(close(pipe_old) == -1){
+ ErrorExit("Error closing pipe c");
+ }
+#endif
+ SendMessageToParent("NULL: "); // TODO: Why does it stop working without this?
+ return ret_val;
+ }
+ return 0;
+}
+
+} // namespace ""
+
+int ProcessPool::WorkerProcessMain(const JobMap &job_map) {
+ SendMessageToParent(kChildInitString);
+ while(1){
+ string msg = WaitForParentMessage();
+ ChildProcessMessage(msg, job_map);
+ SendMessageToParent("STATE: IDLE");
+ }
+ return 0;
+}
+
+void ProcessPool::NotifyTaskComplete() {
+#ifdef _WIN32
+ WaitForSingleObject(mutex_,INFINITE);
+#else
+ if(pthread_mutex_lock(&mutex_)){
+ ErrorExit("Error locking mutex");
+ }
+#endif
+ ProcessFirstTaskInQueue();
+#ifdef _WIN32
+ if(!ReleaseMutex(mutex_)){
+ ErrorExit("Error releasing mutex");
+ }
+#else
+ if(pthread_mutex_unlock(&mutex_)){
+ ErrorExit("Error releasing mutex");
+ }
+#endif
+}
+
+void ProcessPool::WaitForTasksToComplete() {
+ #ifdef _WIN32
+ WaitForSingleObject(idle_event_, INFINITE);
+ #else
+ if(pthread_mutex_lock(&idle_event_mutex_)){
+ ErrorExit("Error locking mutex");
+ }
+ if(!idle_event_bool_){
+ pthread_cond_wait(&idle_event_cond_, &idle_event_mutex_);
+ }
+ if(pthread_mutex_unlock(&idle_event_mutex_)){
+ ErrorExit("Error locking mutex");
+ }
+
+
+ #endif
+}
+
+void ProcessPool::ClearQueuedTasks() {
+ queue<string> empty;
+ swap(tasks_, empty);
+}
+
+int ProcessPool::NumProcesses() {
+ return processes_.size();
+}