├── Fault.cpp ├── Fault.h ├── CMakeLists.txt ├── .github └── workflows │ ├── cmake_ubuntu.yml │ ├── cmake_clang.yml │ └── cmake_windows.yml ├── LICENSE ├── main.cpp ├── WorkerThread.h ├── .gitignore ├── WorkerThread.cpp └── README.md /Fault.cpp: -------------------------------------------------------------------------------- 1 | #include "Fault.h" 2 | #include 3 | #if WIN32 4 | #include "windows.h" 5 | #endif 6 | 7 | //---------------------------------------------------------------------------- 8 | // FaultHandler 9 | //---------------------------------------------------------------------------- 10 | void FaultHandler(const char* file, unsigned short line) 11 | { 12 | #if WIN32 13 | // If you hit this line, it means one of the ASSERT macros failed. 14 | DebugBreak(); 15 | #endif 16 | 17 | assert(0); 18 | } -------------------------------------------------------------------------------- /Fault.h: -------------------------------------------------------------------------------- 1 | #ifndef _FAULT_H 2 | #define _FAULT_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #define ASSERT() \ 9 | FaultHandler(__FILE__, (unsigned short) __LINE__) 10 | 11 | #define ASSERT_TRUE(condition) \ 12 | do {if (!(condition)) FaultHandler(__FILE__, (unsigned short) __LINE__);} while (0) 13 | 14 | /// Handles all software assertions in the system. 15 | /// @param[in] file - the file name that the software assertion occurred on 16 | /// @param[in] line - the line number that the software assertion occurred on 17 | void FaultHandler(const char* file, unsigned short line); 18 | 19 | #ifdef __cplusplus 20 | } 21 | #endif 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Example CMake command line to create project build files: 2 | # 3 | # *** Windows *** 4 | # cmake -G "Visual Studio 17 2022" -A Win32 -B Build -S . 5 | # 6 | # *** Linux *** 7 | # cmake -G "Unix Makefiles" -B Build -S . 8 | 9 | # Specify the minimum CMake version required 10 | cmake_minimum_required(VERSION 3.10) 11 | 12 | # Project name and language (C or C++) 13 | project(StdWorkerThread VERSION 1.0 LANGUAGES CXX) 14 | 15 | # Collect all .cpp source files in the current directory 16 | file(GLOB SOURCES "${CMAKE_SOURCE_DIR}/*.cpp" "${CMAKE_SOURCE_DIR}/*.h") 17 | 18 | # Add an executable target 19 | add_executable(StdWorkerThreadApp ${SOURCES}) 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/cmake_ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Trigger on push 7 | pull_request: 8 | branches: 9 | - master # Trigger on pull 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest # Use Ubuntu environment for the build 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 # Checkout the repository code 18 | 19 | - name: Configure CMake 20 | run: cmake -S . -B build # Configure CMake to generate build files in 'build' directory 21 | 22 | - name: Build 23 | run: cmake --build build # Build the project using CMake 24 | 25 | - name: Run StdWorkerThreadApp 26 | run: ./build/StdWorkerThreadApp # Run the built executable 27 | -------------------------------------------------------------------------------- /.github/workflows/cmake_clang.yml: -------------------------------------------------------------------------------- 1 | name: Clang 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Trigger on push 7 | pull_request: 8 | branches: 9 | - master # Trigger on pull 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest # Use Ubuntu environment for the build 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 # Checkout the repository code 18 | 19 | - name: Configure CMake with Clang 20 | run: cmake -S . -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ # Configure CMake with Clang as the compiler 21 | 22 | - name: Build 23 | run: cmake --build build # Build the project using CMake 24 | 25 | - name: Run StdWorkerThreadApp 26 | run: ./build/StdWorkerThreadApp # Run the built executable 27 | -------------------------------------------------------------------------------- /.github/workflows/cmake_windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Trigger on push 7 | pull_request: 8 | branches: 9 | - master # Trigger on pull 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-latest # Use Windows environment for the build 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 # Checkout the repository code 18 | 19 | - name: Set up Visual Studio 20 | uses: microsoft/setup-msbuild@v1.1 # Set up Visual Studio environment (MSBuild) 21 | 22 | - name: Configure CMake 23 | run: cmake -S . -B build -G "Visual Studio 17 2022" # Configure CMake for Visual Studio 24 | 25 | - name: Build 26 | run: cmake --build build --config Release # Build the project using CMake with Release configuration 27 | 28 | - name: Run StdWorkerThreadApp 29 | run: .\build\Release\StdWorkerThreadApp.exe # Run the built executable (adjust path for MSBuild) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 David Lafreniere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "WorkerThread.h" 2 | #include "Fault.h" 3 | #include 4 | 5 | // @see https://github.com/endurodave/StdWorkerThread 6 | // David Lafreniere 7 | 8 | using namespace std; 9 | 10 | // Worker thread instances 11 | WorkerThread workerThread1("WorkerThread1"); 12 | WorkerThread workerThread2("WorkerThread2"); 13 | 14 | //------------------------------------------------------------------------------ 15 | // main 16 | //------------------------------------------------------------------------------ 17 | int main(void) 18 | { 19 | // Create worker threads 20 | workerThread1.CreateThread(); 21 | workerThread2.CreateThread(); 22 | 23 | // Create message to send to worker thread 1 24 | std::shared_ptr userData1(new UserData()); 25 | userData1->msg = "Hello world"; 26 | userData1->year = 2017; 27 | 28 | // Post the message to worker thread 1 29 | workerThread1.PostMsg(userData1); 30 | 31 | // Create message to send to worker thread 2 32 | std::shared_ptr userData2(new UserData()); 33 | userData2->msg = "Goodbye world"; 34 | userData2->year = 2017; 35 | 36 | // Post the message to worker thread 2 37 | workerThread2.PostMsg(userData2); 38 | 39 | std::this_thread::sleep_for(std::chrono::seconds(1)); 40 | 41 | workerThread1.ExitThread(); 42 | workerThread2.ExitThread(); 43 | 44 | return 0; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /WorkerThread.h: -------------------------------------------------------------------------------- 1 | #ifndef _THREAD_STD_H 2 | #define _THREAD_STD_H 3 | 4 | // @see https://www.codeproject.com/Articles/1169105/Cplusplus-std-thread-Event-Loop-with-Message-Queue 5 | // David Lafreniere, Feb 2017. 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | struct UserData 16 | { 17 | std::string msg; 18 | int year; 19 | }; 20 | 21 | struct ThreadMsg; 22 | 23 | class WorkerThread 24 | { 25 | public: 26 | /// Constructor 27 | WorkerThread(const std::string& threadName); 28 | 29 | /// Destructor 30 | ~WorkerThread(); 31 | 32 | /// Called once to create the worker thread 33 | /// @return True if thread is created. False otherwise. 34 | bool CreateThread(); 35 | 36 | /// Called once a program exit to exit the worker thread 37 | void ExitThread(); 38 | 39 | /// Get the ID of this thread instance 40 | /// @return The worker thread ID 41 | std::thread::id GetThreadId(); 42 | 43 | /// Get the ID of the currently executing thread 44 | /// @return The current thread ID 45 | static std::thread::id GetCurrentThreadId(); 46 | 47 | /// Add a message to the thread queue 48 | /// @param[in] data - thread specific message information 49 | void PostMsg(std::shared_ptr msg); 50 | 51 | /// Get size of thread message queue. 52 | size_t GetQueueSize(); 53 | 54 | /// Get thread name 55 | std::string GetThreadName() { return THREAD_NAME; } 56 | 57 | private: 58 | WorkerThread(const WorkerThread&) = delete; 59 | WorkerThread& operator=(const WorkerThread&) = delete; 60 | 61 | /// Entry point for the worker thread 62 | void Process(); 63 | 64 | /// Entry point for timer thread 65 | void TimerThread(); 66 | 67 | void SetThreadName(std::thread::native_handle_type handle, const std::string& name); 68 | 69 | std::unique_ptr m_thread; 70 | std::queue> m_queue; 71 | std::mutex m_mutex; 72 | std::condition_variable m_cv; 73 | std::atomic m_timerExit; 74 | const std::string THREAD_NAME; 75 | 76 | // Promise and future to synchronize thread start 77 | std::promise m_threadStartPromise; 78 | std::future m_threadStartFuture; 79 | 80 | std::atomic m_exit; 81 | }; 82 | 83 | #endif 84 | 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | Build 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # Benchmark Results 47 | BenchmarkDotNet.Artifacts/ 48 | 49 | # .NET Core 50 | project.lock.json 51 | project.fragment.lock.json 52 | artifacts/ 53 | **/Properties/launchSettings.json 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # Visual Studio Trace Files 101 | *.e2e 102 | 103 | # TFS 2012 Local Workspace 104 | $tf/ 105 | 106 | # Guidance Automation Toolkit 107 | *.gpState 108 | 109 | # ReSharper is a .NET coding add-in 110 | _ReSharper*/ 111 | *.[Rr]e[Ss]harper 112 | *.DotSettings.user 113 | 114 | # JustCode is a .NET coding add-in 115 | .JustCode 116 | 117 | # TeamCity is a build add-in 118 | _TeamCity* 119 | 120 | # DotCover is a Code Coverage Tool 121 | *.dotCover 122 | 123 | # AxoCover is a Code Coverage Tool 124 | .axoCover/* 125 | !.axoCover/settings.json 126 | 127 | # Visual Studio code coverage results 128 | *.coverage 129 | *.coveragexml 130 | 131 | # NCrunch 132 | _NCrunch_* 133 | .*crunch*.local.xml 134 | nCrunchTemp_* 135 | 136 | # MightyMoose 137 | *.mm.* 138 | AutoTest.Net/ 139 | 140 | # Web workbench (sass) 141 | .sass-cache/ 142 | 143 | # Installshield output folder 144 | [Ee]xpress/ 145 | 146 | # DocProject is a documentation generator add-in 147 | DocProject/buildhelp/ 148 | DocProject/Help/*.HxT 149 | DocProject/Help/*.HxC 150 | DocProject/Help/*.hhc 151 | DocProject/Help/*.hhk 152 | DocProject/Help/*.hhp 153 | DocProject/Help/Html2 154 | DocProject/Help/html 155 | 156 | # Click-Once directory 157 | publish/ 158 | 159 | # Publish Web Output 160 | *.[Pp]ublish.xml 161 | *.azurePubxml 162 | # Note: Comment the next line if you want to checkin your web deploy settings, 163 | # but database connection strings (with potential passwords) will be unencrypted 164 | *.pubxml 165 | *.publishproj 166 | 167 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 168 | # checkin your Azure Web App publish settings, but sensitive information contained 169 | # in these scripts will be unencrypted 170 | PublishScripts/ 171 | 172 | # NuGet Packages 173 | *.nupkg 174 | # The packages folder can be ignored because of Package Restore 175 | **/[Pp]ackages/* 176 | # except build/, which is used as an MSBuild target. 177 | !**/[Pp]ackages/build/ 178 | # Uncomment if necessary however generally it will be regenerated when needed 179 | #!**/[Pp]ackages/repositories.config 180 | # NuGet v3's project.json files produces more ignorable files 181 | *.nuget.props 182 | *.nuget.targets 183 | 184 | # Microsoft Azure Build Output 185 | csx/ 186 | *.build.csdef 187 | 188 | # Microsoft Azure Emulator 189 | ecf/ 190 | rcf/ 191 | 192 | # Windows Store app package directories and files 193 | AppPackages/ 194 | BundleArtifacts/ 195 | Package.StoreAssociation.xml 196 | _pkginfo.txt 197 | *.appx 198 | 199 | # Visual Studio cache files 200 | # files ending in .cache can be ignored 201 | *.[Cc]ache 202 | # but keep track of directories ending in .cache 203 | !*.[Cc]ache/ 204 | 205 | # Others 206 | ClientBin/ 207 | ~$* 208 | *~ 209 | *.dbmdl 210 | *.dbproj.schemaview 211 | *.jfm 212 | *.pfx 213 | *.publishsettings 214 | orleans.codegen.cs 215 | 216 | # Since there are multiple workflows, uncomment next line to ignore bower_components 217 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 218 | #bower_components/ 219 | 220 | # RIA/Silverlight projects 221 | Generated_Code/ 222 | 223 | # Backup & report files from converting an old project file 224 | # to a newer Visual Studio version. Backup files are not needed, 225 | # because we have git ;-) 226 | _UpgradeReport_Files/ 227 | Backup*/ 228 | UpgradeLog*.XML 229 | UpgradeLog*.htm 230 | 231 | # SQL Server files 232 | *.mdf 233 | *.ldf 234 | *.ndf 235 | 236 | # Business Intelligence projects 237 | *.rdl.data 238 | *.bim.layout 239 | *.bim_*.settings 240 | 241 | # Microsoft Fakes 242 | FakesAssemblies/ 243 | 244 | # GhostDoc plugin setting file 245 | *.GhostDoc.xml 246 | 247 | # Node.js Tools for Visual Studio 248 | .ntvs_analysis.dat 249 | node_modules/ 250 | 251 | # Typescript v1 declaration files 252 | typings/ 253 | 254 | # Visual Studio 6 build log 255 | *.plg 256 | 257 | # Visual Studio 6 workspace options file 258 | *.opt 259 | 260 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 261 | *.vbw 262 | 263 | # Visual Studio LightSwitch build output 264 | **/*.HTMLClient/GeneratedArtifacts 265 | **/*.DesktopClient/GeneratedArtifacts 266 | **/*.DesktopClient/ModelManifest.xml 267 | **/*.Server/GeneratedArtifacts 268 | **/*.Server/ModelManifest.xml 269 | _Pvt_Extensions 270 | 271 | # Paket dependency manager 272 | .paket/paket.exe 273 | paket-files/ 274 | 275 | # FAKE - F# Make 276 | .fake/ 277 | 278 | # JetBrains Rider 279 | .idea/ 280 | *.sln.iml 281 | 282 | # CodeRush 283 | .cr/ 284 | 285 | # Python Tools for Visual Studio (PTVS) 286 | __pycache__/ 287 | *.pyc 288 | 289 | # Cake - Uncomment if you are using it 290 | # tools/** 291 | # !tools/packages.config 292 | 293 | # Tabs Studio 294 | *.tss 295 | 296 | # Telerik's JustMock configuration file 297 | *.jmconfig 298 | 299 | # BizTalk build output 300 | *.btp.cs 301 | *.btm.cs 302 | *.odx.cs 303 | *.xsd.cs 304 | 305 | # OpenCover UI analysis results 306 | OpenCover/ -------------------------------------------------------------------------------- /WorkerThread.cpp: -------------------------------------------------------------------------------- 1 | #include "WorkerThread.h" 2 | #include "Fault.h" 3 | #include 4 | 5 | #ifdef WIN32 6 | #include 7 | #endif 8 | 9 | using namespace std; 10 | 11 | #define MSG_EXIT_THREAD 1 12 | #define MSG_POST_USER_DATA 2 13 | #define MSG_TIMER 3 14 | 15 | struct ThreadMsg 16 | { 17 | ThreadMsg(int i, std::shared_ptr m) { id = i; msg = m; } 18 | int id; 19 | std::shared_ptr msg; 20 | }; 21 | 22 | //---------------------------------------------------------------------------- 23 | // WorkerThread 24 | //---------------------------------------------------------------------------- 25 | WorkerThread::WorkerThread(const std::string& threadName) : 26 | m_thread(nullptr), 27 | m_exit(false), 28 | m_timerExit(false), 29 | THREAD_NAME(threadName) 30 | { 31 | } 32 | 33 | //---------------------------------------------------------------------------- 34 | // ~WorkerThread 35 | //---------------------------------------------------------------------------- 36 | WorkerThread::~WorkerThread() 37 | { 38 | ExitThread(); 39 | } 40 | 41 | //---------------------------------------------------------------------------- 42 | // CreateThread 43 | //---------------------------------------------------------------------------- 44 | bool WorkerThread::CreateThread() 45 | { 46 | if (!m_thread) 47 | { 48 | m_threadStartFuture = m_threadStartPromise.get_future(); 49 | 50 | m_thread = std::unique_ptr(new thread(&WorkerThread::Process, this)); 51 | 52 | auto handle = m_thread->native_handle(); 53 | SetThreadName(handle, THREAD_NAME); 54 | 55 | // Wait for the thread to enter the Process method 56 | m_threadStartFuture.get(); 57 | } 58 | 59 | return true; 60 | } 61 | 62 | //---------------------------------------------------------------------------- 63 | // GetThreadId 64 | //---------------------------------------------------------------------------- 65 | std::thread::id WorkerThread::GetThreadId() 66 | { 67 | ASSERT_TRUE(m_thread != nullptr); 68 | return m_thread->get_id(); 69 | } 70 | 71 | //---------------------------------------------------------------------------- 72 | // GetCurrentThreadId 73 | //---------------------------------------------------------------------------- 74 | std::thread::id WorkerThread::GetCurrentThreadId() 75 | { 76 | return this_thread::get_id(); 77 | } 78 | 79 | //---------------------------------------------------------------------------- 80 | // GetQueueSize 81 | //---------------------------------------------------------------------------- 82 | size_t WorkerThread::GetQueueSize() 83 | { 84 | lock_guard lock(m_mutex); 85 | return m_queue.size(); 86 | } 87 | 88 | //---------------------------------------------------------------------------- 89 | // SetThreadName 90 | //---------------------------------------------------------------------------- 91 | void WorkerThread::SetThreadName(std::thread::native_handle_type handle, const std::string& name) 92 | { 93 | #ifdef WIN32 94 | // Set the thread name so it shows in the Visual Studio Debug Location toolbar 95 | std::wstring wstr(name.begin(), name.end()); 96 | HRESULT hr = SetThreadDescription(handle, wstr.c_str()); 97 | if (FAILED(hr)) 98 | { 99 | // Handle error if needed 100 | } 101 | #endif 102 | } 103 | 104 | //---------------------------------------------------------------------------- 105 | // ExitThread 106 | //---------------------------------------------------------------------------- 107 | void WorkerThread::ExitThread() 108 | { 109 | if (!m_thread) 110 | return; 111 | 112 | // Create a new ThreadMsg 113 | std::shared_ptr threadMsg(new ThreadMsg(MSG_EXIT_THREAD, 0)); 114 | 115 | // Put exit thread message into the queue 116 | { 117 | lock_guard lock(m_mutex); 118 | m_queue.push(threadMsg); 119 | m_cv.notify_one(); 120 | } 121 | 122 | m_exit.store(true); 123 | m_thread->join(); 124 | 125 | // Clear the queue if anything added while waiting for join 126 | { 127 | lock_guard lock(m_mutex); 128 | m_thread = nullptr; 129 | while (!m_queue.empty()) 130 | m_queue.pop(); 131 | } 132 | } 133 | 134 | //---------------------------------------------------------------------------- 135 | // PostMsg 136 | //---------------------------------------------------------------------------- 137 | void WorkerThread::PostMsg(std::shared_ptr data) 138 | { 139 | if (m_exit.load()) 140 | return; 141 | ASSERT_TRUE(m_thread); 142 | 143 | // Create a new ThreadMsg 144 | std::shared_ptr threadMsg(new ThreadMsg(MSG_POST_USER_DATA, data)); 145 | 146 | // Add user data msg to queue and notify worker thread 147 | std::unique_lock lk(m_mutex); 148 | m_queue.push(threadMsg); 149 | m_cv.notify_one(); 150 | } 151 | 152 | //---------------------------------------------------------------------------- 153 | // TimerThread 154 | //---------------------------------------------------------------------------- 155 | void WorkerThread::TimerThread() 156 | { 157 | while (!m_timerExit) 158 | { 159 | // Sleep for 250mS then put a MSG_TIMER into the message queue 160 | std::this_thread::sleep_for(std::chrono::milliseconds(250)); 161 | 162 | std::shared_ptr threadMsg (new ThreadMsg(MSG_TIMER, 0)); 163 | 164 | // Add timer msg to queue and notify worker thread 165 | std::unique_lock lk(m_mutex); 166 | m_queue.push(threadMsg); 167 | m_cv.notify_one(); 168 | } 169 | } 170 | 171 | //---------------------------------------------------------------------------- 172 | // Process 173 | //---------------------------------------------------------------------------- 174 | void WorkerThread::Process() 175 | { 176 | // Signal that the thread has started processing to notify CreateThread 177 | m_threadStartPromise.set_value(); 178 | 179 | m_timerExit = false; 180 | std::thread timerThread(&WorkerThread::TimerThread, this); 181 | 182 | while (1) 183 | { 184 | std::shared_ptr msg; 185 | { 186 | // Wait for a message to be added to the queue 187 | std::unique_lock lk(m_mutex); 188 | while (m_queue.empty()) 189 | m_cv.wait(lk); 190 | 191 | if (m_queue.empty()) 192 | continue; 193 | 194 | msg = m_queue.front(); 195 | m_queue.pop(); 196 | } 197 | 198 | switch (msg->id) 199 | { 200 | case MSG_POST_USER_DATA: 201 | { 202 | ASSERT_TRUE(msg->msg != NULL); 203 | 204 | auto userData = std::static_pointer_cast(msg->msg); 205 | cout << userData->msg.c_str() << " " << userData->year << " on " << THREAD_NAME << endl; 206 | 207 | break; 208 | } 209 | 210 | case MSG_TIMER: 211 | cout << "Timer expired on " << THREAD_NAME << endl; 212 | break; 213 | 214 | case MSG_EXIT_THREAD: 215 | { 216 | m_timerExit = true; 217 | timerThread.join(); 218 | return; 219 | } 220 | 221 | default: 222 | ASSERT(); 223 | } 224 | } 225 | } 226 | 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![License MIT](https://img.shields.io/github/license/BehaviorTree/BehaviorTree.CPP?color=blue) 2 | [![conan Ubuntu](https://github.com/endurodave/StdWorkerThread/actions/workflows/cmake_ubuntu.yml/badge.svg)](https://github.com/endurodave/StdWorkerThread/actions/workflows/cmake_ubuntu.yml) 3 | [![conan Ubuntu](https://github.com/endurodave/StdWorkerThread/actions/workflows/cmake_clang.yml/badge.svg)](https://github.com/endurodave/StdWorkerThread/actions/workflows/cmake_clang.yml) 4 | [![conan Windows](https://github.com/endurodave/StdWorkerThread/actions/workflows/cmake_windows.yml/badge.svg)](https://github.com/endurodave/StdWorkerThread/actions/workflows/cmake_windows.yml) 5 | 6 | # C++ std::thread Event Loop with Message Queue and Timer 7 | 8 | Create a worker thread with an event loop, message queue and a timer using the C++11 thread support library. 9 | 10 | # Table of Contents 11 | 12 | - [C++ std::thread Event Loop with Message Queue and Timer](#c-stdthread-event-loop-with-message-queue-and-timer) 13 | - [Table of Contents](#table-of-contents) 14 | - [Preface](#preface) 15 | - [Getting Started](#getting-started) 16 | - [Introduction](#introduction) 17 | - [Background](#background) 18 | - [WorkerThread](#workerthread) 19 | - [Event Loop](#event-loop) 20 | - [Event Loop (Win32)](#event-loop-win32) 21 | - [Timer](#timer) 22 | - [Usage](#usage) 23 | - [Related Repositories](#related-repositories) 24 | - [Conclusion](#conclusion) 25 | 26 | # Preface 27 | 28 | Originally published on CodeProject at: C++ std::thread Event Loop with Message Queue and Timer 29 | 30 | # Getting Started 31 | 32 | [CMake](https://cmake.org/) is used to create the project build files on any Windows or Linux machine. The source code works on any C++ compiler with `std::thread` support. 33 | 34 | 1. Clone the repository. 35 | 2. From the repository root, run the following CMake command: 36 | `cmake -B Build .` 37 | 3. Build and run the project within the `Build` directory. 38 | 39 | # Introduction 40 | 41 |

