├── .gitattributes ├── .gitignore ├── .vscode └── c_cpp_properties.json ├── Awaitable.sln ├── Awaitable ├── Awaitable.cpp ├── Awaitable.h ├── Awaitable.vcxproj ├── Awaitable.vcxproj.filters ├── ReadMe.txt ├── stdafx.cpp ├── stdafx.h └── targetver.h └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [ 9 | "_DEBUG", 10 | "UNICODE", 11 | "_UNICODE" 12 | ], 13 | "windowsSdkVersion": "10.0.22621.0", 14 | "compilerPath": "cl.exe", 15 | "cStandard": "c17", 16 | "cppStandard": "c++20", 17 | "intelliSenseMode": "windows-msvc-x64" 18 | } 19 | ], 20 | "version": 4 21 | } -------------------------------------------------------------------------------- /Awaitable.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34622.214 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Awaitable", "AWaitable\Awaitable.vcxproj", "{1DB1C9BD-9762-4391-8A69-458B158F89AF}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Debug|x64.ActiveCfg = Debug|x64 17 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Debug|x64.Build.0 = Debug|x64 18 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Debug|x86.ActiveCfg = Debug|Win32 19 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Debug|x86.Build.0 = Debug|Win32 20 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Release|x64.ActiveCfg = Release|x64 21 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Release|x64.Build.0 = Release|x64 22 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Release|x86.ActiveCfg = Release|Win32 23 | {1DB1C9BD-9762-4391-8A69-458B158F89AF}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {3673C71C-D7D7-4DF6-87DD-D4EF0C29014C} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Awaitable/Awaitable.cpp: -------------------------------------------------------------------------------- 1 | // Awaitable.cpp : This file contains the 'main' function. Program execution begins and ends there. 2 | // 3 | 4 | #include 5 | 6 | // Awaitable.cpp : Defines the entry point for the console application. 7 | // 8 | 9 | #include "Awaitable.h" 10 | 11 | #include 12 | 13 | using namespace pi; 14 | 15 | co_void set_ready_after_timeout(awaitable a, std::chrono::high_resolution_clock::duration timeout) 16 | { 17 | co_await timeout; // timed wait 18 | 19 | a.set_ready(123); 20 | } 21 | 22 | co_void set_exception_after_timeout(awaitable a, std::chrono::high_resolution_clock::duration timeout) 23 | { 24 | co_await timeout; 25 | 26 | a.set_exception(std::make_exception_ptr(std::exception("set_exception_after_timeout"))); 27 | } 28 | 29 | awaitable named_counter(std::string name) 30 | { 31 | std::cout << "counter(" << name << ") resumed #" << 0 << std::endl; 32 | 33 | co_await 5s; // timed wait 34 | std::cout << "counter(" << name << ") resumed #" << 1 << std::endl; 35 | 36 | int i = co_await awaitable{}; // yield, returns the default value 37 | std::cout << "counter(" << name << ") resumed #" << 2 << " ### " << i << std::endl; 38 | 39 | { 40 | auto a = awaitable{ true }; // suspend, and returns the value from somewhere else 41 | set_ready_after_timeout(a, 3s); 42 | auto x = co_await a; 43 | std::cout << "counter(" << name << ") resumed #" << 3 << " ### " << x << std::endl; 44 | } 45 | 46 | try 47 | { 48 | auto a = awaitable{ true }; // suspend, and returns the value from somewhere else 49 | set_exception_after_timeout(a, 3s); 50 | auto x = co_await a; 51 | std::cout << "counter(" << name << ") resumed #" << 4 << " ### " << x << std::endl; 52 | } 53 | catch (std::exception e) 54 | { 55 | std::cout << "### caught exception: " << e.what() << std::endl; 56 | } 57 | 58 | co_return 42; 59 | } 60 | 61 | awaitable test_exception() 62 | { 63 | co_await 0s; 64 | 65 | throw 0; 66 | 67 | std::cout << "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl; 68 | } 69 | 70 | co_void test_multi_await(awaitable a, const std::string& name) 71 | { 72 | co_await a; 73 | 74 | std::cout << "### test_multi_await: " << name << std::endl; 75 | } 76 | 77 | co_void test() 78 | { 79 | try 80 | { 81 | co_await test_exception(); 82 | } 83 | catch (...) 84 | { 85 | std::cout << "caught exception" << std::endl; 86 | } 87 | 88 | { 89 | auto a = awaitable{ true }; 90 | set_ready_after_timeout(a, 3s); 91 | test_multi_await(a, "A"); 92 | test_multi_await(a, "B"); 93 | co_await a; 94 | std::cout << "### after 'co_await a' ### " << std::endl; 95 | } 96 | 97 | { 98 | auto a1 = awaitable{ 5s }; 99 | auto a2 = awaitable{ 4s }; 100 | std::deque> as{ a1, a2 }; 101 | auto ar = co_await awaitable::when_any(as); 102 | assert(ar == a2); 103 | std::cout << "co_await awaitable::when_any(as)" << std::endl; 104 | co_await(a1 && a2); 105 | } 106 | 107 | { 108 | // NB: it doesn't make sense to chain temporary awaitables with operator||, because the return value must be referencing one of the input awaitables 109 | auto a1 = awaitable{ 3s }; 110 | auto a2 = awaitable{ 4s }; 111 | auto a3 = awaitable{ 5s }; 112 | auto a4 = awaitable{ 6s }; 113 | { 114 | auto ar = co_await(a2 || (a3 || a1) || a4); 115 | assert(ar == a1); 116 | std::cout << "co_await (a1 || a2)" << std::endl; 117 | } 118 | } 119 | 120 | // NB: what happens if we do: co_await (a1 || a2 || a1 || a2)? 121 | 122 | { 123 | auto a1 = awaitable{ 3s }; 124 | auto a2 = awaitable{ 4s }; 125 | auto a3 = awaitable{ 5s }; 126 | co_await(a1 && a2 && a3); 127 | std::cout << "co_await (a1 && a2 && a3)" << std::endl; 128 | } 129 | 130 | { 131 | auto a1 = awaitable{ 5s }; 132 | auto a2 = awaitable{ 4s }; 133 | std::deque> as{ a1, a2 }; 134 | co_await awaitable::when_all(as); 135 | std::cout << "co_await awaitable::when_all(as)" << std::endl; 136 | } 137 | 138 | auto x = co_await named_counter("x"); 139 | std::cout << "### after co_await named_counter(x): " << x << std::endl; 140 | 141 | auto y = co_await named_counter("y"); 142 | std::cout << "### after co_await named_counter(y): " << y << std::endl; 143 | } 144 | 145 | co_void cancel_after_timeout(cancellation source, std::chrono::high_resolution_clock::duration timeout) 146 | { 147 | co_await timeout; 148 | 149 | source.fire(); 150 | } 151 | 152 | co_void test_cancellation_1(cancellation::token token) 153 | { 154 | auto a = awaitable{ true }; // suspend, and returns the value from somewhere else 155 | 156 | // NB: I rely on the fact that a stays on the stack, so I can capture it by reference 157 | token.register_action([&a] { a.set_exception(std::make_exception_ptr(std::exception("test_cancellation_1"))); }); 158 | 159 | try 160 | { 161 | auto x = co_await a; 162 | } 163 | catch (std::exception e) 164 | { 165 | std::cout << "test_cancellation_1: canceled!" << std::endl; 166 | } 167 | } 168 | 169 | co_void test_cancellation_2(cancellation::token token) 170 | { 171 | auto a = awaitable{ 4s }; 172 | 173 | token.register_action([&a] { a.set_exception(std::make_exception_ptr(std::exception("test_cancellation_2"))); }); 174 | 175 | try 176 | { 177 | co_await a; 178 | } 179 | catch (std::exception e) 180 | { 181 | std::cout << "test_cancellation_2: canceled!" << std::endl; 182 | } 183 | } 184 | 185 | void test_cancellation() 186 | { 187 | cancellation source; 188 | cancel_after_timeout(source, 3s); 189 | test_cancellation_1(source.get_token()); 190 | test_cancellation_2(source.get_token()); 191 | } 192 | 193 | co_void test_loop_1s() 194 | { 195 | int count = -1; 196 | while (count < 100) 197 | { 198 | co_await 1s; 199 | 200 | std::cout << std::format("test_loop_1s: {0}", ++count) << std::endl; 201 | } 202 | } 203 | 204 | co_void test_loop_2s() 205 | { 206 | int count = -1; 207 | while (count < 50) 208 | { 209 | co_await 2s; 210 | 211 | std::cout << std::format("test_loop_2s **: {0}", ++count) << std::endl; 212 | } 213 | } 214 | 215 | void run_tests() 216 | { 217 | // all the functions below will run concurrently (on the main thread) 218 | test_cancellation(); 219 | 220 | test(); 221 | 222 | test_loop_1s(); 223 | test_loop_2s(); 224 | } 225 | 226 | int main() 227 | { 228 | run_tests(); 229 | 230 | executor::singleton().loop(); 231 | return 0; 232 | } 233 | -------------------------------------------------------------------------------- /Awaitable/Awaitable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | using namespace std::chrono; 16 | 17 | namespace std 18 | { 19 | template<> 20 | struct hash> 21 | { 22 | size_t operator()(const std::coroutine_handle<>& ch) const 23 | { 24 | return hash()(ch.address()); 25 | } 26 | }; 27 | } 28 | 29 | namespace pi 30 | { 31 | class executor 32 | { 33 | public: 34 | static executor& singleton() 35 | { 36 | thread_local static executor s_singleton; 37 | return s_singleton; 38 | } 39 | 40 | void add_ready_coro(std::coroutine_handle<> coro) 41 | { 42 | _ready_coros.push(coro); 43 | } 44 | 45 | void add_timed_wait_coro(std::chrono::high_resolution_clock::time_point when, std::coroutine_handle<> coro) 46 | { 47 | auto r = _timed_wait_coros.emplace(when, std::pair>, std::unordered_map, std::list>::iterator>>{}); 48 | assert(r.first->second.first.size() == r.first->second.second.size()); 49 | r.first->second.first.emplace_back(coro); // NB: the same coroutine cannot be suspended multiple times! 50 | r.first->second.second.emplace(coro, --r.first->second.first.end()); 51 | assert(r.first->second.first.size() == r.first->second.second.size()); 52 | } 53 | 54 | void remove_timed_wait_coro(std::chrono::high_resolution_clock::time_point when, std::coroutine_handle<> coro) 55 | { 56 | auto it = _timed_wait_coros.find(when); 57 | if (it != _timed_wait_coros.end()) 58 | { 59 | assert(it->second.first.size() == it->second.second.size()); 60 | auto it2 = it->second.second.find(coro); 61 | if (it2 != it->second.second.end()) 62 | { 63 | it->second.first.erase(it2->second); 64 | it->second.second.erase(it2); 65 | assert(it->second.first.size() == it->second.second.size()); 66 | } 67 | if (it->second.first.empty()) 68 | { 69 | _timed_wait_coros.erase(it); 70 | } 71 | } 72 | } 73 | 74 | void increment_num_outstanding_coros() 75 | { 76 | ++_num_outstanding_coros; 77 | } 78 | 79 | void decrement_num_outstanding_coros() 80 | { 81 | --_num_outstanding_coros; 82 | } 83 | 84 | bool tick() 85 | { 86 | if (!_ready_coros.empty() || !_timed_wait_coros.empty() || _num_outstanding_coros > 0) 87 | { 88 | if (!_ready_coros.empty()) 89 | { 90 | auto coro = _ready_coros.front(); 91 | _ready_coros.pop(); 92 | 93 | coro.resume(); 94 | } 95 | 96 | while (!_timed_wait_coros.empty()) 97 | { 98 | auto it = _timed_wait_coros.begin(); 99 | if (std::chrono::high_resolution_clock::now() < it->first) 100 | break; 101 | 102 | for (auto& coro : it->second.first) 103 | { 104 | _ready_coros.push(coro); 105 | } 106 | 107 | _timed_wait_coros.erase(it); 108 | } 109 | 110 | return true; 111 | } 112 | 113 | return false; 114 | } 115 | 116 | void loop() 117 | { 118 | while (tick()) 119 | ; 120 | } 121 | 122 | private: 123 | executor() = default; 124 | ~executor() = default; 125 | 126 | std::queue> _ready_coros; 127 | std::map>, std::unordered_map, std::list>::iterator>>> _timed_wait_coros; 128 | 129 | int _num_outstanding_coros = 0; 130 | }; 131 | 132 | // NB: try keep cancellation sources in scope, and it can freely pass tokens to other coroutines without worrying about becoming dangling 133 | class cancellation 134 | { 135 | private: 136 | struct impl 137 | { 138 | typedef std::shared_ptr ptr; 139 | 140 | impl() = default; 141 | ~impl() = default; 142 | 143 | impl(const impl&) = delete; 144 | impl(impl&&) = delete; 145 | impl& operator=(const impl&) = delete; 146 | 147 | void fire() 148 | { 149 | for (auto& entry : _registry) 150 | { 151 | for (auto& f : entry.second) 152 | { 153 | f(); 154 | } 155 | } 156 | 157 | _registry.clear(); 158 | } 159 | 160 | std::unordered_map>> _registry; 161 | }; 162 | 163 | impl::ptr _impl_ptr; 164 | 165 | public: 166 | cancellation() 167 | : _impl_ptr(std::make_shared()) 168 | {} 169 | 170 | ~cancellation() = default; 171 | cancellation(const cancellation&) = default; 172 | cancellation& operator=(const cancellation&) = default; 173 | cancellation(cancellation&& other) = default; 174 | 175 | // NB: try keep the token on the stack or in scope, it would keep effective during the course of co_await! 176 | class token 177 | { 178 | public: 179 | token(impl::ptr source = nullptr) 180 | : _source(source) 181 | { 182 | } 183 | 184 | token(const token&) = default; 185 | token(token&&) = default; 186 | 187 | void register_action(std::function&& f) 188 | { 189 | if (_source) 190 | { 191 | auto r = _source->_registry.emplace(this, std::deque>{}); 192 | r.first->second.emplace_back(std::move(f)); 193 | } 194 | } 195 | 196 | void unregister() 197 | { 198 | if (_source) 199 | { 200 | _source->_registry.erase(this); 201 | } 202 | } 203 | 204 | ~token() 205 | { 206 | unregister(); 207 | } 208 | 209 | static token& none() 210 | { 211 | static token s_none; 212 | return s_none; 213 | } 214 | 215 | private: 216 | impl::ptr _source; 217 | }; 218 | 219 | token get_token() 220 | { 221 | return { _impl_ptr }; 222 | } 223 | 224 | void fire() 225 | { 226 | _impl_ptr->fire(); 227 | } 228 | }; 229 | 230 | // NB: this class is intended for fire and forget type of coroutines 231 | // specifically, final_suspend returns suspend_never, so the coroutine will end its course by itself 232 | // OTOH, awaitable's final_suspend returns suspend_always, giving await_resume a chance to retrieve any return value or propagate any exception 233 | struct co_void 234 | { 235 | struct promise_type 236 | { 237 | co_void get_return_object() 238 | { 239 | return {}; 240 | } 241 | 242 | auto initial_suspend() 243 | { 244 | return std::suspend_never{}; 245 | } 246 | 247 | auto final_suspend() noexcept 248 | { 249 | return std::suspend_never{}; 250 | } 251 | 252 | void return_void() noexcept {} 253 | void unhandled_exception() {} 254 | }; 255 | }; 256 | 257 | // The API level awaitable, which can be copied freely, while all the state is saved in the internal shared_ptr 258 | template 259 | class awaitable 260 | { 261 | private: 262 | class impl 263 | { 264 | public: 265 | typedef std::shared_ptr ptr; 266 | 267 | impl() = default; 268 | impl(const impl&) = delete; 269 | impl(impl&&) = delete; 270 | impl& operator=(const impl&) = delete; 271 | ~impl() = default; 272 | 273 | explicit impl(bool suspend) 274 | : _suspend(suspend) 275 | { 276 | } 277 | 278 | explicit impl(std::chrono::high_resolution_clock::duration timeout) 279 | : _when(std::chrono::high_resolution_clock::now() + timeout) 280 | { 281 | } 282 | 283 | template 284 | struct promise_type_base 285 | { 286 | X _value = X{}; 287 | 288 | void return_value(X&& value) 289 | { 290 | _value = std::move(value); 291 | } 292 | }; 293 | 294 | template <> 295 | struct promise_type_base 296 | { 297 | void return_void() 298 | { 299 | } 300 | }; 301 | 302 | struct promise_type : promise_type_base 303 | { 304 | awaitable get_return_object() 305 | { 306 | return awaitable{ *this }; 307 | } 308 | 309 | auto initial_suspend() 310 | { 311 | // NB: we want the coroutine to run until the first actual suspension point, unless explicitly requested to suspend 312 | return std::suspend_never{}; 313 | } 314 | 315 | // NB: we need to enforce FIFO ordering 316 | std::list> _awaiter_coros; 317 | 318 | auto final_suspend() noexcept 319 | { 320 | if (!_awaiter_coros.empty()) 321 | { 322 | for (auto coro : _awaiter_coros) 323 | { 324 | executor::singleton().add_ready_coro(coro); 325 | executor::singleton().decrement_num_outstanding_coros(); 326 | } 327 | _awaiter_coros.clear(); 328 | } 329 | return std::suspend_always{}; // NB: if we want to access the return value in await_resume, we need to keep the current coroutine around, even though coro.done() is now true! 330 | } 331 | 332 | std::exception_ptr _exp; 333 | void set_exception(std::exception_ptr exp) 334 | { 335 | _exp = exp; 336 | } 337 | 338 | void unhandled_exception() {} 339 | }; 340 | 341 | explicit impl(std::coroutine_handle coroutine) 342 | : _coroutine(coroutine) {} 343 | 344 | bool await_ready() noexcept 345 | { 346 | // if I'm enclosing a coroutine, use its status; otherwise, suspend if not ready 347 | return _coroutine ? _coroutine.done() : _when != std::chrono::high_resolution_clock::time_point{} ? std::chrono::high_resolution_clock::now() >= _when : _ready; 348 | } 349 | 350 | void await_suspend(std::coroutine_handle<> awaiter_coro) noexcept 351 | { 352 | if (!_coroutine) 353 | { 354 | // I'm not enclosing a coroutine while I'm awaited (await resumable_thing{};), add the awaiter's frame 355 | 356 | if (_when != std::chrono::high_resolution_clock::time_point{}) 357 | { 358 | if (std::chrono::high_resolution_clock::now() >= _when) 359 | { 360 | // the timer has already expired - but we should have guarded this situation in await_ready, so this should not happen 361 | // however, if this does happen, we should just put the awaiter_coro into the ready queue 362 | assert(false); // let's make sure this does not happen actually, but nevertheless, we add the awaiter_coro to the ready queue 363 | executor::singleton().add_ready_coro(awaiter_coro); 364 | } 365 | else 366 | { 367 | _awaiter_coros.emplace_back(awaiter_coro); // NB: guarantee FIFO ordering of the awaiters ... 368 | executor::singleton().add_timed_wait_coro(_when, awaiter_coro); 369 | } 370 | } 371 | else if (_suspend) 372 | { 373 | _awaiter_coros.emplace_back(awaiter_coro); // NB: guarantee FIFO ordering of the awaiters ... 374 | executor::singleton().increment_num_outstanding_coros(); 375 | } 376 | else 377 | { 378 | executor::singleton().add_ready_coro(awaiter_coro); 379 | } 380 | } 381 | else 382 | { 383 | // I'm waiting for some other coroutine to finish, the awaiter's frame can only be queued until my awaited one finishes 384 | _coroutine.promise()._awaiter_coros.emplace_back(awaiter_coro); // NB: guarantee FIFO ordering of the awaiters ... 385 | executor::singleton().increment_num_outstanding_coros(); 386 | } 387 | } 388 | 389 | template 390 | struct value 391 | { 392 | X _value = X{}; 393 | X& get() { return _value; } 394 | X move() { return std::move(_value); } 395 | }; 396 | 397 | template <> 398 | struct value 399 | { 400 | void get() {} 401 | void move() {} 402 | }; 403 | 404 | template 405 | struct save_promise_value 406 | { 407 | static void apply(value& v, promise_type& p) 408 | { 409 | v._value = std::move(p._value); 410 | } 411 | }; 412 | 413 | template <> 414 | struct save_promise_value 415 | { 416 | static void apply(value&, promise_type&) 417 | { 418 | } 419 | }; 420 | 421 | T await_resume() 422 | { 423 | if (_coroutine) 424 | { 425 | if (_coroutine.promise()._exp) 426 | { 427 | _exp = _coroutine.promise()._exp; 428 | } 429 | else 430 | { 431 | save_promise_value::apply(_value, _coroutine.promise()); 432 | } 433 | 434 | // the coroutine is finished, but returned from final_suspend (suspend_always), so we get a chance to retrieve any exception or value 435 | assert(_coroutine.done()); 436 | _coroutine.destroy(); 437 | _coroutine = nullptr; 438 | } 439 | 440 | _awaiter_coros.clear(); 441 | _when = std::chrono::high_resolution_clock::time_point{}; 442 | 443 | if (_exp) 444 | { 445 | std::rethrow_exception(_exp); 446 | } 447 | 448 | return _value.get(); 449 | } 450 | 451 | void set_ready() 452 | { 453 | if (!_awaiter_coros.empty()) 454 | { 455 | for (auto coro : _awaiter_coros) 456 | { 457 | executor::singleton().add_ready_coro(coro); 458 | if (_suspend) 459 | { 460 | executor::singleton().decrement_num_outstanding_coros(); 461 | } 462 | else 463 | { 464 | executor::singleton().remove_timed_wait_coro(_when, coro); 465 | } 466 | } 467 | 468 | _awaiter_coros.clear(); 469 | _when = std::chrono::high_resolution_clock::time_point{}; 470 | } 471 | _ready = true; 472 | } 473 | 474 | template ::value>::type* = nullptr> 475 | void set_ready(U&& value) 476 | { 477 | _value._value = std::forward(value); 478 | set_ready(); 479 | } 480 | 481 | void set_exception(std::exception_ptr exp) 482 | { 483 | _exp = exp; 484 | set_ready(); 485 | } 486 | 487 | value& get_value() 488 | { 489 | return _value; 490 | } 491 | 492 | private: 493 | value _value; 494 | 495 | std::exception_ptr _exp; 496 | 497 | // the coroutine this awaitable is enclosing; this is created by promise_type::get_return_object 498 | std::coroutine_handle _coroutine{ nullptr }; 499 | 500 | // the awaiter coroutine when this awaitable is a primitive - i.e. it doesn't enclose a coroutine, set only when _coroutine is nullptr! 501 | std::list> _awaiter_coros; 502 | std::chrono::high_resolution_clock::time_point _when; // NB: this should be initialized in the constructor, and cannot be modified 503 | 504 | bool _ready = false; 505 | bool _suspend = false; 506 | }; 507 | 508 | typename impl::ptr _impl_ptr; 509 | 510 | explicit awaitable(typename impl::promise_type& promise) 511 | : _impl_ptr(std::make_shared(std::coroutine_handle::from_promise(promise))) 512 | { 513 | } 514 | 515 | public: 516 | struct promise_type : impl::promise_type 517 | { 518 | }; 519 | 520 | awaitable() 521 | : _impl_ptr(std::make_shared()) 522 | { 523 | } 524 | 525 | explicit awaitable(bool suspend) 526 | : _impl_ptr(std::make_shared(suspend)) 527 | { 528 | } 529 | 530 | explicit awaitable(std::chrono::high_resolution_clock::duration timeout) 531 | : _impl_ptr(std::make_shared(timeout)) 532 | { 533 | } 534 | 535 | awaitable(awaitable const&) = default; 536 | awaitable& operator=(awaitable const&) = default; 537 | awaitable(awaitable&& other) = default; 538 | ~awaitable() = default; 539 | 540 | bool operator==(const awaitable& other) const 541 | { 542 | return _impl_ptr == other._impl_ptr; 543 | } 544 | 545 | bool await_ready() noexcept 546 | { 547 | return _impl_ptr->await_ready(); 548 | } 549 | 550 | void await_suspend(std::coroutine_handle<> awaiter_coro) noexcept 551 | { 552 | _impl_ptr->await_suspend(awaiter_coro); 553 | } 554 | 555 | T await_resume() 556 | { 557 | return _impl_ptr->await_resume(); 558 | } 559 | 560 | void set_ready() 561 | { 562 | _impl_ptr->set_ready(); 563 | } 564 | 565 | template ::value>::type* = nullptr> 566 | void set_ready(U&& value) 567 | { 568 | _impl_ptr->set_ready(std::forward(value)); 569 | } 570 | 571 | void set_exception(std::exception_ptr exp) 572 | { 573 | _impl_ptr->set_exception(exp); 574 | } 575 | 576 | T get_value() 577 | { 578 | return _impl_ptr->get_value().get(); 579 | } 580 | 581 | private: 582 | // NB: use of template template parameter is to avoid recursive template instantiation when retrieving the proxy type! 583 | //template < template class _awaitable > // TODO: try without template template parameter 584 | static co_void await_one(awaitable a, awaitable r, cancellation::token ct = cancellation::token::none()) 585 | { 586 | // NB: the cancellation token will remain in scope until the current function returns 587 | ct.register_action([&a] { a.set_exception(std::make_exception_ptr(std::exception("await_one.cancellation"))); }); 588 | 589 | try 590 | { 591 | // TODO: await_resume moves the result out, and here we need to retain the value, and probably need to set it back to 'a' 592 | // the challenge now is to deal gracefully both the void and non-void return types with co_await! 593 | co_await a; 594 | r.set_ready(a); 595 | } 596 | catch (...) 597 | { 598 | r.set_exception(std::current_exception()); 599 | co_return; 600 | } 601 | } 602 | 603 | // NB: use of template template parameter is to avoid recursive template instantiation when retrieving the proxy type! 604 | //template < template class _awaitable > // TODO: try without template template parameter 605 | //static co_void await_one(awaitable aa, awaitable r, cancellation::token ct = cancellation::token::none()) 606 | //{ 607 | // // NB: the cancellation token will remain in scope until the current function returns 608 | // ct.register_action([&aa] { aa.set_exception(std::make_exception_ptr(std::exception("await_one.cancellation"))); }); 609 | 610 | // try 611 | // { 612 | // co_await aa; 613 | // r.set_ready(aa.get_value()); 614 | // } 615 | // catch (...) 616 | // { 617 | // r.set_exception(std::current_exception()); 618 | // return; 619 | // } 620 | //} 621 | 622 | static co_void await_one(awaitable a, awaitable r, size_t& count = 0, cancellation::token ct = cancellation::token::none()) 623 | { 624 | // NB: the cancellation token will remain in scope until the current function returns 625 | ct.register_action([&a] { a.set_exception(std::make_exception_ptr(std::exception("await_one.cancellation"))); }); 626 | 627 | try 628 | { 629 | co_await a; 630 | 631 | if (count > 0 && --count == 0) 632 | { 633 | r.set_ready(); 634 | } 635 | } 636 | catch (...) 637 | { 638 | r.set_exception(std::current_exception()); 639 | co_return; 640 | } 641 | } 642 | 643 | public: 644 | static awaitable when_any(std::deque& awaitables, cancellation::token ct = cancellation::token::none()) 645 | { 646 | awaitable r{ true }; 647 | 648 | for (auto a : awaitables) 649 | { 650 | // NB: cannot register the cancellation action here, since we cannot maintain the call frame here 651 | // the cancellation token will be destructed when this function goes out of scope even before await_one ends 652 | // thus unregisters the registered action! 653 | await_one(a, r, ct); 654 | } 655 | 656 | return r; 657 | } 658 | 659 | // NB: input type of awaitable&& makes no sense, since the final result is a reference to an most decayed awaitable, an rvalue is temporary in nature, and thus should not be referenced! 660 | // so we don't define the following verion 661 | // friend awaitable operator||(awaitable&& a1, awaitable&& a2); 662 | // as well as the combinations of awaitable&& with other awaitable types 663 | 664 | // [1] 665 | friend awaitable operator||(awaitable a1, awaitable a2) 666 | { 667 | awaitable r{ true }; 668 | await_one(a1, r); 669 | await_one(a2, r); 670 | return r; 671 | } 672 | 673 | // [2-1] 674 | friend awaitable operator||(awaitable a1, awaitable a2) 675 | { 676 | // NB: instead of creating another awaitable, we just reuse a1! 677 | await_one(a2, a1); 678 | return a1; 679 | } 680 | 681 | // [2-2] 682 | friend awaitable operator||(awaitable a1, awaitable a2) 683 | { 684 | return a2 || a1; 685 | } 686 | 687 | // [3] - would this collide with the method as defined in the next level (when instantiating awaitable - probably 688 | //friend awaitable operator||(awaitable a1, awaitable a2) 689 | //{ 690 | // awaitable r{ true }; 691 | // await_one(a1, r); 692 | // await_one(a2, r); 693 | // return r; 694 | //} 695 | 696 | static awaitable when_all(std::deque& awaitables, cancellation::token ct = cancellation::token::none()) 697 | { 698 | awaitable r{ true }; 699 | 700 | size_t count = awaitables.size(); // NB: count remains on the stack due to the co_await below 701 | for (auto a : awaitables) 702 | { 703 | await_one(a, r, count, ct); 704 | } 705 | 706 | co_await r; 707 | } 708 | 709 | friend awaitable operator&&(awaitable a1, awaitable a2) 710 | { 711 | awaitable r{ true }; 712 | 713 | size_t count = 2; // NB: count remains on the stack due to the co_await below 714 | await_one(a1, r, count); 715 | await_one(a2, r, count); 716 | 717 | co_await r; 718 | } 719 | 720 | template 721 | friend std::enable_if_t::value, awaitable> operator&&(awaitable a1, awaitable a2); 722 | 723 | template 724 | friend std::enable_if_t::value, awaitable> operator&&(awaitable a1, awaitable a2); 725 | }; 726 | 727 | template 728 | std::enable_if_t::value, awaitable> operator&&(awaitable a1, awaitable a2) 729 | { 730 | awaitable r{ true }; 731 | 732 | size_t count = 2; // NB: count remains on the stack due to the co_await below 733 | awaitable::await_one(a1, r, count); 734 | awaitable::await_one(a2, r, count); 735 | 736 | co_await r; 737 | } 738 | 739 | template 740 | std::enable_if_t::value, awaitable> operator&&(awaitable a1, awaitable a2) 741 | { 742 | return a2 && a1; 743 | } 744 | 745 | auto operator co_await(std::chrono::high_resolution_clock::duration duration) 746 | { 747 | return awaitable{duration}; 748 | } 749 | } 750 | 751 | -------------------------------------------------------------------------------- /Awaitable/Awaitable.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {1db1c9bd-9762-4391-8a69-458b158f89af} 25 | Awaitable 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | true 77 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 78 | true 79 | stdcpp20 80 | stdc17 81 | 82 | 83 | Console 84 | true 85 | 86 | 87 | 88 | 89 | Level3 90 | true 91 | true 92 | true 93 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 94 | true 95 | stdcpp20 96 | stdc17 97 | 98 | 99 | Console 100 | true 101 | true 102 | true 103 | 104 | 105 | 106 | 107 | Level3 108 | true 109 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 110 | true 111 | stdcpp20 112 | stdc17 113 | 114 | 115 | Console 116 | true 117 | 118 | 119 | 120 | 121 | Level3 122 | true 123 | true 124 | true 125 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 126 | true 127 | stdcpp20 128 | stdc17 129 | 130 | 131 | Console 132 | true 133 | true 134 | true 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /Awaitable/Awaitable.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | 23 | 24 | Header Files 25 | 26 | 27 | -------------------------------------------------------------------------------- /Awaitable/ReadMe.txt: -------------------------------------------------------------------------------- 1 | ======================================================================== 2 | CONSOLE APPLICATION : Awaitable Project Overview 3 | ======================================================================== 4 | 5 | AppWizard has created this Awaitable application for you. 6 | 7 | This file contains a summary of what you will find in each of the files that 8 | make up your Awaitable application. 9 | 10 | 11 | Awaitable.vcxproj 12 | This is the main project file for VC++ projects generated using an Application Wizard. 13 | It contains information about the version of Visual C++ that generated the file, and 14 | information about the platforms, configurations, and project features selected with the 15 | Application Wizard. 16 | 17 | Awaitable.vcxproj.filters 18 | This is the filters file for VC++ projects generated using an Application Wizard. 19 | It contains information about the association between the files in your project 20 | and the filters. This association is used in the IDE to show grouping of files with 21 | similar extensions under a specific node (for e.g. ".cpp" files are associated with the 22 | "Source Files" filter). 23 | 24 | Awaitable.cpp 25 | This is the main application source file. 26 | 27 | ///////////////////////////////////////////////////////////////////////////// 28 | Other standard files: 29 | 30 | StdAfx.h, StdAfx.cpp 31 | These files are used to build a precompiled header (PCH) file 32 | named Awaitable.pch and a precompiled types file named StdAfx.obj. 33 | 34 | ///////////////////////////////////////////////////////////////////////////// 35 | Other notes: 36 | 37 | AppWizard uses "TODO:" comments to indicate parts of the source code you 38 | should add to or customize. 39 | 40 | ///////////////////////////////////////////////////////////////////////////// 41 | -------------------------------------------------------------------------------- /Awaitable/stdafx.cpp: -------------------------------------------------------------------------------- 1 | // stdafx.cpp : source file that includes just the standard includes 2 | // Awaitable.pch will be the pre-compiled header 3 | // stdafx.obj will contain the pre-compiled type information 4 | 5 | #include "stdafx.h" 6 | 7 | // TODO: reference any additional headers you need in STDAFX.H 8 | // and not in this file 9 | -------------------------------------------------------------------------------- /Awaitable/stdafx.h: -------------------------------------------------------------------------------- 1 | // stdafx.h : include file for standard system include files, 2 | // or project specific include files that are used frequently, but 3 | // are changed infrequently 4 | // 5 | 6 | #pragma once 7 | 8 | #include "targetver.h" 9 | 10 | #include 11 | #include 12 | 13 | 14 | 15 | // TODO: reference additional headers your program requires here 16 | -------------------------------------------------------------------------------- /Awaitable/targetver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Including SDKDDKVer.h defines the highest available Windows platform. 4 | 5 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 6 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 7 | 8 | #include 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awaitable 2 | 3 | An single-threaded (with a simple single-threaded executor), cooperative awaitable<> facility with async timer & cancellation support, based on C++20 coroutine support. 4 | 5 | Tested on Visual Studio 2022 17.9.x 6 | 7 | See Awaitable.cpp for a handful of examples. 8 | --------------------------------------------------------------------------------