Răsfoiți Sursa

Merge pull request #5491 from IvanSavenko/threading

Use tbb thread pool instead of custom one for RMG
Ivan Savenko 8 luni în urmă
părinte
comite
272943f38a

+ 32 - 7
docs/developers/Code_Structure.md

@@ -109,16 +109,41 @@ Stupid AI is recent and used battle AI.
 
 VCAI module is currently developed agent-based system driven by goals and heroes.
 
-### Programming challenge
+## Threading Model
 
-### Fuzzy logic
+## Long-living threads
 
-VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make use of fuzzy rule-based algorithms. They are useful to handle uncertainty and resemble human behaviour who takes decisions based on rough observations. FuzzyLite is linked as separate static library in AI/FuzzyLite.lib file.
+Here is list of threads including their name that can be seen in logging or in debugging:
+
+- Main thread (`MainGUI`). This is main thread that is created on app start. This thread is responsible for input processing (including updating screen due to player actions) and for final rendering steps. Note that on some OS'es (like Linux) name of main thread is also name of the application. Because of that, thread name is only set for logging, while debugger will show this thread with default name.
+
+- Network thread (`runNetwork`). Name is semi-historical, since in case of single-player game no longer uses networking, but intra-process communication. In either case, this thread permanently runs boost::asio io_service, and processes any callbacks received through it, whether it is incoming packets from network, or incoming data from another thread. Following actions are also done on this thread, due to being called as part of netpack processing:
+  - combat AI actions
+  - UI feedback on any incoming netpacks. Note that this also includes awaiting for any animation - whether in-combat or adventure map. When animations are playing, network thread will be waiting for animations to finish.
+  - Initial reaction of adventure map AI on netpack. However, AI will usually dispatch this event to AI thread, and perform actual processing in AI thread.
+
+- Server thread (`runServer`). This thread exists for as long as player is in singleplayer game or hosting a multiplayer game, whether on map selection, or already in loaded game. Just like networking thread, this thread also permanently runs own boost::asio io_service, and processes any incoming player (either human or AI) requests, whether through network or through intraprocess communication. When standalone vcmiserver is used, entire server will be represented by this thread.
+
+- Console thread (`consoleHandler`). This thread usually does nothing, and only performs processing of incoming console commands on standard input, which is accessible by running vcmiclient directly.
+
+### Intel TBB
 
-## Utilities
+- NullkillerAI parallelizes a lot of its tasks using TBB methods, mostly parallel_for
+- Random map generator actively uses thread pool provided by TBB
+- Client performs image upscaling in background thread to avoid visible freezes
 
-### Launcher
+## Short-living threads
 
-### Duels
+- AI thread (`AIGateway::makeTurn`). Adventure AI creates its thread whenever it stars a new turn, and terminates it when turn ends. Majority of AI event processing is done in this thread, however some actions are either offloaded entirely as tbb task, or parallelized using methods like parallel_for.
 
-### ERM parser
+- AI helper thread (`AIGateway::doActionASAP`). Adventure AI creates such thread whenever it receives event that requires processing without locking network thread that initiated the call.
+
+- Autocombat initiation thread (`autofightingAI`). Combat AI usually runs on network thread, as reaction on unit taking turn netpack event. However initial activation of AI when player presses hotkey or button is done in input processing (`MainGUI`) thread. To avoid freeze when AI selects its first action, this action is done on a temporary thread
+
+- Initializition thread (`initialize`). On game start, to avoid delay in game loading, most of game library initialization is done in separate thread while main thread is playing intro movies.
+
+- Console command processing (`processCommand`). Some console commands that can be entered in game chat either take a long time to process or expect to run without holding any mutexes (like interface mutex). To avoid such problems, all commands entered in game chat are run in separate thread.
+
+### Fuzzy logic
+
+VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make use of fuzzy rule-based algorithms. They are useful to handle uncertainty and resemble human behaviour who takes decisions based on rough observations. FuzzyLite is linked as separate static library in AI/FuzzyLite.lib file.

+ 2 - 4
lib/CMakeLists.txt