An event loop, or sometimes called a message loop, is a thread that waits for and dispatches incoming events. The thread blocks waiting for requests to arrive and then dispatches the event to an event handler function. A message queue is typically used by the loop to hold incoming messages. Each message is sequentially dequeued, decoded, and then an action is performed. Event loops are one way to implement inter-process communication.

42 | 43 |

All operating systems provide support for multi-threaded applications. Each OS has unique function calls for creating threads, message queues and timers. With the advent of the C++11 thread support library, it’s now possible to create portable code and avoid the OS-specific function calls. This article provides a simple example of how to create a thread event loop, message queue and timer services while only relying upon the C++ Standard Library. Any C++11 compiler supporting the thread library should be able to compile the attached source.

44 | 45 | # Background 46 | 47 |

Typically, I need a thread to operate as an event loop. Incoming messages are dequeued by the thread and data is dispatched to an appropriate function handler based on a unique message identifier. Timer support capable of invoking a function is handy for low speed polling or to generate a timeout if something doesn’t happen in the expected amount of time. Many times, the worker thread is created at startup and isn’t destroyed until the application terminates.

48 | 49 |

A key requirement for the implementation is that the incoming messages must execute on the same thread instance. Whereas say std::async may use a temporary thread from a pool, this class ensures that all incoming messages use the same thread. For instance, a subsystem could be implemented with code that is not thread-safe. A single WorkerThread instance is used to safely dispatch function calls into the subsystem.

