dispatch_queue_rb 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|