dispatch_queue_rb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +98 -0
- data/dispatch_queue_rb.gemspec +43 -0
- data/lib/dispatch_queue_rb.rb +32 -0
- data/lib/dispatch_queue_rb/concurrent_queue.rb +116 -0
- data/lib/dispatch_queue_rb/dispatch.rb +66 -0
- data/lib/dispatch_queue_rb/dispatch_group.rb +72 -0
- data/lib/dispatch_queue_rb/internal/condition_variable_pool.rb +33 -0
- data/lib/dispatch_queue_rb/internal/continuation.rb +41 -0
- data/lib/dispatch_queue_rb/internal/heap.rb +88 -0
- data/lib/dispatch_queue_rb/internal/thread_pool_queue.rb +127 -0
- data/lib/dispatch_queue_rb/internal/thread_queue.rb +62 -0
- data/lib/dispatch_queue_rb/internal/timer_pool.rb +71 -0
- data/lib/dispatch_queue_rb/mixins/dispatch_after_impl.rb +18 -0
- data/lib/dispatch_queue_rb/mixins/dispatch_sync_impl.rb +42 -0
- data/lib/dispatch_queue_rb/serial_queue.rb +77 -0
- data/lib/dispatch_queue_rb/version.rb +12 -0
- data/rakefile.rb +110 -0
- data/test/_test_env.rb +52 -0
- data/test/test_concurrent_queue.rb +90 -0
- data/test/test_condition_variable_pool.rb +41 -0
- data/test/test_continuation.rb +23 -0
- data/test/test_dispatch.rb +91 -0
- data/test/test_dispatch_group.rb +59 -0
- data/test/test_group_concurrent_queue.rb +75 -0
- data/test/test_group_serial_queue.rb +33 -0
- data/test/test_group_thread_pool_queue.rb +34 -0
- data/test/test_heap.rb +58 -0
- data/test/test_serial_queue.rb +77 -0
- data/test/test_thread_pool_queue.rb +63 -0
- data/test/test_thread_queue.rb +77 -0
- data/test/test_timer_pool.rb +124 -0
- data/test/test_version.rb +155 -0
- metadata +181 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : lib/dispatch_queue_rb/mixins/dispatch_sync_impl.rb
|
4
|
+
# PROJECT : DispatchQueue
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
module DispatchQueue
|
11
|
+
module DispatchSyncImpl
|
12
|
+
|
13
|
+
def dispatch_sync( group:nil, &task )
|
14
|
+
mutex, condition = ConditionVariablePool.acquire()
|
15
|
+
result = nil
|
16
|
+
mutex.synchronize do
|
17
|
+
dispatch_async( group:group ) do
|
18
|
+
result = task.call()
|
19
|
+
mutex.synchronize { condition.signal() }
|
20
|
+
end
|
21
|
+
condition.wait( mutex )
|
22
|
+
end
|
23
|
+
ConditionVariablePool.release( mutex, condition )
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
def dispatch_barrier_sync( group:nil, &task )
|
28
|
+
mutex, condition = ConditionVariablePool.acquire()
|
29
|
+
result = nil
|
30
|
+
mutex.synchronize do
|
31
|
+
dispatch_barrier_async( group:group ) do
|
32
|
+
result = task.call()
|
33
|
+
mutex.synchronize { condition.signal() }
|
34
|
+
end
|
35
|
+
condition.wait( mutex )
|
36
|
+
end
|
37
|
+
ConditionVariablePool.release( mutex, condition )
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
end # class DispatchSyncImpl
|
42
|
+
end # module DispatchQueue
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : lib/dispatch_queue_rb/serial_queue.rb
|
4
|
+
# PROJECT : DispatchQueue
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
module DispatchQueue
|
11
|
+
class SerialQueue
|
12
|
+
|
13
|
+
def initialize( parent_queue: nil )
|
14
|
+
@mutex = Mutex.new
|
15
|
+
@condition = ConditionVariable.new
|
16
|
+
@task_list = []
|
17
|
+
@active = false
|
18
|
+
@parent_queue = parent_queue || Dispatch.default_queue
|
19
|
+
end
|
20
|
+
|
21
|
+
def dispatch_async( group:nil, &task )
|
22
|
+
group.enter() if group
|
23
|
+
continuation = Continuation.new( target_queue:@parent_queue, group:group ) do
|
24
|
+
_run_task( task )
|
25
|
+
end
|
26
|
+
|
27
|
+
continuation.run() if _try_activate_or_enqueue( continuation )
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :dispatch_barrier_async, :dispatch_async
|
32
|
+
|
33
|
+
include DispatchSyncImpl
|
34
|
+
include DispatchAfterImpl
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
private
|
39
|
+
def _try_activate_or_enqueue( continuation )
|
40
|
+
@mutex.synchronize do
|
41
|
+
if (!@active)
|
42
|
+
@active = true
|
43
|
+
return true
|
44
|
+
else
|
45
|
+
@task_list << continuation
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def _run_task( task )
|
52
|
+
previous_queue = Thread.current[:current_queue]
|
53
|
+
Thread.current[:current_queue] = self
|
54
|
+
|
55
|
+
begin
|
56
|
+
task.call()
|
57
|
+
ensure
|
58
|
+
Thread.current[:current_queue] = previous_queue
|
59
|
+
_dispatch_next_task()
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def _dispatch_next_task()
|
64
|
+
continuation = _suspend_or_next_task()
|
65
|
+
continuation.run() if continuation
|
66
|
+
end
|
67
|
+
|
68
|
+
def _suspend_or_next_task()
|
69
|
+
@mutex.synchronize do
|
70
|
+
return @task_list.shift if !@task_list.empty?
|
71
|
+
@active = false
|
72
|
+
return nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end # class SerialQueue
|
77
|
+
end # module DispatchQueue
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : lib/dispatch_queue_rb/version.rb
|
4
|
+
# PROJECT : DispatchQueueRb
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
module DispatchQueue
|
11
|
+
VERSION = '1.0.0'
|
12
|
+
end
|
data/rakefile.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : rakefile.rb
|
4
|
+
# PROJECT : DispatchQueueRb
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
|
11
|
+
require 'bundler/gem_tasks'
|
12
|
+
require 'rake/testtask'
|
13
|
+
|
14
|
+
task default: [:test, :build]
|
15
|
+
|
16
|
+
Rake::TestTask.new do |t|
|
17
|
+
t.libs << '.' << 'test'
|
18
|
+
t.test_files = FileList['test/**/test_*.rb']
|
19
|
+
t.verbose = false
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
# ----------------------------------------------------------------------------
|
25
|
+
# Definitions to help formating 'rake watch' results
|
26
|
+
# ----------------------------------------------------------------------------
|
27
|
+
|
28
|
+
TERM_WIDTH = `tput cols`.to_i || 80
|
29
|
+
|
30
|
+
def tty_red(str); "\e[31m#{str}\e[0m" end
|
31
|
+
def tty_green(str); "\e[32m#{str}\e[0m" end
|
32
|
+
def tty_blink(str); "\e[5m#{str}\e[25m" end
|
33
|
+
def tty_reverse_color(str); "\e[7m#{str}\e[27m" end
|
34
|
+
|
35
|
+
def print_separator( success = true )
|
36
|
+
if success
|
37
|
+
puts tty_green( "-" * TERM_WIDTH )
|
38
|
+
else
|
39
|
+
puts tty_reverse_color(tty_red( "-" * TERM_WIDTH ))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
# ----------------------------------------------------------------------------
|
46
|
+
# Definition of watch task, that monitors the project folder for any relevant
|
47
|
+
# file change and runs the unit test of the project.
|
48
|
+
# ----------------------------------------------------------------------------
|
49
|
+
|
50
|
+
begin
|
51
|
+
require 'watch'
|
52
|
+
|
53
|
+
desc 'Run unit tests everytime a source or test file is changed'
|
54
|
+
task :watch do
|
55
|
+
Watch.new( '**/*.rb' ) do
|
56
|
+
success = system "clear && rake test"
|
57
|
+
print_separator( success )
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
rescue LoadError
|
62
|
+
|
63
|
+
desc 'Run unit tests everytime a source or test file is changed'
|
64
|
+
task :watch do
|
65
|
+
puts
|
66
|
+
puts "'rake watch' requires the watch gem to be available"
|
67
|
+
puts
|
68
|
+
puts "To install:"
|
69
|
+
puts " gem install watch"
|
70
|
+
puts " or "
|
71
|
+
puts " sudo gem install watch"
|
72
|
+
puts
|
73
|
+
fail
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
# ----------------------------------------------------------------------------
|
80
|
+
# Definition of add_class[class_name] task, that uses folder_template to add
|
81
|
+
# a new class to a ruvy project
|
82
|
+
# ----------------------------------------------------------------------------
|
83
|
+
|
84
|
+
PROJECT_CONTEXT = {
|
85
|
+
project_name: "dispatch_queue_rb",
|
86
|
+
project_namespace: "DispatchQueue",
|
87
|
+
copyright_owner: "Marc-Antoine Argenton",
|
88
|
+
copyright_year: "2016",
|
89
|
+
}
|
90
|
+
|
91
|
+
begin
|
92
|
+
require 'folder_template'
|
93
|
+
|
94
|
+
task :add_class, :class_name do |t, args|
|
95
|
+
context = PROJECT_CONTEXT.merge( name:args[:class_name])
|
96
|
+
FolderTemplate::SetupFolderCmd.run( '.', 'rubyclass', context )
|
97
|
+
end
|
98
|
+
rescue LoadError
|
99
|
+
task :add_class, :class_name do |t, args|
|
100
|
+
puts
|
101
|
+
puts "'rake add_class[class_name]' task requires the folder_template gem to be available"
|
102
|
+
puts
|
103
|
+
puts "To install:"
|
104
|
+
puts " gem install folder_template"
|
105
|
+
puts " or "
|
106
|
+
puts " sudo gem install folder_template"
|
107
|
+
puts
|
108
|
+
fail
|
109
|
+
end
|
110
|
+
end
|
data/test/_test_env.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : test/_test_env.rb
|
4
|
+
# PROJECT : DispatchQueueRb
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
require 'minitest/autorun'
|
11
|
+
require 'minitest/reporters'
|
12
|
+
require 'fileutils'
|
13
|
+
require 'pp'
|
14
|
+
require 'rr'
|
15
|
+
require 'timeout'
|
16
|
+
|
17
|
+
require 'lib/dispatch_queue_rb.rb'
|
18
|
+
|
19
|
+
# Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|
20
|
+
# Minitest.backtrace_filter
|
21
|
+
# Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new( color:true ), Minitest::Reporters::JUnitReporter.new]
|
22
|
+
|
23
|
+
|
24
|
+
Thread.abort_on_exception = true
|
25
|
+
|
26
|
+
module Minitest::Assertions
|
27
|
+
def assert_must_timeout( timeout_delay = 0.001 )
|
28
|
+
success = false
|
29
|
+
begin
|
30
|
+
timeout( timeout_delay ) do
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
rescue Timeout::Error => e
|
34
|
+
success = true
|
35
|
+
end
|
36
|
+
|
37
|
+
assert success, "Expected operation to not complete within #{timeout_delay}s, but did"
|
38
|
+
end
|
39
|
+
|
40
|
+
def assert_wont_timeout( timeout_delay = 0.001 )
|
41
|
+
success = true
|
42
|
+
begin
|
43
|
+
timeout( timeout_delay ) do
|
44
|
+
yield
|
45
|
+
end
|
46
|
+
rescue Timeout::Error => e
|
47
|
+
success = false
|
48
|
+
end
|
49
|
+
|
50
|
+
assert success, "Expected operation to complete within #{timeout_delay}s, but did not"
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : test/test_concurrent_queue.rb
|
4
|
+
# PROJECT : DispatchQueue
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
|
11
|
+
require '_test_env.rb'
|
12
|
+
|
13
|
+
module DispatchQueue
|
14
|
+
describe ConcurrentQueue do
|
15
|
+
|
16
|
+
let( :task_count ) { 10 }
|
17
|
+
let( :log ) { [] }
|
18
|
+
let( :lock ) { Mutex.new }
|
19
|
+
let( :logger ) { Proc.new { |e, d| lock.synchronize { log << [e, d] } } }
|
20
|
+
subject { ConcurrentQueue.new }
|
21
|
+
|
22
|
+
it "can initialize a serial queue" do
|
23
|
+
subject.must_be_instance_of ConcurrentQueue
|
24
|
+
end
|
25
|
+
|
26
|
+
def task( task_id )
|
27
|
+
logger.call( :begin_task, { task_id:task_id, thread:Thread.current } )
|
28
|
+
sleep(0.001)
|
29
|
+
logger.call( :end_task, { task_id:task_id, thread:Thread.current } )
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "dispatch_async" do
|
33
|
+
it "executes tasks concurrently" do
|
34
|
+
(1..task_count).each { |i| subject.dispatch_async { task( i ) } }
|
35
|
+
subject.dispatch_barrier_sync {} # wait for all previous tasks to complete
|
36
|
+
|
37
|
+
running_count = 0
|
38
|
+
parallel_task_count = log.map do |e,d|
|
39
|
+
running_count += 1 if e == :begin_task
|
40
|
+
running_count -= 1 if e == :end_task
|
41
|
+
running_count
|
42
|
+
end
|
43
|
+
|
44
|
+
parallel_task_count.max.must_be :>, 1, parallel_task_count
|
45
|
+
end
|
46
|
+
|
47
|
+
it "preserves ordering arround barrier" do
|
48
|
+
(1..task_count).each { |i| subject.dispatch_async { task( "a#{i}".to_sym ) } }
|
49
|
+
subject.dispatch_barrier_async { task( "b0".to_sym ) }
|
50
|
+
(1..task_count).each { |i| subject.dispatch_async { task( "c#{i}".to_sym ) } }
|
51
|
+
subject.dispatch_barrier_async { task( "d0".to_sym ) }
|
52
|
+
|
53
|
+
subject.dispatch_barrier_sync {} # wait for all previous tasks to complete
|
54
|
+
|
55
|
+
task_id_list = log.select { |e,d| e == :begin_task }.map { |e,d| d[:task_id] }
|
56
|
+
task_chunks = task_id_list.chunk { |e| e.to_s[0] }.map { |c, l| l }
|
57
|
+
task_chunks.count.must_be :==, 4, task_chunks
|
58
|
+
end
|
59
|
+
|
60
|
+
it "resumes execution after synchronous barrier" do
|
61
|
+
(1..task_count).each { |i| subject.dispatch_async { task( "a#{i}".to_sym ) } }
|
62
|
+
subject.dispatch_barrier_sync { task( "b0".to_sym ) }
|
63
|
+
(1..task_count).each { |i| subject.dispatch_async { task( "c#{i}".to_sym ) } }
|
64
|
+
subject.dispatch_barrier_sync { task( "d0".to_sym ) }
|
65
|
+
|
66
|
+
subject.dispatch_barrier_sync {} # wait for all previous tasks to complete
|
67
|
+
|
68
|
+
task_id_list = log.select { |e,d| e == :begin_task }.map { |e,d| d[:task_id] }
|
69
|
+
task_chunks = task_id_list.chunk { |e| e.to_s[0] }.map { |c, l| l }
|
70
|
+
task_chunks.count.must_be :==, 4, task_chunks
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "with multiple concurrent queues" do
|
75
|
+
let( :subject2 ) { ConcurrentQueue.new }
|
76
|
+
|
77
|
+
it "interleaves barrier tasks from different queues" do
|
78
|
+
(1..task_count).each { |i| subject.dispatch_barrier_async { task( "a#{i}".to_sym ) } }
|
79
|
+
(1..task_count).each { |i| subject2.dispatch_barrier_async { task( "b#{i}".to_sym ) } }
|
80
|
+
subject.dispatch_barrier_sync {} # wait for all previous tasks to complete
|
81
|
+
subject2.dispatch_barrier_sync {} # wait for all previous tasks to complete
|
82
|
+
|
83
|
+
task_id_list = log.select { |e,d| e == :begin_task }.map { |e,d| d[:task_id] }
|
84
|
+
task_chunks = task_id_list.chunk { |e| e.to_s[0] }.map { |c, l| l }
|
85
|
+
task_chunks.count.must_be :>, 3, task_chunks
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : test/test_condition_variable_pool.rb
|
4
|
+
# PROJECT : DispatchQueue
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
|
11
|
+
require '_test_env.rb'
|
12
|
+
|
13
|
+
module DispatchQueue
|
14
|
+
describe ConditionVariablePool do
|
15
|
+
|
16
|
+
describe "acquire" do
|
17
|
+
it "returns a pair of mutex and condition variable" do
|
18
|
+
mutex, condition = ConditionVariablePool.acquire()
|
19
|
+
mutex.must_be_instance_of Mutex
|
20
|
+
condition.must_be_instance_of ConditionVariable
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "release" do
|
25
|
+
it "releases mutex and condition variable" do
|
26
|
+
mutex, condition = ConditionVariablePool.acquire()
|
27
|
+
ConditionVariablePool.release( mutex, condition )
|
28
|
+
end
|
29
|
+
|
30
|
+
it "recycles released mutex and condition variable" do
|
31
|
+
mutex, condition = ConditionVariablePool.acquire()
|
32
|
+
ConditionVariablePool.release( mutex, condition )
|
33
|
+
mutex2, condition2 = ConditionVariablePool.acquire()
|
34
|
+
|
35
|
+
mutex2.must_be_same_as mutex
|
36
|
+
condition2.must_be_same_as condition
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# =============================================================================
|
2
|
+
#
|
3
|
+
# MODULE : test/test_continuation.rb
|
4
|
+
# PROJECT : DispatchQueue
|
5
|
+
# DESCRIPTION :
|
6
|
+
#
|
7
|
+
# Copyright (c) 2016, Marc-Antoine Argenton. All rights reserved.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
|
11
|
+
require '_test_env.rb'
|
12
|
+
|
13
|
+
module DispatchQueue
|
14
|
+
describe Continuation do
|
15
|
+
|
16
|
+
subject { Continuation.new }
|
17
|
+
|
18
|
+
it "passes this one" do
|
19
|
+
subject.must_be_instance_of Continuation
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|