50 | 51 |

At first glance, the C++ thread support seems to be missing some key features. Yes, std::thread is available to spin off a thread but there is no thread-safe queue and no timers – services that most OS’s provide. I’ll show how to use the C++ Standard Library to create these “missing” features and provide an event processing loop familiar to many programmers.

52 | 53 | # WorkerThread 54 | 55 |

The WorkerThread class encapsulates all the necessary event loop mechanisms. A simple class interface allows thread creation, posting messages to the event loop, and eventual thread termination. The interface is shown below:

56 | 57 |
 58 | class WorkerThread
 59 | {
 60 | public:
 61 |     /// Constructor
 62 |     WorkerThread(const char* threadName);
 63 | 
 64 |     /// Destructor
 65 |     ~WorkerThread();
 66 | 
 67 |     /// Called once to create the worker thread
 68 |     /// @return True if thread is created. False otherwise. 
 69 |     bool CreateThread();
 70 | 
 71 |     /// Called once a program exit to exit the worker thread
 72 |     void ExitThread();
 73 | 
 74 |     /// Get the ID of this thread instance
 75 |     /// @return The worker thread ID
 76 |     std::thread::id GetThreadId();
 77 | 
 78 |     /// Get the ID of the currently executing thread
 79 |     /// @return The current thread ID
 80 |     static std::thread::id GetCurrentThreadId();
 81 | 
 82 |     /// Add a message to the thread queue
 83 |     /// @param[in] data - thread specific message information
 84 |     void PostMsg(std::shared_ptr<UserData> msg);
 85 | 
 86 | private:
 87 |     WorkerThread(const WorkerThread&) = delete;
 88 |     WorkerThread& operator=(const WorkerThread&) = delete;
 89 | 
 90 |     /// Entry point for the worker thread
 91 |     void Process();
 92 | 
 93 |     /// Entry point for timer thread
 94 |     void TimerThread();
 95 | 
 96 |     std::unique_ptr<std::thread> m_thread;
 97 |     std::queue<std::shared_ptr<ThreadMsg>> m_queue;
 98 |     std::mutex m_mutex;
 99 |     std::condition_variable m_cv;
100 |     std::atomic<bool> m_timerExit;
101 |     const char* THREAD_NAME;
102 | };
103 | 104 |

