├── .github └── workflows │ └── test.yaml ├── .gitignore ├── README.md ├── gems.rb ├── source └── Concurrent │ ├── Coentry.hpp │ ├── Condition.cpp │ ├── Condition.hpp │ ├── Fiber.cpp │ ├── Fiber.hpp │ ├── Stack.cpp │ └── Stack.hpp ├── teapot.rb └── test └── Concurrent ├── Condition.cpp ├── Fiber.cpp └── Stack.cpp /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CONSOLE_OUTPUT: XTerm 7 | 8 | jobs: 9 | test: 10 | name: ${{matrix.ruby}} on ${{matrix.os}} 11 | runs-on: ${{matrix.os}}-latest 12 | 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu 17 | - macos 18 | 19 | ruby: 20 | - "3.2" 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{matrix.ruby}} 27 | bundler-cache: true 28 | 29 | - uses: kurocha/setup-cpp@master 30 | 31 | - name: Run tests 32 | timeout-minutes: 5 33 | run: | 34 | bundle exec teapot fetch 35 | bundle exec teapot build Test/Concurrent 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | teapot/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Concurrent 2 | 3 | Provides basic concurrency primitives, including stackful coroutines. 4 | 5 | ## Motivation 6 | 7 | Coroutines are [a negative overhead abstraction](https://www.youtube.com/watch?v=_fu0gx-xseY). They allow for efficient, expressive code which is superior to manual state tracking. Normal flow control is not disturbed by structures required for concurrent execution. 8 | 9 | This minimises the cognitive overhead of dealing with both execution logic and asynchronous logic at the same time. Compare the following: 10 | 11 | ```c++ 12 | // According to N4399 Working Draft 13 | future do_while(std::function()> body) { 14 | return body().then([=](future notDone) { 15 | return notDone.get() ? do_while(body) : make_ready_future(); 16 | }); 17 | } 18 | 19 | future tcp_reader(int64_t total) { 20 | struct State { 21 | char buf[4 * 1024]; 22 | int64_t total; 23 | Tcp::Connection conn; 24 | explicit State(int64_t total) : total(total) {} 25 | }; 26 | 27 | auto state = make_shared(total); 28 | 29 | return Tcp::Connect("127.0.0.1", 1337).then( 30 | [state](future conn) { 31 | state->conn = std::move(conn.get()); 32 | return do_while([state]()->future { 33 | if (state->total <= 0) 34 | return make_ready_future(false); 35 | 36 | return state->conn.read(state->buf, sizeof(state->buf)).then( 37 | [state](future nBytesFut) { 38 | auto nBytes = nBytesFut.get(); 39 | 40 | if (nBytes == 0) 41 | return make_ready_future(false); 42 | 43 | state->total -= nBytes; 44 | return make_ready_future(true); 45 | } 46 | ); 47 | }); 48 | } 49 | ).then([state](future) { 50 | return make_ready_future(state->total) 51 | }); 52 | } 53 | ``` 54 | 55 | with: 56 | 57 | ```c++ 58 | int tcp_reader(int total) 59 | { 60 | char buf[4 * 1024]; 61 | auto conn = Tcp::Connect("127.0.0.1", 1337); 62 | for (;;) { 63 | auto bytesRead = conn.Read(buf, sizeof(buf)); 64 | total -= bytesRead; 65 | 66 | if (total <= 0 || bytesRead == 0) 67 | return total; 68 | } 69 | } 70 | ``` 71 | 72 | Not only is this simpler, it's also faster (better throughput). You can implement code like this using an [event-driven reactor](https://github.com/kurocha/async). 73 | 74 | ### Useful Definitions 75 | 76 | - **Parallel** programs distribute their tasks to multiple processors, that actively work on all of them simultaneously. 77 | - **Concurrent** programs handle tasks that are all in progress at the same time, but it is only necessary to work briefly and separately on each task, so the work can be interleaved in whatever order the tasks require. 78 | - **Asynchronous**: programs dispatch tasks to devices that can take care of themselves, leaving the program free do something else until it receives a signal that the results are available. 79 | 80 | Thanks to Jan Christian Meyer, Ph.D. in Computer Science, for these concise definitions. 81 | 82 | ## Setup 83 | 84 | Firstly the build tool `teapot` needs to be installed (which requires [Ruby][2]): 85 | 86 | $ gem install teapot 87 | 88 | To fetch all dependencies, run: 89 | 90 | $ teapot fetch 91 | 92 | [2]: http://www.ruby-lang.org/en/downloads/ 93 | 94 | ## Usage 95 | 96 | To run unit tests: 97 | 98 | $ teapot Test/Concurrent 99 | 100 | ### Fibers 101 | 102 | `Concurrent::Fiber` provides cooperative multi-tasking. 103 | 104 | ```c++ 105 | int x = 10; 106 | 107 | Fiber fiber([&]{ 108 | x = 20; 109 | Fiber::current->yield(); 110 | x = 30; 111 | }); 112 | 113 | fiber.resume(); 114 | // x is now 20. 115 | fiber.resume(); 116 | // x is now 30. 117 | ``` 118 | 119 | The implementation uses `Concurrent::Stack` to allocate (`mmap`) a stack, in which the given lambda is allocated using `Concurrent::Coentry`. 120 | 121 | The stack includes guard pages to protect against stack overflow. 122 | 123 | There is a `Concurrent::Condition` primitive which allows synchronisation between fibers. 124 | 125 | #### Fiber Pool 126 | 127 | If you have a server which is allocating a fiber per request, use a `Concurrent::Fiber::Pool`. This reuses stacks to minimse per-request overhead. 128 | 129 | ```c++ 130 | Concurrent::Fiber::Pool pool; 131 | 132 | // Server accept loop: 133 | while (...) { 134 | pool.resume([&]{ 135 | // Per-request work... 136 | }); 137 | } 138 | ``` 139 | 140 | ### Distributor 141 | 142 | `Concurrent::Distributor` provides a multi-threaded work queue. 143 | 144 | ```c++ 145 | Distributor> distributor; 146 | 147 | distributor([&]{ 148 | do_work(); 149 | }); 150 | ``` 151 | 152 | A distributor schedules work over available hardware processors. It is useful for implementing a job queue. 153 | 154 | ## Contributing 155 | 156 | 1. Fork it. 157 | 2. Create your feature branch (`git checkout -b my-new-feature`). 158 | 3. Commit your changes (`git commit -am 'Add some feature'`). 159 | 4. Push to the branch (`git push origin my-new-feature`). 160 | 5. Create new Pull Request. 161 | 162 | ## License 163 | 164 | Released under the MIT license. 165 | 166 | Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 167 | 168 | Permission is hereby granted, free of charge, to any person obtaining a copy 169 | of this software and associated documentation files (the "Software"), to deal 170 | in the Software without restriction, including without limitation the rights 171 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 172 | copies of the Software, and to permit persons to whom the Software is 173 | furnished to do so, subject to the following conditions: 174 | 175 | The above copyright notice and this permission notice shall be included in 176 | all copies or substantial portions of the Software. 177 | 178 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 179 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 180 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 181 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 182 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 183 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 184 | THE SOFTWARE. 185 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "teapot", "~> 3.5" 4 | gem "rugged", "~> 1.4.4" 5 | 6 | -------------------------------------------------------------------------------- /source/Concurrent/Coentry.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Coentry.hpp 3 | // File file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 4/7/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #pragma once 10 | 11 | #include 12 | #include "Stack.hpp" 13 | 14 | #include 15 | 16 | namespace Concurrent 17 | { 18 | template 19 | struct Coentry { 20 | FunctionT function; 21 | 22 | static COROUTINE cocall(CoroutineContext * from, CoroutineContext * self); 23 | Coentry(FunctionT && function_) : function(std::move(function_)) {} 24 | }; 25 | 26 | template 27 | Coentry make_coentry(FunctionT && function) 28 | { 29 | return Coentry(std::move(function)); 30 | } 31 | 32 | template 33 | Coentry * emplace_coentry(Stack & stack, FunctionT && function) 34 | { 35 | return stack.emplace>(std::move(function)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/Concurrent/Condition.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Condition.cpp 3 | // File file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 29/6/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #include "Condition.hpp" 10 | 11 | #include "Fiber.hpp" 12 | 13 | #include 14 | 15 | namespace Concurrent 16 | { 17 | Condition::Condition() 18 | { 19 | } 20 | 21 | Condition::~Condition() 22 | { 23 | // std::cerr << "Condition@" << this << "::~Condition _waiting=" << _waiting.size() << " _current=" << Fiber::current << std::endl; 24 | while (!_waiting.empty()) { 25 | auto fiber = _waiting.back(); 26 | _waiting.pop_back(); 27 | 28 | if (fiber) 29 | fiber->stop(); 30 | } 31 | } 32 | 33 | void Condition::wait() 34 | { 35 | // std::cerr << "Condition@" << this << "::wait _current=" << Fiber::current << std::endl; 36 | 37 | _waiting.push_back(Fiber::current); 38 | Fiber::current->yield(); 39 | } 40 | 41 | void Condition::resume() 42 | { 43 | while (!_waiting.empty()) { 44 | auto fiber = _waiting.back(); 45 | _waiting.pop_back(); 46 | 47 | if (fiber->status() != Status::FINISHED) 48 | fiber->resume(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/Concurrent/Condition.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Condition.hpp 3 | // File file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 29/6/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | namespace Concurrent 14 | { 15 | class Fiber; 16 | 17 | // A synchronization primative, which allows fibers to wait until a particular condition is triggered. 18 | class Condition 19 | { 20 | public: 21 | Condition(); 22 | 23 | // If a condition goes out of scope, all fibers waiting on it will be stopped. 24 | ~Condition(); 25 | 26 | Condition(const Condition & other) = delete; 27 | Condition & operator=(const Condition & other) = delete; 28 | 29 | void wait(); 30 | void resume(); 31 | 32 | std::size_t count() const noexcept {return _waiting.size();} 33 | 34 | private: 35 | std::vector _waiting; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /source/Concurrent/Fiber.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Fiber.cpp 3 | // File file is part of the "Memory" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 28/6/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #include "Fiber.hpp" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 16 | #include 17 | #endif 18 | 19 | namespace Concurrent 20 | { 21 | thread_local Fiber Fiber::main; 22 | thread_local Fiber * Fiber::current = &Fiber::main; 23 | thread_local std::size_t Fiber::level = 0; 24 | 25 | Fiber::Fiber() noexcept : _status(Status::MAIN), _annotation("main") 26 | { 27 | } 28 | 29 | Fiber::~Fiber() 30 | { 31 | // std::cerr << std::string(Fiber::level, '\t') << "-> ~Fiber " << _annotation << " with status " << (int)_status << std::endl; 32 | 33 | if (_status == Status::READY) { 34 | // Nothing to do here. 35 | } else if (_status == Status::RUNNING) { 36 | // Force fiber to stop. 37 | stop(); 38 | } else if (_status == Status::FINISHING) { 39 | // Still cleaning up... 40 | resume(); 41 | } 42 | 43 | // On exiting, we should log any uncaught exceptions, otherwise the fiber will silently exit even though it might have failed. 44 | if (_exception) { 45 | try { 46 | std::rethrow_exception(_exception); 47 | } 48 | catch (const std::exception & exception) { 49 | std::cerr << "Fiber '" << _annotation << "' exiting with unhandled exception: " << exception.what() << std::endl; 50 | } 51 | } 52 | 53 | // std::cerr << std::string(Fiber::level, '\t') << "<- ~Fiber " << _annotation << std::endl; 54 | } 55 | 56 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 57 | void Fiber::start_push_stack(std::string annotation) 58 | { 59 | // std::cerr << "Fiber::start_push_stack(" << annotation << ", " << _stack.base() << ", " << _stack.allocated_size() << ")" << std::endl; 60 | __sanitizer_start_switch_fiber(&_fake_stack, _stack.base(), _stack.allocated_size()); 61 | } 62 | 63 | void Fiber::finish_push_stack(std::string annotation) 64 | { 65 | __sanitizer_finish_switch_fiber(_fake_stack, &_from_stack_bottom, &_from_stack_size); 66 | // std::cerr << "Fiber::finish_push_stack(" << annotation << ", " << _from_stack_bottom << ", " << _from_stack_size << ")" << std::endl; 67 | } 68 | 69 | void Fiber::start_pop_stack(std::string annotation, bool terminating) 70 | { 71 | // std::cerr << "Fiber::start_pop_stack(" << annotation << ", " << _from_stack_bottom << ", " << _from_stack_size << ", " << terminating << ")" << std::endl; 72 | __sanitizer_start_switch_fiber(terminating ? nullptr : &_fake_stack, _from_stack_bottom, _from_stack_size); 73 | } 74 | 75 | void Fiber::finish_pop_stack(std::string annotation) 76 | { 77 | __sanitizer_finish_switch_fiber(_fake_stack, &_from_stack_bottom, &_from_stack_size); 78 | // std::cerr << "Fiber::finish_pop_stack(" << annotation << ", " << _from_stack_bottom << ", " << _from_stack_size << ")" << std::endl; 79 | } 80 | #endif 81 | 82 | void Fiber::resume() 83 | { 84 | // We cannot double-resume. 85 | assert(_caller == nullptr); 86 | 87 | _caller = Fiber::current; 88 | 89 | assert(_status != Status::FINISHED); 90 | 91 | Fiber::current = this; 92 | // std::cerr << std::string(Fiber::level, '\t') << _caller->_annotation << " resuming " << _annotation << std::endl; 93 | 94 | Fiber::level += 1; 95 | 96 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 97 | start_push_stack("resume"); 98 | #endif 99 | 100 | // Switch from the fiber that called this function to the fiber this object represents. 101 | coroutine_transfer(&_caller->_context, &_context); 102 | 103 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 104 | finish_pop_stack("resume"); 105 | #endif 106 | 107 | Fiber::level -= 1; 108 | 109 | // std::cerr << std::string(Fiber::level, '\t') << "resume back in " << _caller->_annotation << std::endl; 110 | 111 | Fiber::current = _caller; 112 | 113 | this->_caller = nullptr; 114 | 115 | // Once we yield back to the caller, if there was an exception, we rethrow it. 116 | if (_exception) { 117 | // Get a copy of the exception pointer: 118 | auto exception = _exception; 119 | 120 | // Clear the exception pointer so we don't rethrow it again: 121 | _exception = nullptr; 122 | 123 | // Throw the exception itself: 124 | std::rethrow_exception(exception); 125 | } 126 | } 127 | 128 | void Fiber::yield() 129 | { 130 | assert(_caller != nullptr); 131 | 132 | // std::cerr << std::string(Fiber::level, '\t') << _annotation << " yielding to " << _caller->_annotation << std::endl; 133 | 134 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 135 | start_pop_stack("yield"); 136 | #endif 137 | 138 | coroutine_transfer(&_context, &_caller->_context); 139 | 140 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 141 | finish_push_stack("yield"); 142 | #endif 143 | 144 | // std::cerr << std::string(Fiber::level, '\t') << "yield back to " << _annotation << std::endl; 145 | 146 | if (_status == Status::STOPPED) { 147 | throw Stop(); 148 | } 149 | } 150 | 151 | void Fiber::transfer() 152 | { 153 | // Transferring to ourselves is a no-op. 154 | if (Fiber::current == this) return; 155 | 156 | Fiber * current = Fiber::current; 157 | 158 | // std::cerr << std::string(Fiber::level, '\t') << "transfer from " << current->_annotation << " to " << _annotation << " with status " << (int)_status << std::endl; 159 | 160 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 161 | start_push_stack("transfer"); 162 | #endif 163 | 164 | Fiber::current = this; 165 | 166 | coroutine_transfer(¤t->_context, &_context); 167 | 168 | Fiber::current = current; 169 | 170 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 171 | finish_pop_stack("transfer"); 172 | #endif 173 | 174 | // std::cerr << std::string(Fiber::level, '\t') << "transfer back to " << current->_annotation << " with status " << (int)current->_status << std::endl; 175 | 176 | if (current->_status == Status::STOPPED) { 177 | throw Stop(); 178 | } 179 | } 180 | 181 | void Fiber::coreturn() 182 | { 183 | auto caller = _caller; 184 | 185 | // If there was no caller (i.e. transfer), return to the main fiber. 186 | if (caller == nullptr) { 187 | caller = &Fiber::main; 188 | } 189 | 190 | assert(caller != nullptr); 191 | 192 | // std::cerr << std::string(Fiber::level, '\t') << _annotation << " terminating to " << caller->_annotation << std::endl; 193 | 194 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 195 | start_pop_stack("coreturn", true); 196 | #endif 197 | 198 | coroutine_transfer(&_context, &caller->_context); 199 | 200 | std::terminate(); 201 | } 202 | 203 | void Fiber::wait() 204 | { 205 | // Cannot wait for own self to complete. 206 | assert(Fiber::current != this); 207 | 208 | _completion.wait(); 209 | } 210 | 211 | void Fiber::stop() 212 | { 213 | if (Fiber::current == this) { 214 | throw Stop(); 215 | } 216 | 217 | _status = Status::STOPPED; 218 | 219 | resume(); 220 | } 221 | 222 | Fiber::Context::Context() 223 | { 224 | this->stack_pointer = nullptr; 225 | } 226 | 227 | Fiber::Context::~Context() 228 | { 229 | this->stack_pointer = nullptr; 230 | } 231 | 232 | Fiber::Pool::Pool(std::size_t stack_size) : _stack_size(stack_size) 233 | { 234 | } 235 | 236 | Fiber::Pool::~Pool() 237 | { 238 | // std::cerr << "Fiber pool going out of scope with " << _fibers.size() << " fibers allocated" << std::endl; 239 | // 240 | // for (auto && fiber : _fibers) { 241 | // std::cerr << "\tFiber " << &fiber << " stack " << fiber.stack().top() << ": " << fiber.annotation() << " (" << (std::size_t)(fiber.status()) << ")" << std::endl; 242 | // } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /source/Concurrent/Fiber.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Fiber.hpp 3 | // File file is part of the "Memory" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 28/6/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include "Stack.hpp" 17 | #include "Condition.hpp" 18 | #include "Coentry.hpp" 19 | 20 | #include 21 | #include 22 | 23 | #if defined(__SANITIZE_ADDRESS__) 24 | #define CONCURRENT_SANITIZE_ADDRESS 25 | #elif defined(__has_feature) 26 | #if __has_feature(address_sanitizer) 27 | #define CONCURRENT_SANITIZE_ADDRESS 28 | #endif 29 | #endif 30 | 31 | namespace Concurrent 32 | { 33 | enum class Status 34 | { 35 | MAIN = 0, 36 | READY = 1, 37 | RUNNING = 2, 38 | STOPPED = 3, 39 | FINISHING = 4, 40 | FINISHED = 5 41 | }; 42 | 43 | class Stop {}; 44 | 45 | class Fiber 46 | { 47 | public: 48 | thread_local static Fiber main; 49 | thread_local static Fiber * current; 50 | thread_local static std::size_t level; 51 | 52 | bool transient = false; 53 | 54 | // TODO assess how much of a performance impact this has in the presence of virtual memory. Can it be bigger? Should it be smaller? 55 | static constexpr std::size_t DEFAULT_STACK_SIZE = 1024*1024*4; 56 | 57 | template 58 | Fiber(FunctionT && function, std::size_t stack_size = DEFAULT_STACK_SIZE) : _stack(stack_size), _context(_stack, function) 59 | { 60 | } 61 | 62 | template 63 | Fiber(std::string annotation, FunctionT && function, std::size_t stack_size = DEFAULT_STACK_SIZE) : _annotation(annotation), _stack(stack_size), _context(_stack, function) 64 | { 65 | } 66 | 67 | ~Fiber(); 68 | 69 | Fiber(const Fiber & other) = delete; 70 | Fiber & operator=(const Fiber & other) = delete; 71 | 72 | const Status & status() const noexcept {return _status;} 73 | explicit operator bool() const noexcept {return _status != Status::FINISHED;} 74 | 75 | /// Resume the function. 76 | void resume(); 77 | 78 | /// Yield back to the caller. 79 | void yield(); 80 | 81 | /// Transfer control to this fiber. 82 | void transfer(); 83 | 84 | /// Resumes the fiber, raising the Stop exception. 85 | void stop(); 86 | 87 | /// Next time the fiber is resumed, it will be stopped? 88 | void cancel() 89 | { 90 | _status = Status::STOPPED; 91 | } 92 | 93 | /// Yield the calling fiber until this fiber completes execution. 94 | void wait(); 95 | 96 | void annotate(const std::string & annotation) {_annotation = annotation;} 97 | 98 | const std::string & annotation() const {return _annotation;} 99 | Stack & stack() {return _stack;} 100 | 101 | private: 102 | class Context : public CoroutineContext 103 | { 104 | public: 105 | Context(); 106 | 107 | template 108 | Context(Stack & stack, FunctionT && function) 109 | { 110 | auto * coentry = emplace_coentry(stack, std::move(function)); 111 | 112 | coroutine_initialize(this, coentry->cocall, coentry, stack.current(), stack.size()); 113 | } 114 | 115 | ~Context(); 116 | }; 117 | 118 | Fiber() noexcept; 119 | [[noreturn]] static void coentry(void * arg); 120 | 121 | [[noreturn]] void coreturn(); 122 | 123 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 124 | void * _fake_stack = nullptr; 125 | const void * _from_stack_bottom = nullptr; 126 | std::size_t _from_stack_size = 0; 127 | 128 | void start_push_stack(std::string annotation); 129 | void finish_push_stack(std::string annotation); 130 | 131 | void start_pop_stack(std::string annotation, bool terminating = false); 132 | void finish_pop_stack(std::string annotation); 133 | #endif 134 | 135 | Status _status = Status::READY; 136 | std::string _annotation; 137 | 138 | Stack _stack; 139 | Context _context; 140 | 141 | std::exception_ptr _exception; 142 | 143 | Condition _completion; 144 | Fiber * _caller = nullptr; 145 | 146 | template 147 | friend struct Coentry; 148 | 149 | public: 150 | class Pool 151 | { 152 | public: 153 | Pool(std::size_t stack_size = DEFAULT_STACK_SIZE); 154 | ~Pool(); 155 | 156 | Pool(const Pool & other) = delete; 157 | Pool & operator=(const Pool & other) = delete; 158 | 159 | template 160 | Fiber & resume(FunctionT && function) 161 | { 162 | _fibers.emplace_back(function); 163 | 164 | auto & fiber = _fibers.back(); 165 | 166 | fiber.resume(); 167 | 168 | return fiber; 169 | } 170 | 171 | protected: 172 | std::size_t _stack_size = 0; 173 | 174 | std::list _stacks; 175 | std::list _fibers; 176 | }; 177 | }; 178 | 179 | template 180 | COROUTINE Coentry::cocall(CoroutineContext * from, CoroutineContext * self) 181 | { 182 | auto fiber = Fiber::current; 183 | auto * coentry = reinterpret_cast(self->argument); 184 | 185 | assert(fiber); 186 | assert(coentry); 187 | 188 | #if defined(CONCURRENT_SANITIZE_ADDRESS) 189 | fiber->finish_push_stack("cocall"); 190 | #endif 191 | 192 | try { 193 | fiber->_status = Status::RUNNING; 194 | coentry->function(); 195 | } catch (Stop) { 196 | // Ignore - not an actual error. 197 | } catch (...) { 198 | fiber->_exception = std::current_exception(); 199 | } 200 | 201 | fiber->_status = Status::FINISHING; 202 | // Notify other fibers that we've completed. 203 | fiber->_completion.resume(); 204 | fiber->_status = Status::FINISHED; 205 | 206 | // Going out of scope. 207 | coentry->~Coentry(); 208 | 209 | fiber->coreturn(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /source/Concurrent/Stack.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Stack.cpp 3 | // File file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 4/7/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #include "Stack.hpp" 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | 18 | namespace Concurrent 19 | { 20 | const std::size_t Stack::ALIGNMENT = 16; 21 | 22 | Stack::Stack(std::size_t size) 23 | { 24 | const std::size_t GUARD_PAGES = 1; 25 | static const std::size_t PAGE_SIZE = sysconf(_SC_PAGESIZE); 26 | 27 | std::size_t page_count = ((size+PAGE_SIZE) / PAGE_SIZE); 28 | auto stack_size = (GUARD_PAGES + page_count) * PAGE_SIZE; 29 | 30 | // The base allocation: 31 | _base = ::mmap(0, stack_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 32 | 33 | if (_base == MAP_FAILED) { 34 | _base = nullptr; 35 | 36 | throw std::system_error(errno, std::generic_category(), "mmap(...)"); 37 | } 38 | 39 | // The top of the stack: 40 | _top = (Byte*)_base + stack_size; 41 | 42 | // The current top of the stack, taking into account any emplacements: 43 | _current = _top; 44 | 45 | // Protect the bottom of the stack so we don't have silent stack overflow: 46 | ::mprotect(_base, GUARD_PAGES*PAGE_SIZE, PROT_NONE); 47 | _bottom = (Byte*)_base + GUARD_PAGES*PAGE_SIZE; 48 | } 49 | 50 | Stack::Stack() : _base(nullptr), _bottom(nullptr), _current(nullptr), _top(nullptr) 51 | { 52 | } 53 | 54 | Stack::~Stack() noexcept(false) 55 | { 56 | if (_base) { 57 | auto result = ::munmap(_base, (Byte*)_top - (Byte*)_base); 58 | 59 | if (result == -1) { 60 | throw std::system_error(errno, std::generic_category(), "munmap(...)"); 61 | } 62 | } 63 | } 64 | 65 | Stack::Stack(Stack && other) 66 | { 67 | _base = other._base; 68 | _bottom = other._bottom; 69 | _current = other._current; 70 | _top = other._top; 71 | 72 | other._base = nullptr; 73 | other._bottom = nullptr; 74 | other._current = nullptr; 75 | other._top = nullptr; 76 | } 77 | 78 | Stack & Stack::operator=(Stack && other) 79 | { 80 | if (_base) { 81 | ::munmap(_base, (Byte*)_top - (Byte*)_base); 82 | } 83 | 84 | _base = other._base; 85 | _bottom = other._bottom; 86 | _current = other._current; 87 | _top = other._top; 88 | 89 | other._base = nullptr; 90 | other._bottom = nullptr; 91 | other._current = nullptr; 92 | other._top = nullptr; 93 | 94 | return *this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /source/Concurrent/Stack.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Stack.hpp 3 | // File file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 4/7/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | namespace Concurrent 15 | { 16 | class Stack 17 | { 18 | typedef unsigned char Byte; 19 | 20 | public: 21 | Stack(std::size_t size); 22 | Stack(); 23 | 24 | Stack(const Stack & other) = delete; 25 | Stack & operator=(const Stack & other) = delete; 26 | 27 | Stack(Stack && other); 28 | Stack & operator=(Stack && other); 29 | 30 | ~Stack() noexcept(false); 31 | 32 | static const std::size_t ALIGNMENT; 33 | 34 | template 35 | Type* emplace(Args&&... args) 36 | { 37 | auto alignment = std::max(ALIGNMENT, alignof(Type)); 38 | 39 | // The stack grows from top towards bottom. 40 | void * next = (Byte*)_current - (sizeof(Type) + alignment); 41 | std::size_t space = (Byte*)_current - (Byte*)next; 42 | 43 | std::align(alignment, sizeof(Type), next, space); 44 | _current = next; 45 | 46 | return new(_current) Type(std::forward(args)...); 47 | } 48 | 49 | template 50 | Type * push(Type && value) 51 | { 52 | auto alignment = std::max(ALIGNMENT, alignof(Type)); 53 | 54 | // The stack grows from top towards bottom. 55 | void * next = (Byte*)_current - (sizeof(Type) + alignof(Type)); 56 | std::size_t space = (Byte*)_current - (Byte*)next; 57 | 58 | std::align(alignment, sizeof(Type), next, space); 59 | _current = next; 60 | 61 | return new(_current) Type(std::move(value)); 62 | }; 63 | 64 | // A pointer to the stack memory allocation. 65 | void * base() {return _base;} 66 | void * bottom() {return _bottom;} 67 | void * current() {return _current;} 68 | void * top() {return _top;} 69 | 70 | // The current available stack space: 71 | std::size_t size() {return (Byte*)_current - (Byte*)_bottom;} 72 | std::size_t allocated_size() {return (Byte*)top() - (Byte*)base();} 73 | 74 | private: 75 | void * _base, * _bottom, * _current, * _top; 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /teapot.rb: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This file is part of the "Teapot" project, and is released under the MIT license. 4 | # 5 | 6 | teapot_version "1.3" 7 | 8 | # Project Metadata 9 | 10 | define_project "concurrent" do |project| 11 | project.title = "Concurrent" 12 | 13 | project.summary = 'Primitives for concurrent execution.' 14 | 15 | project.license = "MIT License" 16 | 17 | project.add_author 'Samuel Williams', email: 'samuel.williams@oriontransfer.co.nz' 18 | 19 | project.version = "1.0.0" 20 | end 21 | 22 | # Build Targets 23 | 24 | define_target 'concurrent-library' do |target| 25 | target.depends "Language/C++14" 26 | 27 | target.depends "Library/Coroutine", public: true 28 | 29 | target.provides "Library/Concurrent" do 30 | source_root = target.package.path + 'source' 31 | 32 | library_path = build static_library: "Concurrent", source_files: source_root.glob('Concurrent/**/*.{cpp,c}') 33 | 34 | append linkflags library_path 35 | append header_search_paths source_root 36 | end 37 | end 38 | 39 | define_target "concurrent-tests" do |target| 40 | target.depends "Library/UnitTest" 41 | target.depends "Language/C++14" 42 | 43 | target.depends "Library/Concurrent" 44 | 45 | target.provides "Test/Concurrent" do |*arguments| 46 | test_root = target.package.path 47 | 48 | run source_files: test_root.glob('test/Concurrent/**/*.cpp'), arguments: arguments 49 | end 50 | end 51 | 52 | # Configurations 53 | 54 | define_configuration "development" do |configuration| 55 | configuration[:source] = "https://github.com/kurocha/" 56 | 57 | configuration.import "concurrent" 58 | 59 | # Provides all the build related infrastructure: 60 | configuration.require "platforms" 61 | configuration.require "build-files" 62 | 63 | # Provides unit testing infrastructure and generators: 64 | configuration.require "unit-test" 65 | 66 | # Provides some useful C++ generators: 67 | configuration.require 'generate-cpp-class' 68 | configuration.require 'generate-project' 69 | configuration.require 'generate-travis' 70 | end 71 | 72 | define_configuration "concurrent" do |configuration| 73 | configuration.public! 74 | 75 | configuration.require "coroutine" 76 | end 77 | -------------------------------------------------------------------------------- /test/Concurrent/Condition.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Test.Condition.cpp 3 | // This file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 29/6/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace Concurrent 15 | { 16 | UnitTest::Suite ConditionTestSuite { 17 | "Concurrent::Condition", 18 | 19 | {"it should wait until signalled", 20 | [](UnitTest::Examiner & examiner) { 21 | Condition condition; 22 | 23 | Fiber fiber("waiting", [&]{ 24 | condition.wait(); 25 | }); 26 | 27 | examiner.expect(condition.count()) == 0; 28 | 29 | fiber.resume(); 30 | 31 | examiner.expect(condition.count()) == 1; 32 | 33 | condition.resume(); 34 | 35 | examiner.expect(condition.count()) == 0; 36 | } 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /test/Concurrent/Fiber.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Test.Fiber.cpp 3 | // This file is part of the "Memory" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 28/6/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #include 10 | 11 | #include "Concurrent/Fiber.hpp" 12 | 13 | namespace Concurrent 14 | { 15 | using namespace UnitTest::Expectations; 16 | 17 | static std::ostream & operator<<(std::ostream & output, const Status & status) 18 | { 19 | if (status == Status::MAIN) { 20 | return output << "MAIN"; 21 | } else if (status == Status::READY) { 22 | return output << "READY"; 23 | } else if (status == Status::RUNNING) { 24 | return output << "RUNNING"; 25 | } else if (status == Status::STOPPED) { 26 | return output << "STOPPED"; 27 | } else if (status == Status::FINISHED) { 28 | return output << "FINISHED"; 29 | } else { 30 | return output << "???"; 31 | } 32 | } 33 | 34 | UnitTest::Suite FiberTestSuite { 35 | "Concurrent::Fiber", 36 | 37 | {"it should resume", 38 | [](UnitTest::Examiner & examiner) { 39 | int x = 10; 40 | 41 | Fiber fiber([&]{ 42 | x = 20; 43 | }); 44 | 45 | fiber.resume(); 46 | 47 | examiner.expect(x) == 20; 48 | } 49 | }, 50 | 51 | {"it should yield", 52 | [](UnitTest::Examiner & examiner) { 53 | int x = 10; 54 | 55 | Fiber fiber([&]{ 56 | x = 20; 57 | Fiber::current->yield(); 58 | x = 30; 59 | }); 60 | 61 | fiber.resume(); 62 | examiner.expect(x) == 20; 63 | 64 | fiber.resume(); 65 | examiner.expect(x) == 30; 66 | } 67 | }, 68 | 69 | {"it should throw exceptions", 70 | [](UnitTest::Examiner & examiner) { 71 | Fiber fiber([&]{ 72 | throw std::logic_error("your logic has failed me"); 73 | }); 74 | 75 | examiner.expect([&]{ 76 | fiber.resume(); 77 | }).to(throw_exception()); 78 | } 79 | }, 80 | 81 | {"it can be stopped", 82 | [](UnitTest::Examiner & examiner) { 83 | int count = 0; 84 | 85 | Fiber fiber([&]{ 86 | while (true) { 87 | count += 1; 88 | Fiber::current->yield(); 89 | } 90 | }); 91 | 92 | examiner.expect(fiber.status()) == Status::READY; 93 | 94 | fiber.resume(); 95 | 96 | examiner.expect(count) == 1; 97 | examiner.expect(fiber.status()) == Status::RUNNING; 98 | 99 | fiber.stop(); 100 | 101 | examiner.expect(fiber.status()) == Status::FINISHED; 102 | } 103 | }, 104 | 105 | {"it should resume in a nested fiber", 106 | [](UnitTest::Examiner & examiner) { 107 | std::string order; 108 | 109 | order += 'A'; 110 | 111 | Fiber outer([&]{ 112 | order += 'B'; 113 | 114 | Fiber inner([&]{ 115 | order += 'C'; 116 | }); 117 | 118 | order += 'D'; 119 | inner.resume(); 120 | order += 'E'; 121 | }); 122 | 123 | order += 'F'; 124 | outer.resume(); 125 | order += 'G'; 126 | 127 | examiner.expect(order) == "AFBDCEG"; 128 | } 129 | }, 130 | 131 | {"it can allocate fibers from a pool", 132 | [](UnitTest::Examiner & examiner) { 133 | std::string order; 134 | 135 | // A pool groups together fibers and will stop them once it goes out of scope. 136 | Fiber::Pool pool; 137 | 138 | std::size_t count = 0; 139 | for (std::size_t i = 0; i < 5; i += 1) { 140 | pool.resume([&]{ 141 | count += 1; 142 | }); 143 | } 144 | 145 | examiner.expect(count) == 5; 146 | } 147 | }, 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /test/Concurrent/Stack.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Test.Stack.cpp 3 | // This file is part of the "Concurrent" project and released under the MIT License. 4 | // 5 | // Created by Samuel Williams on 4/7/2017. 6 | // Copyright, 2017, by Samuel Williams. All rights reserved. 7 | // 8 | 9 | #include 10 | 11 | #include 12 | 13 | namespace Concurrent 14 | { 15 | struct Move 16 | { 17 | std::size_t value; 18 | 19 | Move() = default; 20 | Move(const Move & other) = delete; 21 | Move(Move && other) = default; 22 | }; 23 | 24 | UnitTest::Suite StackTestSuite { 25 | "Concurrent::Stack", 26 | 27 | {"should allocate a stack at least as big as requested", 28 | [](UnitTest::Examiner & examiner) { 29 | Stack stack(1024); 30 | 31 | examiner.expect(stack.size()) >= 1024; 32 | } 33 | }, 34 | 35 | {"can emplace an item on the stack", 36 | [](UnitTest::Examiner & examiner) { 37 | Stack stack(1024); 38 | 39 | stack.emplace(10); 40 | 41 | std::size_t * current = reinterpret_cast(stack.current()); 42 | 43 | examiner.expect(*current) == 10; 44 | } 45 | }, 46 | 47 | {"can push an item on the stack", 48 | [](UnitTest::Examiner & examiner) { 49 | Stack stack(1024); 50 | 51 | Move * current = stack.push(Move{10}); 52 | 53 | examiner.expect(current->value) == 10; 54 | } 55 | }, 56 | }; 57 | } 58 | --------------------------------------------------------------------------------