@@ -217,7 +217,7 @@ set(lib_MAIN_SRCS
 	rmg/modificators/ObstaclePlacer.cpp
 	rmg/modificators/RiverPlacer.cpp
 	rmg/modificators/TerrainPainter.cpp
-	rmg/threadpool/MapProxy.cpp
+	rmg/MapProxy.cpp
 
 	serializer/BinaryDeserializer.cpp
 	serializer/BinarySerializer.cpp
@@ -633,9 +633,7 @@ set(lib_MAIN_HEADERS
 	rmg/modificators/ObstaclePlacer.h
 	rmg/modificators/RiverPlacer.h
 	rmg/modificators/TerrainPainter.h
-	rmg/threadpool/BlockingQueue.h
-	rmg/threadpool/ThreadPool.h
-	rmg/threadpool/MapProxy.h
+	rmg/MapProxy.h
 
 	serializer/BinaryDeserializer.h
 	serializer/BinarySerializer.h

+ 6 - 11
lib/rmg/CMapGenerator.cpp

@@ -29,7 +29,6 @@
 #include "Zone.h"
 #include "Functions.h"
 #include "RmgMap.h"
-#include "threadpool/ThreadPool.h"
 #include "modificators/ObjectManager.h"
 #include "modificators/TreasurePlacer.h"
 #include "modificators/RoadPlacer.h"
@@ -37,6 +36,8 @@
 #include <vstd/RNG.h>
 #include <vcmi/HeroTypeService.h>
 
+#include <tbb/task_group.h>
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 CMapGenerator::CMapGenerator(CMapGenOptions& mapGenOptions, IGameCallback * cb, int RandomSeed) :
@@ -395,10 +396,7 @@ void CMapGenerator::fillZones()
 	}
 	else
 	{
-		ThreadPool pool;
-		std::vector<boost::future<void>> futures;
-		//At most one Modificator can run for every zone
-		pool.init(std::min<int>(boost::thread::hardware_concurrency(), numZones));
+		tbb::task_group pool;
 
 		while (!allJobs.empty())
 		{
@@ -412,12 +410,12 @@ void CMapGenerator::fillZones()
 				else if ((*it)->isReady())
 				{
 					auto jobCopy = *it;
-					futures.emplace_back(pool.async([this, jobCopy]() -> void
+					pool.run([this, jobCopy]() -> void
 						{
 							jobCopy->run();
 							Progress::Progress::step(); //Update progress bar
 						}
-					));
+					);
 					it = allJobs.erase(it);
 				}
 				else
@@ -428,10 +426,7 @@ void CMapGenerator::fillZones()
 		}
 
 		//Wait for all the tasks
-		for (auto& fut : futures)
-		{
-			fut.get();
-		}
+		pool.wait();
 	}
 
 	for (const auto& it : map->getZones())

+ 2 - 2
lib/rmg/threadpool/MapProxy.cpp → lib/rmg/MapProxy.cpp

@@ -9,8 +9,8 @@
  */
 
 #include "MapProxy.h"
-#include "../../TerrainHandler.h"
-#include "../../GameLibrary.h"
+#include "../TerrainHandler.h"
+#include "../GameLibrary.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 3 - 3
lib/rmg/threadpool/MapProxy.h → lib/rmg/MapProxy.h

@@ -11,9 +11,9 @@
 #pragma once
 
 #include "StdInc.h"
-#include "../../mapping/CMap.h"
-#include "../RmgMap.h"
-#include "../../mapping/CMapEditManager.h"
+#include "../mapping/CMap.h"
+#include "RmgMap.h"
+#include "../mapping/CMapEditManager.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 1 - 1
lib/rmg/RmgMap.h

@@ -11,7 +11,7 @@
 #pragma once
 #include "../int3.h"
 #include "../GameConstants.h"
-#include "threadpool/MapProxy.h"
+#include "MapProxy.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 2 - 2
lib/rmg/modificators/Modificator.h

@@ -13,7 +13,7 @@
 #include "../../GameConstants.h"
 #include "../../int3.h"
 #include "../Zone.h"
-#include "../threadpool/MapProxy.h"
+#include "../MapProxy.h"
 
 class RmgMap;
 class CMapGenerator;
@@ -78,4 +78,4 @@ private:
 	void dump();
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/rmg/modificators/RoadPlacer.cpp

@@ -15,7 +15,7 @@
 #include "RockFiller.h"
 #include "../Functions.h"
 #include "../CMapGenerator.h"
-#include "../threadpool/MapProxy.h"
+#include "../MapProxy.h"
 #include "../../mapping/CMapEditManager.h"
 #include "../../mapObjects/CGObjectInstance.h"
 #include "../../modding/IdentifierStorage.h"

+ 1 - 1
lib/rmg/modificators/RockFiller.cpp

@@ -21,7 +21,7 @@
 #include "../../TerrainHandler.h"
 #include "../lib/mapping/CMapEditManager.h"
 #include "../TileInfo.h"
-#include "../threadpool/MapProxy.h"
+#include "../MapProxy.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 0 - 91
lib/rmg/threadpool/BlockingQueue.h

@@ -1,91 +0,0 @@
-/*
- * BlockingQueue.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-
-#pragma once
-
-#include "StdInc.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-//Credit to https://github.com/Liam0205/toy-threadpool/tree/master/yuuki
-
-template <typename T>
-class DLL_LINKAGE BlockingQueue : protected std::queue<T>
-{
-	using WriteLock = std::unique_lock<std::shared_mutex>;
-	using Readlock = std::shared_lock<std::shared_mutex>;
-
-public:
-	BlockingQueue() = default;
-	~BlockingQueue()
-	{
-		clear();
-  	}
-	BlockingQueue(const BlockingQueue&) = delete;
-	BlockingQueue(BlockingQueue&&) = delete;
-	BlockingQueue& operator=(const BlockingQueue&) = delete;
-	BlockingQueue& operator=(BlockingQueue&&) = delete;
-
-public:
-	bool empty() const
-	{
-		Readlock lock(mx);
-		return std::queue<T>::empty();
-	}
-
-	size_t size() const
-	{
-		Readlock lock(mx);
-		return std::queue<T>::size();
-	}
-
-public:
-	void clear()
-	{
-		WriteLock lock(mx);
-		while (!std::queue<T>::empty())
-		{
-			std::queue<T>::pop();
-		}
-	}
-
-	void push(const T& obj)
-	{
-		WriteLock lock(mx);
-		std::queue<T>::push(obj);
-	}
-
-	template <typename... Args>
-	void emplace(Args&&... args)
-	{
-		WriteLock lock(mx);
-		std::queue<T>::emplace(std::forward<Args>(args)...);
-	}
-
-	bool pop(T& holder)
-	{
-		WriteLock lock(mx);
-		if (std::queue<T>::empty())
-		{
-			return false;
-		}
-		else
-		{
-			holder = std::move(std::queue<T>::front());
-			std::queue<T>::pop();
-			return true;
-		}
-	}
-
-private:
-	mutable std::shared_mutex mx;
-};
-
-VCMI_LIB_NAMESPACE_END

+ 0 - 191
lib/rmg/threadpool/ThreadPool.h

@@ -1,191 +0,0 @@
-/*
- * ThreadPool.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-
-#pragma once
-
-#include "BlockingQueue.h"
-#include <boost/thread/future.hpp>
-#include <boost/thread/condition_variable.hpp>
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-typedef std::function<void()> TRMGfunction ;
-typedef std::optional<TRMGfunction> TRMGJob;
-
-//Credit to https://github.com/Liam0205/toy-threadpool/tree/master/yuuki
-
-class DLL_LINKAGE ThreadPool
-{
-private:
-	using Lock = std::unique_lock<std::shared_mutex>;
-	mutable std::shared_mutex mx;
-	mutable std::condition_variable_any cv;
-	mutable std::once_flag once;
-
-	bool isInitialized = false;
-	bool stopping = false;
-	bool canceling = false;
-public:
-	ThreadPool();
-	~ThreadPool();
-
-	void init(size_t numThreads);
-	void spawn();
-	void terminate();
-	void cancel();
-
-public:
-	bool initialized() const;
-	bool running() const;
-	int size() const;
-private:
-	bool isRunning() const;
-
-public:
-	auto async(std::function<void()>&& f) const -> boost::future<void>;
-
-private:
-	std::vector<boost::thread> workers;
-	mutable BlockingQueue<TRMGfunction> tasks;
-};
-
-ThreadPool::ThreadPool()
-{};
-
-ThreadPool::~ThreadPool()
-{
-	terminate();
-}
-
-inline void ThreadPool::init(size_t numThreads)
-{
-	std::call_once(once, [this, numThreads]()
-	{
-		Lock lock(mx);
-		stopping = false;
-		canceling = false;
-		workers.reserve(numThreads);
-		for (size_t i = 0; i < numThreads; ++i)
-		{
-			workers.emplace_back(std::bind(&ThreadPool::spawn, this));
-		}
-		isInitialized = true;
-	});
-}
-
-bool ThreadPool::isRunning() const
-{
-	return isInitialized && !stopping && !canceling;
-}
-
-inline bool ThreadPool::initialized() const
-{
-	Lock lock(mx);
-	return isInitialized;
-}
-
-inline bool ThreadPool::running() const
-{
-	Lock lock(mx);
-	return isRunning();
-}
-
-inline int ThreadPool::size() const
-{
-	Lock lock(mx);
-	return workers.size();
-}
-
-inline void ThreadPool::spawn()
-{
-	while(true)
-	{
-		bool pop = false;
-		TRMGfunction task;
-		{
-			Lock lock(mx);
-			cv.wait(lock, [this, &pop, &task]
-			{
-				pop = tasks.pop(task);
-				return canceling || stopping || pop;
-			});
-		}
-		if (canceling || (stopping && !pop))
-		{
-			return;
-		}
-		task();
-	}
-}
-
-inline void ThreadPool::terminate()
-{
-	{
-		Lock lock(mx);
-		if (isRunning())
-		{
-			stopping = true;
-		}
-		else
-		{
-			return;
-		}
-	}
-	cv.notify_all();
-	for (auto& worker : workers)
-	{
-		worker.join();
-	}
-}
-
-inline void ThreadPool::cancel()
-{
-	{
-		Lock lock(mx);
-		if (running())
-		{
-			canceling = true;
-		}
-		else
-		{
-			return;
-		}
-	}
-	tasks.clear();
-	cv.notify_all();
-	for (auto& worker : workers)
-	{
-		worker.join();
-	}
-}
-
-auto ThreadPool::async(std::function<void()>&& f) const -> boost::future<void>
-{
-	using TaskT = boost::packaged_task<void>;
-
-    {
-        Lock lock(mx);
-        if (stopping || canceling)
-        {
-            throw std::runtime_error("Delegating task to a threadpool that has been terminated or canceled.");
-        }
-    }
-
-    auto task = std::make_shared<TaskT>(f);
-    boost::future<void> fut = task->get_future();
-    tasks.emplace([task]() -> void
-    {
-        (*task)();
-    });
-    cv.notify_one();
-    return fut;
-}
-
-VCMI_LIB_NAMESPACE_END