The first thing to notice is that std::thread is used to create a main worker thread. The main worker thread function is Process().

105 | 106 |
107 | bool WorkerThread::CreateThread()
108 | {
109 |     if (!m_thread)
110 |         m_thread = new thread(&WorkerThread::Process, this);
111 |     return true;
112 | }
113 | 114 | # Event Loop 115 | 116 |

The Process() event loop is shown below. The thread relies upon a std::queue<ThreadMsg*> for the message queue. std::queue is not thread-safe so all access to the queue must be protected by mutex. A std::condition_variable is used to suspend the thread until notified that a new message has been added to the queue.

117 | 118 |
119 | void WorkerThread::Process()
120 | {
121 |     m_timerExit = false;
122 |     std::thread timerThread(&WorkerThread::TimerThread, this);
123 | 
124 |     while (1)
125 |     {
126 |         std::shared_ptr<ThreadMsg> msg;
127 |         {
128 |             // Wait for a message to be added to the queue
129 |             std::unique_lock<std::mutex> lk(m_mutex);
130 |             while (m_queue.empty())
131 |                 m_cv.wait(lk);
132 | 
133 |             if (m_queue.empty())
134 |                 continue;
135 | 
136 |             msg = m_queue.front();
137 |             m_queue.pop();
138 |         }
139 | 
140 |         switch (msg->id)
141 |         {
142 |             case MSG_POST_USER_DATA:
143 |             {
144 |                 ASSERT_TRUE(msg->msg != NULL);
145 | 
146 |                 auto userData = std::static_pointer_cast<UserData>(msg->msg);
147 |                 cout << userData->msg.c_str() << " " << userData->year << " on " << THREAD_NAME << endl;
148 | 
149 |                 break;
150 |             }
151 | 
152 |             case MSG_TIMER:
153 |                 cout << "Timer expired on " << THREAD_NAME << endl;
154 |                 break;
155 | 
156 |             case MSG_EXIT_THREAD:
157 |             {
158 |                 m_timerExit = true;
159 |                 timerThread.join();
160 |                 return;
161 |             }
162 | 
163 |             default:
164 |                 ASSERT();
165 |         }
166 |     }
167 | }
168 | 169 |

