├── ext └── ractor │ └── tvar │ ├── extconf.rb │ └── tvar.c ├── .gitignore ├── lib └── ractor │ ├── tvar │ └── version.rb │ └── tvar.rb ├── bin ├── setup └── console ├── test ├── test_helper.rb └── ractor │ └── tvar_test.rb ├── Gemfile ├── Rakefile ├── .github └── workflows │ └── main.yml ├── LICENSE.txt ├── ractor-tvar.gemspec └── README.md /ext/ractor/tvar/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | create_makefile('ractor/tvar') 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /lib/ractor/tvar/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Ractor 4 | class TVar 5 | VERSION = "0.4.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "ractor/tvar" 5 | 6 | require "test-unit" 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in ractor-tvar.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "test-unit", "~> 3.0" 11 | -------------------------------------------------------------------------------- /lib/ractor/tvar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ractor/tvar/version" 4 | require 'ractor/tvar.so' 5 | 6 | class Ractor 7 | class TVar 8 | def __increment__ inc 9 | Ractor::atomically do 10 | self.value += inc 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | require "rake/extensiontask" 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << "test" 9 | t.libs << "lib" 10 | t.test_files = FileList["test/**/*_test.rb"] 11 | end 12 | 13 | Rake::ExtensionTask.new "ractor/tvar" do |ext| 14 | 15 | end 16 | 17 | task :test => :compile 18 | task default: :test 19 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "ractor/tvar" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | on: [push,pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu] 9 | ruby: [head, debug] 10 | 11 | runs-on: ${{ matrix.os }}-latest 12 | continue-on-error: ${{ matrix.ruby == 'head' || matrix.ruby == 'debug' }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: ${{ matrix.ruby }} 18 | 19 | - run: bundle install 20 | - run: bundle exec rake 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Koichi Sasada 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ractor-tvar.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/ractor/tvar/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "ractor-tvar" 7 | spec.version = Ractor::TVar::VERSION 8 | spec.authors = ["Koichi Sasada"] 9 | spec.email = ["ko1@atdot.net"] 10 | 11 | spec.summary = "Ractor::TVar" 12 | spec.description = "Ractor::TVar" 13 | spec.homepage = "https://github.com/ko1/ractor-tvar" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0dev") 16 | spec.extensions = %w(ext/ractor/tvar/extconf.rb) 17 | 18 | # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" 19 | 20 | spec.metadata["homepage_uri"] = spec.homepage 21 | spec.metadata["source_code_uri"] = "https://github.com/ko1/ractor-tvar" 22 | spec.metadata["changelog_uri"] = "https://github.com/ko1/ractor-tvar" 23 | 24 | # Specify which files should be added to the gem when it is released. 25 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 26 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 27 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 28 | end 29 | spec.require_paths = ["lib"] 30 | spec.add_development_dependency "rake" 31 | spec.add_development_dependency "rake-compiler" 32 | end 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ractor::TVar 2 | 3 | [Software transactional memory](https://en.wikipedia.org/wiki/Software_transactional_memory) implementation for Ractor and Thread on Ruby 3.0. 4 | 5 | ```ruby 6 | require 'ractor/tvar' 7 | 8 | tv = Ractor::TVar.new(0) 9 | 10 | N = 10_000 11 | 12 | r = Ractor.new tv do |tv| 13 | N.times do 14 | Ractor.atomically do 15 | tv.value += 1 16 | end 17 | end 18 | end 19 | 20 | N.times do 21 | Ractor.atomically do 22 | tv.value += 1 23 | end 24 | end 25 | 26 | r.take # wait for the ractor 27 | 28 | p tv.value #=> 20000 (= N * 2) 29 | ``` 30 | 31 | This script shows that there is no race between ractors. 32 | 33 | ## Installation 34 | 35 | You need recent Ruby 3.0 (development). 36 | 37 | Add this line to your application's Gemfile: 38 | 39 | ```ruby 40 | gem 'ractor-tvar' 41 | ``` 42 | 43 | And then execute: 44 | 45 | $ bundle install 46 | 47 | Or install it yourself as: 48 | 49 | $ gem install ractor-tvar 50 | 51 | ## Development 52 | 53 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test-unit` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 54 | 55 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 56 | 57 | ## Contributing 58 | 59 | Bug reports and pull requests are welcome on GitHub at https://github.com/ko1/ractor-tvar. 60 | 61 | 62 | ## License 63 | 64 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 65 | -------------------------------------------------------------------------------- /test/ractor/tvar_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Ractor::TVarTest < Test::Unit::TestCase 6 | test "VERSION" do 7 | assert do 8 | ::Ractor::TVar.const_defined?(:VERSION) 9 | end 10 | end 11 | 12 | test 'Ractor::TVar can has a value' do 13 | tv = Ractor::TVar.new(1) 14 | assert_equal 1, tv.value 15 | end 16 | 17 | test 'Ractor::TVar without initial value will return nil' do 18 | tv = Ractor::TVar.new 19 | assert_equal nil, tv.value 20 | end 21 | 22 | test 'Ractor::TVar can change the value' do 23 | tv = Ractor::TVar.new 24 | assert_equal nil, tv.value 25 | Ractor::atomically do 26 | tv.value = :ok 27 | end 28 | assert_equal :ok, tv.value 29 | end 30 | 31 | test 'Ractor::TVar update without atomically will raise an exception' do 32 | tv = Ractor::TVar.new 33 | assert_raise Ractor::TransactionError do 34 | tv.value = :ng 35 | end 36 | end 37 | 38 | test 'Ractor::TVar#increment increments the value' do 39 | tv = Ractor::TVar.new(0) 40 | tv.increment 41 | assert_equal 1, tv.value 42 | 43 | tv.increment 2 44 | assert_equal 3, tv.value 45 | 46 | Ractor::atomically do 47 | tv.increment 3 48 | end 49 | assert_equal 6, tv.value 50 | 51 | Ractor::atomically do 52 | tv.value = 1.5 53 | end 54 | tv.increment(-1.5) 55 | assert_equal 0.0, tv.value 56 | end 57 | 58 | test 'Ractor::TVar can not set the unshareable value' do 59 | assert_raise ArgumentError do 60 | Ractor::TVar.new [1] 61 | end 62 | end 63 | 64 | ## with Ractors 65 | N = 10_000 66 | test 'Ractor::TVar consistes with other Ractors' do 67 | tv = Ractor::TVar.new(0) 68 | rs = 4.times.map{ 69 | Ractor.new tv do |tv| 70 | N.times{ Ractor::atomically{ tv.increment } } 71 | end 72 | } 73 | rs.each{|r| r.take} 74 | assert_equal N * 4 , tv.value 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /ext/ractor/tvar/tvar.c: -------------------------------------------------------------------------------- 1 | #include "ruby/ruby.h" 2 | #include "ruby/util.h" 3 | #include "ruby/thread_native.h" 4 | #include "ruby/ractor.h" 5 | 6 | // quoted (and modified) from "internal/fixnum.h" 7 | 8 | static inline long 9 | rb_overflowed_fix_to_int(long x) 10 | { 11 | return (long)((unsigned long)(x >> 1) ^ (1LU << (SIZEOF_LONG * CHAR_BIT - 1))); 12 | } 13 | 14 | static inline VALUE 15 | rb_fix_plus_fix(VALUE x, VALUE y) 16 | { 17 | #if !HAVE_BUILTIN___BUILTIN_ADD_OVERFLOW 18 | long lz = FIX2LONG(x) + FIX2LONG(y); 19 | return LONG2NUM(lz); 20 | #else 21 | long lz; 22 | /* NOTE 23 | * (1) `LONG2FIX(FIX2LONG(x)+FIX2LONG(y))` 24 | + = `((lx*2+1)/2 + (ly*2+1)/2)*2+1` 25 | + = `lx*2 + ly*2 + 1` 26 | + = `(lx*2+1) + (ly*2+1) - 1` 27 | + = `x + y - 1` 28 | * (2) Fixnum's LSB is always 1. 29 | * It means you can always run `x - 1` without overflow. 30 | * (3) Of course `z = x + (y-1)` may overflow. 31 | * At that time true value is 32 | * * positive: 0b0 1xxx...1, and z = 0b1xxx...1 33 | * * nevative: 0b1 0xxx...1, and z = 0b0xxx...1 34 | * To convert this true value to long, 35 | * (a) Use arithmetic shift 36 | * * positive: 0b11xxx... 37 | * * negative: 0b00xxx... 38 | * (b) invert MSB 39 | * * positive: 0b01xxx... 40 | * * negative: 0b10xxx... 41 | */ 42 | if (__builtin_add_overflow((long)x, (long)y-1, &lz)) { 43 | return rb_int2big(rb_overflowed_fix_to_int(lz)); 44 | } 45 | else { 46 | return (VALUE)lz; 47 | } 48 | #endif 49 | } 50 | 51 | static inline unsigned int 52 | rb_popcount32(uint32_t x) 53 | { 54 | #if defined(_MSC_VER) && defined(__AVX__) 55 | /* Note: CPUs since Nehalem and Barcelona have had this instruction so SSE 56 | * 4.2 should suffice, but it seems there is no such thing like __SSE_4_2__ 57 | * predefined macro in MSVC. They do have __AVX__ so use it instead. */ 58 | return (unsigned int)__popcnt(x); 59 | 60 | #elif HAVE_BUILTIN___BUILTIN_POPCOUNT 61 | return (unsigned int)__builtin_popcount(x); 62 | #else 63 | x = (x & 0x55555555) + (x >> 1 & 0x55555555); 64 | x = (x & 0x33333333) + (x >> 2 & 0x33333333); 65 | x = (x & 0x0f0f0f0f) + (x >> 4 & 0x0f0f0f0f); 66 | x = (x & 0x001f001f) + (x >> 8 & 0x001f001f); 67 | x = (x & 0x0000003f) + (x >>16 & 0x0000003f); 68 | return (unsigned int)x; 69 | #endif 70 | } 71 | 72 | // Thread/Ractor support transactional variable Thread::TVar 73 | 74 | // 0: null (BUG/only for evaluation) 75 | // 1: mutex 76 | // TODO: 1: atomic 77 | #define SLOT_LOCK_TYPE 1 78 | 79 | struct slot_lock { 80 | #if SLOT_LOCK_TYPE == 0 81 | #elif SLOT_LOCK_TYPE == 1 82 | rb_nativethread_lock_t lock; 83 | #else 84 | #error unknown 85 | #endif 86 | }; 87 | 88 | struct tvar_slot { 89 | uint64_t version; 90 | VALUE value; 91 | VALUE index; 92 | struct slot_lock lock; 93 | }; 94 | 95 | struct tx_global { 96 | uint64_t version; 97 | rb_nativethread_lock_t version_lock; 98 | 99 | uint64_t slot_index; 100 | rb_nativethread_lock_t slot_index_lock; 101 | }; 102 | 103 | struct tx_log { 104 | VALUE value; 105 | struct tvar_slot *slot; 106 | VALUE tvar; // mark slot 107 | }; 108 | 109 | struct tx_logs { 110 | uint64_t version; 111 | uint32_t logs_cnt; 112 | uint32_t logs_capa; 113 | 114 | struct tx_log *logs; 115 | 116 | bool enabled; 117 | bool stop_adding; 118 | 119 | uint32_t retry_history; 120 | size_t retry_on_commit; 121 | size_t retry_on_read_lock; 122 | size_t retry_on_read_version; 123 | }; 124 | 125 | static struct tx_global tx_global; 126 | 127 | static VALUE rb_eTxRetry; 128 | static VALUE rb_eTxError; 129 | static VALUE rb_exc_tx_retry; 130 | static VALUE rb_cRactorTVar; 131 | static VALUE rb_cRactorTxLogs; 132 | 133 | static ID id_tx_logs; 134 | 135 | static struct tx_global * 136 | tx_global_ptr(void) 137 | { 138 | return &tx_global; 139 | } 140 | 141 | #define TVAR_DEBUG_LOG(...) 142 | 143 | static VALUE 144 | txg_next_index(struct tx_global *txg) 145 | { 146 | VALUE index; 147 | rb_native_mutex_lock(&txg->slot_index_lock); 148 | { 149 | txg->slot_index++; 150 | index = INT2FIX(txg->slot_index); 151 | } 152 | rb_native_mutex_unlock(&txg->slot_index_lock); 153 | 154 | return index; 155 | } 156 | 157 | static uint64_t 158 | txg_version(const struct tx_global *txg) 159 | { 160 | uint64_t version; 161 | version = txg->version; 162 | return version; 163 | } 164 | 165 | static uint64_t 166 | txg_next_version(struct tx_global *txg) 167 | { 168 | uint64_t version; 169 | 170 | rb_native_mutex_lock(&txg->version_lock); 171 | { 172 | txg->version++; 173 | version = txg->version; 174 | TVAR_DEBUG_LOG("new_version:%lu", version); 175 | } 176 | rb_native_mutex_unlock(&txg->version_lock); 177 | 178 | return version; 179 | } 180 | 181 | // tx: transaction 182 | 183 | static void 184 | tx_slot_lock_init(struct slot_lock *lock) 185 | { 186 | #if SLOT_LOCK_TYPE == 0 187 | #elif SLOT_LOCK_TYPE == 1 188 | rb_native_mutex_initialize(&lock->lock); 189 | #else 190 | #error unknown 191 | #endif 192 | } 193 | 194 | static void 195 | tx_slot_lock_free(struct slot_lock *lock) 196 | { 197 | #if SLOT_LOCK_TYPE == 0 198 | #elif SLOT_LOCK_TYPE == 1 199 | rb_native_mutex_destroy(&lock->lock); 200 | #else 201 | #error unknown 202 | #endif 203 | } 204 | 205 | static bool 206 | tx_slot_lock_trylock(struct slot_lock *lock) 207 | { 208 | #if SLOT_LOCK_TYPE == 0 209 | return true; 210 | #elif SLOT_LOCK_TYPE == 1 211 | return rb_native_mutex_trylock(&lock->lock) == 0; 212 | #else 213 | #error unknown 214 | #endif 215 | } 216 | 217 | static void 218 | tx_slot_lock_lock(struct slot_lock *lock) 219 | { 220 | #if SLOT_LOCK_TYPE == 0 221 | #elif SLOT_LOCK_TYPE == 1 222 | rb_native_mutex_lock(&lock->lock); 223 | #else 224 | #error unknown 225 | #endif 226 | } 227 | 228 | static void 229 | tx_slot_lock_unlock(struct slot_lock *lock) 230 | { 231 | #if SLOT_LOCK_TYPE == 0 232 | #elif SLOT_LOCK_TYPE == 1 233 | rb_native_mutex_unlock(&lock->lock); 234 | #else 235 | #error unknown 236 | #endif 237 | } 238 | 239 | static bool 240 | tx_slot_trylock(struct tvar_slot *slot) 241 | { 242 | return tx_slot_lock_trylock(&slot->lock); 243 | } 244 | 245 | static void 246 | tx_slot_lock(struct tvar_slot *slot) 247 | { 248 | tx_slot_lock_lock(&slot->lock); 249 | } 250 | 251 | static void 252 | tx_slot_unlock(struct tvar_slot *slot) 253 | { 254 | tx_slot_lock_unlock(&slot->lock); 255 | } 256 | 257 | 258 | static void 259 | tx_mark(void *ptr) 260 | { 261 | struct tx_logs *tx = (struct tx_logs *)ptr; 262 | 263 | for (uint32_t i=0; ilogs_cnt; i++) { 264 | rb_gc_mark(tx->logs[i].value); 265 | } 266 | } 267 | 268 | static void 269 | tx_free(void *ptr) 270 | { 271 | struct tx_logs *tx = (struct tx_logs *)ptr; 272 | 273 | TVAR_DEBUG_LOG("retry %5lu commit:%lu read_lock:%lu read_version:%lu", 274 | tx->retry_on_commit + tx->retry_on_read_lock + tx->retry_on_read_version, 275 | tx->retry_on_commit, 276 | tx->retry_on_read_lock, 277 | tx->retry_on_read_version); 278 | 279 | ruby_xfree(tx->logs); 280 | ruby_xfree(tx); 281 | } 282 | 283 | static const rb_data_type_t txlogs_type = { 284 | "txlogs", 285 | {tx_mark, tx_free, NULL,}, 286 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 287 | }; 288 | 289 | static struct tx_logs * 290 | tx_logs(void) 291 | { 292 | VALUE cth = rb_thread_current(); 293 | VALUE txobj = rb_thread_local_aref(cth, id_tx_logs); 294 | 295 | if (txobj == Qnil) { 296 | struct tx_logs *tx; 297 | txobj = TypedData_Make_Struct(rb_cRactorTxLogs, struct tx_logs, &txlogs_type, tx); 298 | tx->logs_capa = 0x10; // default 299 | tx->logs = ALLOC_N(struct tx_log, tx->logs_capa); 300 | rb_thread_local_aset(cth, id_tx_logs, txobj); 301 | return tx; 302 | } 303 | else { 304 | // TODO: check 305 | return DATA_PTR(txobj); 306 | } 307 | } 308 | 309 | static struct tx_log * 310 | tx_lookup(struct tx_logs *tx, VALUE tvar) 311 | { 312 | struct tx_log *copies = tx->logs; 313 | uint32_t cnt = tx->logs_cnt; 314 | 315 | for (uint32_t i = 0; i< cnt; i++) { 316 | if (copies[i].tvar == tvar) { 317 | return &copies[i]; 318 | } 319 | } 320 | 321 | return NULL; 322 | } 323 | 324 | static void 325 | tx_add(struct tx_logs *tx, VALUE val, struct tvar_slot *slot, VALUE tvar) 326 | { 327 | if (RB_UNLIKELY(tx->logs_capa == tx->logs_cnt)) { 328 | uint32_t new_capa = tx->logs_capa * 2; 329 | RB_REALLOC_N(tx->logs, struct tx_log, new_capa); 330 | tx->logs_capa = new_capa; 331 | } 332 | if (RB_UNLIKELY(tx->stop_adding)) { 333 | rb_raise(rb_eTxError, "can not handle more transactional variable: %"PRIxVALUE, rb_inspect(tvar)); 334 | } 335 | struct tx_log *log = &tx->logs[tx->logs_cnt++]; 336 | 337 | log->value = val; 338 | log->slot = slot; 339 | log->tvar = tvar; 340 | } 341 | 342 | static VALUE 343 | tx_get(struct tx_logs *tx, struct tvar_slot *slot, VALUE tvar) 344 | { 345 | struct tx_log *ent = tx_lookup(tx, tvar); 346 | 347 | if (ent == NULL) { 348 | VALUE val; 349 | 350 | if (tx_slot_trylock(slot)) { 351 | if (slot->version > tx->version) { 352 | TVAR_DEBUG_LOG("RV < slot->V slot:%u slot->version:%lu, tx->version:%lu", FIX2INT(slot->index), slot->version, tx->version); 353 | tx_slot_unlock(slot); 354 | tx->retry_on_read_version++; 355 | goto abort_and_retry; 356 | } 357 | val = slot->value; 358 | tx_slot_unlock(slot); 359 | } 360 | else { 361 | TVAR_DEBUG_LOG("RV < slot->V slot:%u slot->version:%lu, tx->version:%lu", FIX2INT(slot->index), slot->version, tx->version); 362 | tx->retry_on_read_lock++; 363 | goto abort_and_retry; 364 | } 365 | tx_add(tx, val, slot, tvar); 366 | return val; 367 | 368 | abort_and_retry: 369 | rb_raise(rb_eTxRetry, "retry"); 370 | } 371 | else { 372 | return ent->value; 373 | } 374 | } 375 | 376 | static void 377 | tx_set(struct tx_logs *tx, VALUE val, struct tvar_slot *slot, VALUE tvar) 378 | { 379 | struct tx_log *ent = tx_lookup(tx, tvar); 380 | 381 | if (ent == NULL) { 382 | tx_add(tx, val, slot, tvar); 383 | } 384 | else { 385 | ent->value = val; 386 | } 387 | } 388 | 389 | static void 390 | tx_check(struct tx_logs *tx) 391 | { 392 | if (RB_UNLIKELY(!tx->enabled)) { 393 | rb_raise(rb_eTxError, "can not set without transaction"); 394 | } 395 | } 396 | 397 | static void 398 | tx_setup(struct tx_global *txg, struct tx_logs *tx) 399 | { 400 | RUBY_ASSERT(tx->enabled); 401 | RUBY_ASSERT(tx->logs_cnt == 0); 402 | 403 | tx->version = txg_version(txg); 404 | 405 | TVAR_DEBUG_LOG("tx:%lu", tx->version); 406 | } 407 | 408 | static struct tx_logs * 409 | tx_begin(void) 410 | { 411 | struct tx_global *txg = tx_global_ptr(); 412 | struct tx_logs *tx = tx_logs(); 413 | 414 | RUBY_ASSERT(tx->stop_adding == false); 415 | RUBY_ASSERT(tx->logs_cnt == 0); 416 | 417 | if (tx->enabled == false) { 418 | tx->enabled = true; 419 | tx_setup(txg, tx); 420 | return tx; 421 | } 422 | else { 423 | return NULL; 424 | } 425 | } 426 | 427 | static VALUE 428 | tx_reset(struct tx_logs *tx) 429 | { 430 | struct tx_global *txg = tx_global_ptr(); 431 | tx->logs_cnt = 0; 432 | 433 | // contention management (CM) 434 | if (tx->retry_history != 0) { 435 | int recent_retries = 0; rb_popcount32(tx->retry_history); 436 | TVAR_DEBUG_LOG("retry recent_retries:%d", recent_retries); 437 | 438 | struct timeval tv = { 439 | .tv_sec = 0, 440 | .tv_usec = 1 * recent_retries, 441 | }; 442 | 443 | TVAR_DEBUG_LOG("CM tv_usec:%lu", (unsigned long)tv.tv_usec); 444 | rb_thread_wait_for(tv); 445 | } 446 | 447 | tx_setup(txg, tx); 448 | TVAR_DEBUG_LOG("tx:%lu", tx->version); 449 | 450 | return Qnil; 451 | } 452 | 453 | static VALUE 454 | tx_end(struct tx_logs *tx) 455 | { 456 | TVAR_DEBUG_LOG("tx:%lu", tx->version); 457 | 458 | RUBY_ASSERT(tx->enabled); 459 | RUBY_ASSERT(tx->stop_adding == false); 460 | tx->enabled = false; 461 | tx->logs_cnt = 0; 462 | return Qnil; 463 | } 464 | 465 | static void 466 | tx_commit_release(struct tx_logs *tx, uint32_t n) 467 | { 468 | struct tx_log *copies = tx->logs; 469 | 470 | for (uint32_t i = 0; islot; 473 | tx_slot_unlock(slot); 474 | } 475 | } 476 | 477 | static VALUE 478 | tx_commit(struct tx_logs *tx) 479 | { 480 | struct tx_global *txg = tx_global_ptr(); 481 | uint32_t i; 482 | struct tx_log *copies = tx->logs; 483 | uint32_t logs_cnt = tx->logs_cnt; 484 | 485 | for (i=0; islot; 488 | 489 | if (RB_LIKELY(tx_slot_trylock(slot))) { 490 | if (RB_UNLIKELY(slot->version > tx->version)) { 491 | TVAR_DEBUG_LOG("RV < slot->V slot:%lu tx:%lu rs:%lu", slot->version, tx->version, txg->version); 492 | tx_commit_release(tx, i+1); 493 | goto abort_and_retry; 494 | } 495 | else { 496 | // lock success 497 | TVAR_DEBUG_LOG("lock slot:%lu tx:%lu rs:%lu", slot->version, tx->version, txg->version); 498 | } 499 | } 500 | else { 501 | TVAR_DEBUG_LOG("trylock fail slot:%lu tx:%lu rs:%lu", slot->version, tx->version, txg->version); 502 | tx_commit_release(tx, i); 503 | goto abort_and_retry; 504 | } 505 | } 506 | 507 | // ok 508 | tx->retry_history <<= 1; 509 | 510 | uint64_t new_version = txg_next_version(txg); 511 | 512 | for (i=0; islot; 515 | 516 | if (slot->value != copy->value) { 517 | TVAR_DEBUG_LOG("write slot:%d %d->%d slot->version:%lu->%lu tx:%lu rs:%lu", 518 | FIX2INT(slot->index), FIX2INT(slot->value), FIX2INT(copy->value), 519 | slot->version, new_version, tx->version, txg->version); 520 | 521 | slot->version = new_version; 522 | slot->value = copy->value; 523 | } 524 | } 525 | 526 | tx_commit_release(tx, logs_cnt); 527 | 528 | return Qtrue; 529 | 530 | abort_and_retry: 531 | tx->retry_on_commit++; 532 | 533 | return Qfalse; 534 | } 535 | 536 | // tvar 537 | 538 | static void 539 | tvar_mark(void *ptr) 540 | { 541 | struct tvar_slot *slot = (struct tvar_slot *)ptr; 542 | rb_gc_mark(slot->value); 543 | } 544 | 545 | static void 546 | tvar_free(void *ptr) 547 | { 548 | struct tvar_slot *slot = (struct tvar_slot *)ptr; 549 | tx_slot_lock_free(&slot->lock); 550 | ruby_xfree(slot); 551 | } 552 | 553 | static const rb_data_type_t tvar_data_type = { 554 | "Thread::TVar", 555 | {tvar_mark, tvar_free, NULL,}, 556 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 557 | }; 558 | 559 | static VALUE 560 | tvar_new_(VALUE self, VALUE init) 561 | { 562 | // init should be shareable 563 | if (RB_UNLIKELY(!rb_ractor_shareable_p(init))) { 564 | rb_raise(rb_eArgError, "only shareable object are allowed"); 565 | } 566 | 567 | struct tx_global *txg = tx_global_ptr(); 568 | struct tvar_slot *slot; 569 | VALUE obj = TypedData_Make_Struct(rb_cRactorTVar, struct tvar_slot, &tvar_data_type, slot); 570 | slot->version = 0; 571 | slot->value = init; 572 | slot->index = txg_next_index(txg); 573 | tx_slot_lock_init(&slot->lock); 574 | 575 | FL_SET_RAW(obj, RUBY_FL_SHAREABLE); 576 | 577 | return obj; 578 | } 579 | 580 | static VALUE 581 | tvar_new(int argc, VALUE *argv, VALUE self) 582 | { 583 | VALUE init = Qnil; 584 | rb_scan_args(argc, argv, "01", &init); 585 | return tvar_new_(self, init); 586 | } 587 | 588 | static VALUE 589 | tvar_value(VALUE self) 590 | { 591 | struct tx_logs *tx = tx_logs(); 592 | struct tvar_slot *slot = DATA_PTR(self); 593 | 594 | if (tx->enabled) { 595 | return tx_get(tx, slot, self); 596 | } 597 | else { 598 | // TODO: warn on multi-ractors? 599 | return slot->value; 600 | } 601 | } 602 | 603 | static VALUE 604 | tvar_value_set(VALUE self, VALUE val) 605 | { 606 | if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) { 607 | rb_raise(rb_eArgError, "only shareable object are allowed"); 608 | } 609 | 610 | struct tx_logs *tx = tx_logs(); 611 | tx_check(tx); 612 | struct tvar_slot *slot = DATA_PTR(self); 613 | tx_set(tx, val, slot, self); 614 | return val; 615 | } 616 | 617 | static VALUE 618 | tvar_calc_inc(VALUE v, VALUE inc) 619 | { 620 | if (RB_LIKELY(FIXNUM_P(v) && FIXNUM_P(inc))) { 621 | return rb_fix_plus_fix(v, inc); 622 | } 623 | else { 624 | return Qundef; 625 | } 626 | } 627 | 628 | static VALUE 629 | tvar_value_increment_(VALUE self, VALUE inc) 630 | { 631 | struct tx_global *txg = tx_global_ptr(); 632 | struct tx_logs *tx = tx_logs(); 633 | VALUE recv, ret; 634 | struct tvar_slot *slot = DATA_PTR(self); 635 | 636 | if (!tx->enabled) { 637 | tx_slot_lock(slot); 638 | { 639 | uint64_t new_version = txg_next_version(txg); 640 | recv = slot->value; 641 | ret = tvar_calc_inc(recv, inc); 642 | 643 | if (RB_LIKELY(ret != Qundef)) { 644 | slot->value = ret; 645 | slot->version = new_version; 646 | txg->version = new_version; 647 | } 648 | } 649 | tx_slot_unlock(slot); 650 | 651 | if (RB_UNLIKELY(ret == Qundef)) { 652 | // atomically{ self.value += inc } 653 | ret = rb_funcall(self, rb_intern("__increment__"), 1, inc); 654 | } 655 | } 656 | else { 657 | recv = tx_get(tx, slot, self); 658 | if (RB_UNLIKELY((ret = tvar_calc_inc(recv, inc)) == Qundef)) { 659 | ret = rb_funcall(recv, rb_intern("+"), 1, inc); 660 | } 661 | tx_set(tx, ret, slot, self); 662 | } 663 | 664 | return ret; 665 | } 666 | 667 | static VALUE 668 | tvar_value_increment(int argc, VALUE *argv, VALUE self) 669 | { 670 | switch (argc) { 671 | case 0: return tvar_value_increment_(self, INT2FIX(1)); 672 | case 1: return tvar_value_increment_(self, argv[0]); 673 | // todo: scan args 674 | default: rb_raise(rb_eArgError, "2 or more arguments"); 675 | } 676 | } 677 | 678 | #if 0 // unused 679 | static struct tvar_slot * 680 | tvar_slot_ptr(VALUE v) 681 | { 682 | if (rb_typeddata_is_kind_of(v, &tvar_data_type)) { 683 | return DATA_PTR(v); 684 | } 685 | else { 686 | rb_raise(rb_eArgError, "TVar is needed"); 687 | } 688 | } 689 | #endif 690 | 691 | static VALUE 692 | tx_atomically_body2(VALUE txptr) 693 | { 694 | struct tx_logs *tx = (struct tx_logs *)txptr; 695 | 696 | while (1) { 697 | VALUE ret = rb_yield(Qnil); 698 | 699 | if (tx_commit(tx)) { 700 | return ret; 701 | } 702 | else { 703 | tx_reset(tx); 704 | } 705 | } 706 | } 707 | 708 | static VALUE 709 | tx_atomically_rescue(VALUE txptr, VALUE err) 710 | { 711 | struct tx_logs *tx = (struct tx_logs *)txptr; 712 | tx_reset(tx); 713 | return Qundef; 714 | } 715 | 716 | static VALUE 717 | tx_atomically_body(VALUE txptr) 718 | { 719 | VALUE ret; 720 | 721 | do { 722 | ret = rb_rescue2(tx_atomically_body2, (VALUE)txptr, 723 | tx_atomically_rescue, (VALUE)txptr, 724 | rb_eTxRetry, 0); 725 | } while (ret == Qundef); 726 | 727 | return ret; 728 | } 729 | 730 | static VALUE 731 | tx_atomically_ensure(VALUE txptr) 732 | { 733 | struct tx_logs *tx = (struct tx_logs *)txptr; 734 | tx_end(tx); 735 | return 0; 736 | } 737 | 738 | static VALUE 739 | tx_atomically(VALUE self) 740 | { 741 | struct tx_logs *tx = tx_begin(); 742 | if (tx != NULL) { 743 | return rb_ensure(tx_atomically_body, (VALUE)tx, 744 | tx_atomically_ensure, (VALUE)tx); 745 | } 746 | else { 747 | return rb_yield(Qnil); 748 | } 749 | } 750 | 751 | void 752 | Init_tvar(void) 753 | { 754 | rb_ext_ractor_safe(true); 755 | 756 | // initialixe tx_global 757 | struct tx_global *txg = tx_global_ptr(); 758 | txg->slot_index = 0; 759 | txg->version = 0; 760 | rb_native_mutex_initialize(&txg->slot_index_lock); 761 | rb_native_mutex_initialize(&txg->version_lock); 762 | 763 | id_tx_logs = rb_intern("__ractor_tvar_tls__"); 764 | 765 | // errors 766 | rb_eTxError = rb_define_class_under(rb_cRactor, "TransactionError", rb_eRuntimeError); 767 | rb_eTxRetry = rb_define_class_under(rb_cRactor, "RetryTransaction", rb_eException); 768 | rb_exc_tx_retry = rb_exc_new_cstr(rb_eTxRetry, "Thread::RetryTransaction"); 769 | rb_obj_freeze(rb_exc_tx_retry); 770 | rb_gc_register_mark_object(rb_exc_tx_retry); 771 | 772 | // TxLogs 773 | rb_cRactorTxLogs = rb_define_class_under(rb_cRactor, "TxLogs", rb_cObject); // hidden object 774 | 775 | // TVar APIs 776 | rb_define_singleton_method(rb_cRactor, "atomically", tx_atomically, 0); 777 | 778 | rb_cRactorTVar = rb_define_class_under(rb_cRactor, "TVar", rb_cObject); 779 | rb_define_singleton_method(rb_cRactorTVar, "new", tvar_new, -1); 780 | rb_define_method(rb_cRactorTVar, "value", tvar_value, 0); 781 | rb_define_method(rb_cRactorTVar, "value=", tvar_value_set, 1); 782 | rb_define_method(rb_cRactorTVar, "increment", tvar_value_increment, -1); 783 | // rb_define_method(rb_cRactorTVar, "inspect", tvar_inspect, 0); 784 | } 785 | --------------------------------------------------------------------------------