blob: ae205c6850df3287bc2c2707f9a08cf63dd7309d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.sysds.runtime.util;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sysds.runtime.DMLRuntimeException;
import org.apache.sysds.utils.stats.InfrastructureAnalyzer;
/**
* This common thread pool provides an abstraction to obtain a shared thread pool.
*
* If the number of logical cores is specified a ForkJoinPool.commonPool is returned on all requests.
*
* If pools of different size are requested, we create new pool instances of FixedThreadPool, Unless we currently are on
* the main thread, Then we return a shared instance of the first requested number of cores.
*
* Alternatively the class also contain a dynamic threadPool, that is intended for asynchronous long running tasks with
* low compute overhead, such as broadcast and collect from federated workers.
*/
public class CommonThreadPool implements ExecutorService {
/** Log object */
protected static final Log LOG = LogFactory.getLog(CommonThreadPool.class.getName());
/** The number of threads of the machine */
private static final int size = InfrastructureAnalyzer.getLocalParallelism();
/**
* Shared thread pool used system-wide, potentially by concurrent parfor workers
*
* we use the ForkJoinPool.commonPool() to avoid explicit cleanup, including unnecessary initialization (e.g.,
* problematic in jmlc) and because this commonPool resulted in better performance than a dedicated fixed thread
* pool.
*/
private static final ExecutorService shared = ForkJoinPool.commonPool();
/** A secondary thread local executor that use a custom number of threads for PARFOR */
private static ConcurrentHashMap<Long, CommonThreadPool> shared2 = null;
/** Dynamic thread pool, that dynamically allocate threads as tasks come in. */
private static ExecutorService asyncPool = null;
/** This common thread pool */
private final ExecutorService _pool;
/**
* Constructor of the threadPool. This is intended not to be used except for tests. Please use the static
* constructors.
*
* @param pool The thread pool instance to use.
*/
public CommonThreadPool(ExecutorService pool) {
this._pool = pool;
}
/**
* Get the shared Executor thread pool, that have the number of threads of the host system
*
* @return An ExecutorService
*/
public static ExecutorService get() {
return shared;
}
/**
* Get a Executor thread pool, that have the number of threads specified in k.
*
* The thread pool can be reused by other processes in the same host thread requesting another pool of the same
* number of threads. The executor that is guaranteed ThreadLocal except if it is number of host logical cores.
*
*
* @param k The number of threads wanted
* @return The executor with specified parallelism
*/
public synchronized static ExecutorService get(int k) {
final Thread thisThread = Thread.currentThread();
final String threadName = thisThread.getName();
// Contains main, because we name our test threads TestRunner_main
final boolean mainThread = threadName.contains("main");
if(size == k && mainThread)
return shared; // use the default thread pool if main thread and max parallelism.
else if(mainThread || threadName.contains("PARFOR")) {
CommonThreadPool pool;
if(shared2 == null) // If there is no current shared pool allocate one.
shared2 = new ConcurrentHashMap<>();
pool = shared2.get(thisThread.getId());
if(pool == null) { // If there is no pool for this thread allocate one.
pool = new CommonThreadPool(new ForkJoinPool(k));
shared2.put(thisThread.getId(), pool);
}
return pool; // Return the shared pool for this parfor or main thread.
}
else {
// If we are neither a main thread or parfor thread, allocate a new thread pool
LOG.warn("An instruction allocated it's own thread pool indicating that some task is not properly reusing the threads.");
return Executors.newFixedThreadPool(k);
}
}
/**
* Invoke the collection of tasks and shutdown the pool upon job termination.
*
* @param <T> The type of class to return from the job
* @param pool The pool to execute in
* @param tasks The tasks to execute
*/
public static <T> void invokeAndShutdown(ExecutorService pool, Collection<? extends Callable<T>> tasks) {
try {
// execute tasks
List<Future<T>> ret = pool.invokeAll(tasks);
// check for errors and exceptions
for(Future<T> r : ret)
r.get();
}
catch(Exception ex) {
throw new DMLRuntimeException(ex);
}
finally {
pool.shutdown();
}
}
/**
* Get a dynamic thread pool that allocate threads as the requests are made. This pool is intended for async remote
* calls that does not depend on local compute.
*
* @return A dynamic thread pool.
*/
public synchronized static ExecutorService getDynamicPool() {
if(asyncPool != null && !(asyncPool.isShutdown() || asyncPool.isTerminated()))
return asyncPool;
else {
asyncPool = Executors.newCachedThreadPool();
return asyncPool;
}
}
/**
* Shutdown the cached thread pools.
*/
public synchronized static void shutdownAsyncPools() {
if(asyncPool != null) {
// shutdown prefetch/broadcast thread pool
asyncPool.shutdown();
asyncPool = null;
}
if(shared2 != null) {
try {
for(Entry<Long, CommonThreadPool> e : shared2.entrySet())
for(Runnable a : e.getValue()._pool.shutdownNow())
a.wait();
}
catch(Exception e1) {
throw new RuntimeException(e1);
}
finally {
shared2 = null;
}
}
}
/**
* Shutdown the asynchronous pool associated with the given thread.
*
* @param thread The thread given that could or could not have allocated a thread pool itself.
*/
public synchronized static void shutdownAsyncPools(Thread thread) {
if(shared2 != null) {
try {
final CommonThreadPool p = shared2.get(thread.getId());
if(p != null) {
for(Runnable a : p._pool.shutdownNow())
a.wait();
shared2.remove(thread.getId());
}
}
catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* Get if the current threads that calls have a cached thread pool available.
*
* This method is synchronized to analyze the general cache not the common thread pool for the main thread.
*
* @return If there is a thread pool allocated for this thread.
*/
public static synchronized boolean generalCached() {
return shared2 != null && shared2.get(Thread.currentThread().getId()) != null;
}
/**
* If there is a thread pool cached for this thread.
*
* @return if there is a cached thread pool.
*/
public final boolean isCached() {
return _pool.equals(shared) || generalCached();
}
@Override
public synchronized void shutdown() {
if(!isCached())
_pool.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return !isCached() ? _pool.shutdownNow() : null;
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
return _pool.invokeAll(tasks);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException {
return _pool.invokeAll(tasks, timeout, unit);
}
@Override
public void execute(Runnable command) {
_pool.execute(command);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return _pool.submit(task);
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
return _pool.submit(task, result);
}
@Override
public Future<?> submit(Runnable task) {
return _pool.submit(task);
}
@Override
public boolean isShutdown() {
return isCached() || _pool.isShutdown();
}
@Override
public boolean isTerminated() {
return isCached() || _pool.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return isCached() || _pool.awaitTermination(timeout, unit);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
return _pool.invokeAny(tasks);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return _pool.invokeAny(tasks);
}
/**
* This method returns true or false depending on if the current thread have an overhead of having to allocate it's
* own thread pool if parallelization is requested.
*
* Currently if it is the Main thread or a PARFOR thread, then we suggest using parallelism since we can reuse the
* allocated threads.
*
* @return If parallelism is suggested.
*/
public static boolean useParallelismOnThread() {
Thread t = Thread.currentThread();
String name = t.getName();
if(name.equals("main"))
return true;
else if(name.contains("PARFOR"))
return true;
else if(name.contains("test"))
return true;
else
return false;
}
}