PostMsg() creates a new ThreadMsg on the heap, adds the message to the queue, and then notifies the worker thread using a condition variable.

170 | 171 |
172 | void WorkerThread::PostMsg(std::shared_ptr<UserData> data)
173 | {
174 |     ASSERT_TRUE(m_thread);
175 | 
176 |     // Create a new ThreadMsg
177 |     std::shared_ptr<ThreadMsg> threadMsg(new ThreadMsg(MSG_POST_USER_DATA, data));
178 | 
179 |     // Add user data msg to queue and notify worker thread
180 |     std::unique_lock<std::mutex> lk(m_mutex);
181 |     m_queue.push(threadMsg);
182 |     m_cv.notify_one();
183 | }
184 | 185 |

The loop will continue to process messages until the MSG_EXIT_THREAD is received and the thread exits.

186 | 187 |
188 | void WorkerThread::ExitThread()
189 | {
190 |     if (!m_thread)
191 |         return;
192 | 
193 |     // Create a new ThreadMsg
194 |     std::shared_ptr<ThreadMsg> threadMsg(new ThreadMsg(MSG_EXIT_THREAD, 0));
195 | 
196 |     // Put exit thread message into the queue
197 |     {
198 |         lock_guard<mutex> lock(m_mutex);
199 |         m_queue.push(threadMsg);
200 |         m_cv.notify_one();
201 |     }
202 | 
203 |     m_thread->join();
204 |     m_thread = nullptr;
205 | }
206 | 207 | ## Event Loop (Win32) 208 | 209 |

The code snippet below contrasts the std::thread event loop above with a similar Win32 version using the Windows API. Notice GetMessage() API is used in lieu of the std::queue. Messages are posted to the OS message queue using PostThreadMessage(). And finally, timerSetEvent() is used to place WM_USER_TIMER messages into the queue. All of these services are provided by the OS. The std::thread WorkerThread implementation presented here avoids the raw OS calls yet the implementation functionality is the same as the Win32 version while relying only upon only the C++ Standard Library.

210 | 211 |
212 | unsigned long WorkerThread::Process(void* parameter)
213 | {
214 |     MSG msg;
215 |     BOOL bRet;
216 | 
217 |     // Start periodic timer
218 |     MMRESULT timerId = timeSetEvent(250, 10, &WorkerThread::TimerExpired, 
219 |                        reinterpret_cast<DWORD>(this), TIME_PERIODIC);
220 | 
221 |     while ((bRet = GetMessage(&msg, NULL, WM_USER_BEGIN, WM_USER_END)) != 0)
222 |     {
223 |         switch (msg.message)
224 |         {
225 |             case WM_DISPATCH_DELEGATE:
226 |             {
227 |                 ASSERT_TRUE(msg.wParam != NULL);
228 | 
229 |                 // Convert the ThreadMsg void* data back to a UserData*
230 |                 const UserData* userData = static_cast<const UserData*>(msg.wParam);
231 | 
232 |                 cout << userData->msg.c_str() << " " << userData->year << " on " << THREAD_NAME << endl;
233 | 
234 |                 // Delete dynamic data passed through message queue
235 |                 delete userData;
236 |                 break;
237 |             }
238 | 
239 |             case WM_USER_TIMER:
240 |                 cout << "Timer expired on " << THREAD_NAME << endl;
241 |                 break;
242 | 
243 |             case WM_EXIT_THREAD:
244 |                 timeKillEvent(timerId);
245 |                 return 0;
246 | 
247 |             default:
248 |                 ASSERT();
249 |         }
250 |     }
251 |     return 0;
252 | }
253 | 254 | # Timer 255 | 256 |

A low-resolution periodic timer message is inserted into the queue using a secondary private thread. The timer thread is created inside Process().

257 | 258 |
259 | void WorkerThread::Process()
260 | {
261 |     m_timerExit = false;
262 |     std::thread timerThread(&WorkerThread::TimerThread, this);
263 | 
264 | ...
265 | 266 |

The timer thread’s sole responsibility is to insert a MSG_TIMER message every 250ms. In this implementation, there’s no protection against the timer thread injecting more than one timer message into the queue. This could happen if the worker thread falls behind and can’t service the message queue fast enough. Depending on the worker thread, processing load, and how fast the timer messages are inserted, additional logic could be employed to prevent flooding the queue.

267 | 268 |
269 | void WorkerThread::TimerThread()
270 | {
271 |     while (!m_timerExit)
272 |     {
273 |         // Sleep for 250mS then put a MSG_TIMER into the message queue
274 |         std::this_thread::sleep_for(250ms);
275 | 
276 |         std::shared_ptr<ThreadMsg> threadMsg (new ThreadMsg(MSG_TIMER, 0));
277 | 
278 |         // Add timer msg to queue and notify worker thread
279 |         std::unique_lock<std::mutex> lk(m_mutex);
280 |         m_queue.push(threadMsg);
281 |         m_cv.notify_one();
282 |     }
283 | }
284 | 285 | # Usage 286 | 287 |

The main() function below shows how to use the WorkerThread class. Two worker threads are created and a message is posted to each one. After a short delay, both threads exit.

288 | 289 |
290 | // Worker thread instances
291 | WorkerThread workerThread1("WorkerThread1");
292 | WorkerThread workerThread2("WorkerThread2");
293 | 
294 | int main(void)
295 | {    
296 |     // Create worker threads
297 |     workerThread1.CreateThread();
298 |     workerThread2.CreateThread();
299 | 
300 |     // Create message to send to worker thread 1
301 |     std::shared_ptr<UserData> userData1(new UserData());
302 |     userData1->msg = "Hello world";
303 |     userData1->year = 2017;
304 | 
305 |     // Post the message to worker thread 1
306 |     workerThread1.PostMsg(userData1);
307 | 
308 |     // Create message to send to worker thread 2
309 |     std::shared_ptr<UserData> userData2(new UserData());
310 |     userData2->msg = "Goodbye world";
311 |     userData2->year = 2017;
312 | 
313 |     // Post the message to worker thread 2
314 |     workerThread2.PostMsg(userData2);
315 | 
316 |     // Give time for messages processing on worker threads
317 |     this_thread::sleep_for(1s);
318 | 
319 |     workerThread1.ExitThread();
320 |     workerThread2.ExitThread();
321 | 
322 |     return 0;
323 | }
324 | 325 | # Related Repositories 326 | 327 | Real-world projects using `WorkerThread`. 328 | 329 | * Asynchronous Delegates in C++ - Invoke any C++ callable function synchronously, asynchronously, or on a remote endpoint. 330 | * C++ State Machine with Threads - A framework combining C++ state machines and multicast asynchronous callbacks. 331 | * C++ State Machine with Asynchronous Delegates - A framework combining C++ state machines and asynchronous delegate callbacks. 332 | * Asynchronous State Machine Design in C++ - An asynchronous C++ state machine implemented using an asynchronous delegate library. 333 | * Integration Test Framework using Google Test and Delegates - A multi-threaded C++ software integration test framework using Google Test and DelegateMQ libraries. 334 | * Asynchronous SQLite API using C++ Delegates - An asynchronous SQLite wrapper implemented using an asynchronous delegate library. 335 | 336 | # Conclusion 337 | 338 |

The C++ thread support library offers a platform independent way to write multi-threaded application code without reliance upon OS-specific API’s. The WorkerThread class presented here is a bare-bones implementation of an event loop, yet all the basics are there ready to be expanded upon.

339 | 340 | --------------------------------------------------------------------------------