concurrent 0.1
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.
- data/Rakefile +105 -0
- data/ext/concurrent/futures/extconf.rb +2 -0
- data/ext/concurrent/futures/futures.c +153 -0
- data/java/concurrent/FuturesService.java.rb +163 -0
- data/lib/concurrent/actors.rb +2 -0
- data/lib/concurrent/actors/actor.rb +83 -0
- data/lib/concurrent/actors/mailbox.rb +109 -0
- data/lib/concurrent/core.rb +33 -0
- data/lib/concurrent/futures.rb +151 -0
- data/lib/concurrent/parallel.rb +120 -0
- data/test/test_actors.rb +11 -0
- data/test/test_all.rb +3 -0
- data/test/test_futures.rb +51 -0
- data/test/test_parallel.rb +34 -0
- metadata +58 -0
data/Rakefile
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
desc "Remove build products"
|
6
|
+
task :clean do
|
7
|
+
rm Dir.glob( "lib/**/*.jar" )
|
8
|
+
rm Dir.glob( "lib/**/*.#{ Config::CONFIG['DLEXT'] }" )
|
9
|
+
rm Dir.glob( "ext/**/Makefile" )
|
10
|
+
rm Dir.glob( "ext/**/*.{o,#{ Config::CONFIG['DLEXT'] }}" )
|
11
|
+
rm Dir.glob( "java/**/*.{class,jar}" )
|
12
|
+
rm Dir.glob( "java/concurrent/FuturesService.java" )
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup_extension( dir, lib_dir, extension )
|
16
|
+
ext = File.join( "ext", dir )
|
17
|
+
so_name = "#{ extension }.#{ Config::CONFIG['DLEXT'] }"
|
18
|
+
ext_so = File.join( ext, so_name )
|
19
|
+
lib_so = File.join( "lib", lib_dir, so_name )
|
20
|
+
ext_files = FileList[ File.join( ext, "*.{c,h}" ) ]
|
21
|
+
ext_makefile = File.join( ext, "Makefile" )
|
22
|
+
extconf_rb = File.join( ext, "extconf.rb" )
|
23
|
+
|
24
|
+
file ext_makefile => [ extconf_rb ] do
|
25
|
+
Dir.chdir ext do
|
26
|
+
ruby "extconf.rb"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
file ext_so => ext_files + [ ext_makefile ] do
|
31
|
+
Dir.chdir ext do
|
32
|
+
case PLATFORM
|
33
|
+
when /win32/
|
34
|
+
sh 'nmake'
|
35
|
+
else
|
36
|
+
sh 'make'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
file lib_so => [ ext_so ] do
|
42
|
+
cp ext_so, lib_so
|
43
|
+
end
|
44
|
+
|
45
|
+
task :compile => [ lib_so ]
|
46
|
+
end
|
47
|
+
|
48
|
+
case RUBY_PLATFORM
|
49
|
+
when /java/
|
50
|
+
file 'java/concurrent/FuturesService.java' => [ 'java/concurrent/FuturesService.java.rb' ] do
|
51
|
+
sh File.join( ENV['JRUBY_HOME'], 'bin/jruby' ), 'java/concurrent/FuturesService.java.rb', 'java/concurrent/FuturesService.java'
|
52
|
+
end
|
53
|
+
file 'lib/concurrent/futures.jar' => [ 'java/concurrent/FuturesService.java' ] do
|
54
|
+
Dir.chdir( 'java' ) do
|
55
|
+
sh 'javac', '-classpath', File.join( ENV['JRUBY_HOME'], 'lib/jruby.jar' ), 'concurrent/FuturesService.java'
|
56
|
+
sh 'jar', 'cf', 'concurrent/futures.jar', *Dir.glob( 'concurrent/**/*.class' )
|
57
|
+
end
|
58
|
+
cp 'java/concurrent/futures.jar', 'lib/concurrent/futures.jar'
|
59
|
+
end
|
60
|
+
task :compile => [ "lib/concurrent/futures.jar" ]
|
61
|
+
else
|
62
|
+
setup_extension( 'concurrent/futures', 'concurrent', 'futures' )
|
63
|
+
end
|
64
|
+
|
65
|
+
desc "Compile extensions"
|
66
|
+
task :compile
|
67
|
+
|
68
|
+
task :test => [ :compile ]
|
69
|
+
Rake::TestTask.new do |task|
|
70
|
+
task.libs << 'lib'
|
71
|
+
task.libs << 'test'
|
72
|
+
task.test_files = [ "test/test_all.rb" ]
|
73
|
+
task.verbose = true
|
74
|
+
end
|
75
|
+
|
76
|
+
gemspec = Gem::Specification.new do |gemspec|
|
77
|
+
gemspec.name = "concurrent"
|
78
|
+
gemspec.version = "0.1"
|
79
|
+
gemspec.author = "MenTaLguY <mental@rydia.net>"
|
80
|
+
gemspec.summary = "Omnibus concurrency library for Ruby"
|
81
|
+
gemspec.test_file = 'test/test_all.rb'
|
82
|
+
gemspec.files = FileList[ 'Rakefile', 'test/*.rb', 'ext/**/*.{c,h,rb}',
|
83
|
+
'java/**/*.{java,rb}',
|
84
|
+
"lib/**/*.{rb,jar,#{ Config::CONFIG['DLEXT'] }}" ]
|
85
|
+
gemspec.require_paths = [ 'lib' ]
|
86
|
+
|
87
|
+
case RUBY_PLATFORM
|
88
|
+
when /java/
|
89
|
+
gemspec.platform = 'jruby'
|
90
|
+
when /win32/
|
91
|
+
gemspec.platform = Gem::Platform::WIN32
|
92
|
+
else
|
93
|
+
gemspec.platform = Gem::Platform::RUBY
|
94
|
+
gemspec.extensions = FileList[ 'ext/**/extconf.rb' ]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
task :package => [ :clean, :test ]
|
99
|
+
Rake::GemPackageTask.new( gemspec ) do |task|
|
100
|
+
task.gem_spec = gemspec
|
101
|
+
task.need_tar = true
|
102
|
+
end
|
103
|
+
|
104
|
+
task :default => [ :clean, :test ]
|
105
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
/*
|
2
|
+
* concurrent/futures - futures and lazy evaluation for Ruby
|
3
|
+
*
|
4
|
+
* Copyright (C) 2007 MenTaLguY <mental@rydia.net>
|
5
|
+
*
|
6
|
+
* This file is made available under the same terms as Ruby.
|
7
|
+
*/
|
8
|
+
|
9
|
+
#include "ruby.h"
|
10
|
+
#include "rubysig.h"
|
11
|
+
#include "intern.h"
|
12
|
+
|
13
|
+
static VALUE mConcurrent;
|
14
|
+
static VALUE mFutures;
|
15
|
+
static VALUE eAsyncError;
|
16
|
+
static VALUE cThunk;
|
17
|
+
|
18
|
+
static ID value_id;
|
19
|
+
static ID inspect_id;
|
20
|
+
static ID respond_to_p_id;
|
21
|
+
|
22
|
+
typedef struct {
|
23
|
+
VALUE source;
|
24
|
+
VALUE value;
|
25
|
+
} Thunk;
|
26
|
+
|
27
|
+
static void thunk_copy_atomic(Thunk *to, Thunk const *from) {
|
28
|
+
int saved_critical;
|
29
|
+
saved_critical = rb_thread_critical;
|
30
|
+
rb_thread_critical = 1;
|
31
|
+
*to = *from;
|
32
|
+
rb_thread_critical = saved_critical;
|
33
|
+
}
|
34
|
+
|
35
|
+
static VALUE thunk_value(VALUE obj, int evaluate) {
|
36
|
+
VALUE original;
|
37
|
+
|
38
|
+
original = obj;
|
39
|
+
|
40
|
+
while ( CLASS_OF(obj) == cThunk ) {
|
41
|
+
Thunk *thunk;
|
42
|
+
Thunk copy;
|
43
|
+
|
44
|
+
Data_Get_Struct(obj, Thunk, thunk);
|
45
|
+
thunk_copy_atomic(©, thunk);
|
46
|
+
|
47
|
+
if (RTEST(copy.source)) {
|
48
|
+
if (evaluate) {
|
49
|
+
copy.value = rb_funcall(copy.source, value_id, 0);
|
50
|
+
copy.source = Qnil;
|
51
|
+
thunk_copy_atomic(thunk, ©);
|
52
|
+
}
|
53
|
+
|
54
|
+
if ( obj != original ) {
|
55
|
+
Thunk *original_thunk;
|
56
|
+
Data_Get_Struct(original, Thunk, original_thunk);
|
57
|
+
thunk_copy_atomic(original_thunk, ©);
|
58
|
+
}
|
59
|
+
|
60
|
+
if (!evaluate) {
|
61
|
+
break;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
obj = copy.value;
|
66
|
+
}
|
67
|
+
|
68
|
+
return obj;
|
69
|
+
}
|
70
|
+
|
71
|
+
static VALUE thunk_eval(VALUE thunk) {
|
72
|
+
return thunk_value(thunk, 1);
|
73
|
+
}
|
74
|
+
|
75
|
+
static VALUE thunk_ground(VALUE thunk) {
|
76
|
+
return thunk_value(thunk, 0);
|
77
|
+
}
|
78
|
+
|
79
|
+
void thunk_mark(Thunk const *thunk) {
|
80
|
+
rb_gc_mark(thunk->source);
|
81
|
+
rb_gc_mark(thunk->value);
|
82
|
+
}
|
83
|
+
|
84
|
+
void thunk_free(Thunk *thunk) {
|
85
|
+
free(thunk);
|
86
|
+
}
|
87
|
+
|
88
|
+
static VALUE rb_thunk_new(VALUE klass, VALUE source) {
|
89
|
+
Thunk *thunk;
|
90
|
+
|
91
|
+
thunk = (Thunk *)malloc(sizeof(Thunk));
|
92
|
+
|
93
|
+
thunk->source = source;
|
94
|
+
thunk->value = Qnil;
|
95
|
+
|
96
|
+
return Data_Wrap_Struct(cThunk, thunk_mark, thunk_free, thunk);
|
97
|
+
}
|
98
|
+
|
99
|
+
static VALUE wrap_exception(VALUE unused, VALUE ex) {
|
100
|
+
rb_exc_raise(rb_funcall(eAsyncError, rb_intern("new"), 1, ex));
|
101
|
+
}
|
102
|
+
|
103
|
+
static VALUE rb_thunk_method_missing(int argc, VALUE *argv, VALUE self) {
|
104
|
+
ID name;
|
105
|
+
|
106
|
+
name = SYM2ID(argv[0]);
|
107
|
+
self = thunk_ground(self);
|
108
|
+
|
109
|
+
if ( CLASS_OF(self) == cThunk ) {
|
110
|
+
if ( name == inspect_id && argc == 1 ) {
|
111
|
+
Thunk *thunk;
|
112
|
+
Data_Get_Struct(self, Thunk, thunk);
|
113
|
+
/* FIXME: thunk->source might be nil by the time we get here */
|
114
|
+
return rb_str_plus(rb_str_plus(rb_str_new2("#<Thunk "), rb_funcall(thunk->source, inspect_id, 0)), rb_str_new2(">"));
|
115
|
+
} else if ( name == respond_to_p_id && argc == 2 ) {
|
116
|
+
if ( ID2SYM(inspect_id) == argv[1] ||
|
117
|
+
ID2SYM(respond_to_p_id) == argv[1] )
|
118
|
+
{
|
119
|
+
return Qtrue;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
self = rb_rescue2(thunk_eval, self, wrap_exception, Qnil, rb_cObject, 0);
|
125
|
+
|
126
|
+
return rb_funcall3(self, name, argc-1, argv+1);
|
127
|
+
}
|
128
|
+
|
129
|
+
static VALUE rb_thunk_value(VALUE self, VALUE thunk_v) {
|
130
|
+
return thunk_eval(thunk_v);
|
131
|
+
}
|
132
|
+
|
133
|
+
void Init_futures() {
|
134
|
+
value_id = rb_intern("value");
|
135
|
+
inspect_id = rb_intern("inspect");
|
136
|
+
respond_to_p_id = rb_intern("respond_to?");
|
137
|
+
|
138
|
+
mConcurrent = rb_define_module("Concurrent");
|
139
|
+
mFutures = rb_define_module_under(mConcurrent, "Futures");
|
140
|
+
|
141
|
+
eAsyncError = rb_define_class_under(mFutures, "AsyncError", rb_eRuntimeError);
|
142
|
+
|
143
|
+
cThunk = rb_class_boot(0); /* not Qnil */
|
144
|
+
rb_singleton_class(cThunk);
|
145
|
+
rb_undef_alloc_func(cThunk);
|
146
|
+
rb_const_set(mFutures, rb_intern("Thunk"), cThunk);
|
147
|
+
|
148
|
+
rb_define_singleton_method(cThunk, "new", rb_thunk_new, 1);
|
149
|
+
rb_define_singleton_method(cThunk, "value", rb_thunk_value, 1);
|
150
|
+
rb_define_private_method(cThunk, "method_missing",
|
151
|
+
rb_thunk_method_missing, -1);
|
152
|
+
}
|
153
|
+
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'java'
|
2
|
+
|
3
|
+
begin
|
4
|
+
File.open(ARGV[0], "w") do |stream|
|
5
|
+
stream.print <<EOS
|
6
|
+
/***** BEGIN LICENSE BLOCK *****
|
7
|
+
* Version: CPL 1.0/GPL 2.0/LGPL 2.1
|
8
|
+
*
|
9
|
+
* The contents of this file are subject to the Common Public
|
10
|
+
* License Version 1.0 (the "License"); you may not use this file
|
11
|
+
* except in compliance with the License. You may obtain a copy of
|
12
|
+
* the License at http://www.eclipse.org/legal/cpl-v10.html
|
13
|
+
*
|
14
|
+
* Software distributed under the License is distributed on an "AS
|
15
|
+
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
16
|
+
* implied. See the License for the specific language governing
|
17
|
+
* rights and limitations under the License.
|
18
|
+
*
|
19
|
+
* Copyright (C) 2007 MenTaLguY <mental@rydia.net>
|
20
|
+
*
|
21
|
+
* Alternatively, the contents of this file may be used under the terms of
|
22
|
+
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
23
|
+
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
24
|
+
* in which case the provisions of the GPL or the LGPL are applicable instead
|
25
|
+
* of those above. If you wish to allow use of your version of this file only
|
26
|
+
* under the terms of either the GPL or the LGPL, and not to allow others to
|
27
|
+
* use your version of this file under the terms of the CPL, indicate your
|
28
|
+
* decision by deleting the provisions above and replace them with the notice
|
29
|
+
* and other provisions required by the GPL or the LGPL. If you do not delete
|
30
|
+
* the provisions above, a recipient may use your version of this file under
|
31
|
+
* the terms of any one of the CPL, the GPL or the LGPL.
|
32
|
+
***** END LICENSE BLOCK *****/
|
33
|
+
|
34
|
+
package concurrent;
|
35
|
+
|
36
|
+
import java.io.IOException;
|
37
|
+
|
38
|
+
import org.jruby.Ruby;
|
39
|
+
import org.jruby.RubyClass;
|
40
|
+
import org.jruby.RubyException;
|
41
|
+
import org.jruby.exceptions.RaiseException;
|
42
|
+
import org.jruby.runtime.Block;
|
43
|
+
import org.jruby.runtime.CallbackFactory;
|
44
|
+
import org.jruby.runtime.callback.Callback;
|
45
|
+
import org.jruby.runtime.ObjectAllocator;
|
46
|
+
import org.jruby.runtime.load.BasicLibraryService;
|
47
|
+
import org.jruby.runtime.builtin.IRubyObject;
|
48
|
+
|
49
|
+
public class FuturesService implements BasicLibraryService {
|
50
|
+
public boolean basicLoad(final Ruby runtime) throws IOException {
|
51
|
+
Thunk.setup(runtime);
|
52
|
+
return true;
|
53
|
+
}
|
54
|
+
|
55
|
+
public static class Thunk implements IRubyObject {
|
56
|
+
private Ruby runtime;
|
57
|
+
private RubyClass klass;
|
58
|
+
private IRubyObject source;
|
59
|
+
private IRubyObject value;
|
60
|
+
|
61
|
+
Thunk(Ruby runtime, RubyClass klass, IRubyObject source) {
|
62
|
+
this.runtime = runtime;
|
63
|
+
this.klass = klass;
|
64
|
+
this.source = source;
|
65
|
+
this.value = null;
|
66
|
+
}
|
67
|
+
|
68
|
+
public static IRubyObject newInstance(IRubyObject recv, IRubyObject source, Block block) {
|
69
|
+
return new Thunk(recv.getRuntime(), (RubyClass)recv, source);
|
70
|
+
}
|
71
|
+
|
72
|
+
public static void setup(final Ruby runtime) throws IOException {
|
73
|
+
RubyClass cThunk = runtime.getOrCreateModule("Concurrent").defineModuleUnder("Futures").defineClassUnder("Thunk", null, ObjectAllocator.NOT_ALLOCATABLE_ALLOCATOR);
|
74
|
+
CallbackFactory cb = runtime.callbackFactory(Thunk.class);
|
75
|
+
cThunk.getMetaClass().defineMethod("new", cb.getSingletonMethod("newInstance", IRubyObject.class));
|
76
|
+
cThunk.defineMethod("value", cb.getSingletonMethod("value", IRubyObject.class));
|
77
|
+
}
|
78
|
+
|
79
|
+
public static IRubyObject thunkValue(IRubyObject obj, boolean evaluate) {
|
80
|
+
IRubyObject original=obj;
|
81
|
+
|
82
|
+
while (obj instanceof Thunk) {
|
83
|
+
Thunk thunk=(Thunk)obj;
|
84
|
+
|
85
|
+
synchronized (thunk) {
|
86
|
+
if ( thunk.source != null ) {
|
87
|
+
if (evaluate) {
|
88
|
+
thunk.value = thunk.source.callMethod(thunk.source.getRuntime().getCurrentContext(), "value");
|
89
|
+
thunk.source = null;
|
90
|
+
}
|
91
|
+
|
92
|
+
if ( obj != original ) {
|
93
|
+
Thunk original_thunk = (Thunk)original;
|
94
|
+
synchronized (original_thunk) {
|
95
|
+
original_thunk.value = thunk.value;
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
if (!evaluate) {
|
100
|
+
break;
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
obj = thunk.value;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
return obj;
|
109
|
+
}
|
110
|
+
|
111
|
+
public static IRubyObject evalThunk(IRubyObject obj) {
|
112
|
+
try {
|
113
|
+
return thunkValue(obj, true);
|
114
|
+
} catch (RaiseException e) {
|
115
|
+
RubyClass cAsyncError = obj.getRuntime().getModule("Concurrent").defineModuleUnder("Futures").getClass("AsyncError");
|
116
|
+
RubyException e2 = (RubyException)cAsyncError.callMethod(obj.getRuntime().getCurrentContext(), "new", e.getException());
|
117
|
+
throw new RaiseException(e2);
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
public static IRubyObject value(IRubyObject recv, IRubyObject obj, Block block) {
|
122
|
+
return thunkValue(obj, true);
|
123
|
+
}
|
124
|
+
|
125
|
+
/*** Generated wrapper methods ***/
|
126
|
+
EOS
|
127
|
+
|
128
|
+
def demangle( type )
|
129
|
+
case type
|
130
|
+
when /^\[L(.*);$/
|
131
|
+
"#{ demangle( $1 ) }[]"
|
132
|
+
else
|
133
|
+
type
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
Modifier = java.lang.reflect.Modifier
|
138
|
+
|
139
|
+
c = java.lang.Class.for_name("org.jruby.runtime.builtin.IRubyObject")
|
140
|
+
c.get_declared_methods.each do |method|
|
141
|
+
parameter_types = method.get_parameter_types.map { |t| demangle( t.to_s ) }
|
142
|
+
parameter_names = (0...(parameter_types.length)).map { |i| "p#{i}" }
|
143
|
+
return_type = demangle( method.get_return_type.to_s )
|
144
|
+
stream.print <<EOS
|
145
|
+
|
146
|
+
#{ Modifier.to_string( method.get_modifiers & ~Modifier::ABSTRACT ) } #{ return_type } #{ method.get_name }(#{ parameter_types.zip( parameter_names ).map { |t, n| "#{t} #{n}" }.join(", ")}) {
|
147
|
+
#{ return_type == "void" ? "" : "return" } evalThunk(this).#{ method.get_name }(#{ parameter_names.join(", ") });
|
148
|
+
}
|
149
|
+
EOS
|
150
|
+
end
|
151
|
+
|
152
|
+
stream.print <<EOS
|
153
|
+
}
|
154
|
+
}
|
155
|
+
EOS
|
156
|
+
end
|
157
|
+
rescue Exception => e
|
158
|
+
begin
|
159
|
+
File.unlink(ARGV[0])
|
160
|
+
rescue Errno::ENOENT
|
161
|
+
end
|
162
|
+
raise e
|
163
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
#
|
2
|
+
# concurrent/actors/actor - class representing actors
|
3
|
+
#
|
4
|
+
# Copyright 2007 MenTaLguY <mental@rydia.net>
|
5
|
+
#
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
# Redistribution and use in source and binary forms, with or without
|
9
|
+
# modification, are permitted provided that the following conditions are met:
|
10
|
+
#
|
11
|
+
# * Redistributions of source code must retain the above copyright notice,
|
12
|
+
# thi slist of conditions and the following disclaimer.
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright notice
|
14
|
+
# this list of conditions and the following disclaimer in the documentatio
|
15
|
+
# and/or other materials provided with the distribution.
|
16
|
+
# * Neither the name of the Evan Phoenix nor the names of its contributors
|
17
|
+
# may be used to endorse or promote products derived from this software
|
18
|
+
# without specific prior written permission.
|
19
|
+
#
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
23
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
24
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
25
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
26
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
27
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
28
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
29
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
30
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
31
|
+
|
32
|
+
require 'thread'
|
33
|
+
begin
|
34
|
+
require 'fastthread'
|
35
|
+
rescue LoadError
|
36
|
+
end
|
37
|
+
require 'concurrent/core'
|
38
|
+
require 'concurrent/actors/mailbox.rb'
|
39
|
+
|
40
|
+
module Concurrent
|
41
|
+
module Actors
|
42
|
+
|
43
|
+
class Actor
|
44
|
+
class << self
|
45
|
+
alias :private_new :new
|
46
|
+
private :private_new
|
47
|
+
|
48
|
+
def spawn(&prc)
|
49
|
+
channel = Core::Channel.new
|
50
|
+
Thread.new do
|
51
|
+
channel << current
|
52
|
+
prc.call
|
53
|
+
end
|
54
|
+
channel.receive
|
55
|
+
end
|
56
|
+
alias :new :spawn
|
57
|
+
|
58
|
+
def current
|
59
|
+
Thread.current[:__current_actor__] ||= private_new(current_mailbox)
|
60
|
+
end
|
61
|
+
|
62
|
+
def current_mailbox
|
63
|
+
Thread.current[:__current_mailbox__] ||= Mailbox.new
|
64
|
+
end
|
65
|
+
private :current_mailbox
|
66
|
+
|
67
|
+
def receive(&prc)
|
68
|
+
current_mailbox.receive(&prc)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(mailbox)
|
73
|
+
@mailbox = mailbox
|
74
|
+
end
|
75
|
+
|
76
|
+
def <<(value)
|
77
|
+
@mailbox << value
|
78
|
+
self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
#
|
2
|
+
# concurrent/actors/mailbox - mailbox class supporting actor implementation
|
3
|
+
#
|
4
|
+
# Copyright 2007 MenTaLguY <mental@rydia.net>
|
5
|
+
#
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
# Redistribution and use in source and binary forms, with or without
|
9
|
+
# modification, are permitted provided that the following conditions are met:
|
10
|
+
#
|
11
|
+
# * Redistributions of source code must retain the above copyright notice,
|
12
|
+
# thi slist of conditions and the following disclaimer.
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright notice
|
14
|
+
# this list of conditions and the following disclaimer in the documentatio
|
15
|
+
# and/or other materials provided with the distribution.
|
16
|
+
# * Neither the name of the Evan Phoenix nor the names of its contributors
|
17
|
+
# may be used to endorse or promote products derived from this software
|
18
|
+
# without specific prior written permission.
|
19
|
+
#
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
23
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
24
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
25
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
26
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
27
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
28
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
29
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
30
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
31
|
+
|
32
|
+
require 'thread'
|
33
|
+
begin
|
34
|
+
require 'fastthread'
|
35
|
+
rescue LoadError
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'concurrent/core'
|
39
|
+
|
40
|
+
module Concurrent
|
41
|
+
module Actors
|
42
|
+
|
43
|
+
class Mailbox
|
44
|
+
def initialize
|
45
|
+
@channel = Core::Channel.new
|
46
|
+
@skipped = []
|
47
|
+
end
|
48
|
+
|
49
|
+
# safe for multiple writers
|
50
|
+
def <<(value)
|
51
|
+
@channel << value
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# safe only for a single reader
|
56
|
+
def receive
|
57
|
+
if block_given?
|
58
|
+
filter = Filter.new
|
59
|
+
yield filter
|
60
|
+
|
61
|
+
value = nil
|
62
|
+
action = nil
|
63
|
+
|
64
|
+
found_at = nil
|
65
|
+
@skipped.each_with_index do |obj, index|
|
66
|
+
action = filter.action_for obj
|
67
|
+
if action
|
68
|
+
value = obj
|
69
|
+
found_at = index
|
70
|
+
break
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@skipped.delete_at found_at if found_at
|
74
|
+
|
75
|
+
until action
|
76
|
+
value = @channel.receive
|
77
|
+
action = filter.action_for value
|
78
|
+
@skipped.push value unless action
|
79
|
+
end
|
80
|
+
|
81
|
+
action.call value
|
82
|
+
else
|
83
|
+
unless @skipped.empty?
|
84
|
+
@skipped.shift
|
85
|
+
else
|
86
|
+
@channel.receive
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Filter
|
92
|
+
def initialize
|
93
|
+
@pairs = []
|
94
|
+
end
|
95
|
+
|
96
|
+
def when(pattern, &action)
|
97
|
+
raise ArgumentError, "no block given" unless action
|
98
|
+
@pairs.push [pattern, action]
|
99
|
+
end
|
100
|
+
|
101
|
+
def action_for(value)
|
102
|
+
pair = @pairs.find { |pattern, action| pattern === value }
|
103
|
+
pair ? pair[1] : nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# concurrent/core - pi calculus channels and other core bits
|
2
|
+
#
|
3
|
+
# Copyright 2007 MenTaLguY <mental@rydia.net>
|
4
|
+
#
|
5
|
+
# This file is made available under the same terms as Ruby.
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'thread'
|
9
|
+
begin
|
10
|
+
require 'fastthread'
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
|
14
|
+
module Concurrent
|
15
|
+
module Core
|
16
|
+
|
17
|
+
# an asynchronous pi calculus channel
|
18
|
+
class Channel < Queue
|
19
|
+
alias :receive :shift
|
20
|
+
undef :clear
|
21
|
+
undef :deq
|
22
|
+
undef :empty?
|
23
|
+
undef :enq
|
24
|
+
undef :length
|
25
|
+
undef :num_waiting
|
26
|
+
undef :pop
|
27
|
+
undef :push
|
28
|
+
undef :shift
|
29
|
+
undef :size
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#
|
2
|
+
# concurrent/futures - futures and lazy evaluation for Ruby
|
3
|
+
#
|
4
|
+
# Copyright (C) 2007 MenTaLguY <mental@rydia.net>
|
5
|
+
#
|
6
|
+
# This file is made available under the same terms as Ruby.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'thread'
|
10
|
+
begin
|
11
|
+
require 'fastthread'
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
require 'thwait'
|
15
|
+
|
16
|
+
module Concurrent
|
17
|
+
module Futures
|
18
|
+
extend self
|
19
|
+
|
20
|
+
Future = self
|
21
|
+
|
22
|
+
class AsyncError < RuntimeError
|
23
|
+
attr_reader :reason
|
24
|
+
|
25
|
+
def initialize( reason, desc=nil )
|
26
|
+
super( desc )
|
27
|
+
@reason = reason
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'concurrent/futures.so' # provides Thunk
|
32
|
+
|
33
|
+
def async( &block )
|
34
|
+
Thunk.new( Thread.new( &block ) )
|
35
|
+
end
|
36
|
+
alias new async
|
37
|
+
alias spawn async
|
38
|
+
alias future async
|
39
|
+
|
40
|
+
def await( future )
|
41
|
+
Thunk.value future
|
42
|
+
end
|
43
|
+
|
44
|
+
def await_any( *futures )
|
45
|
+
threads = futures.map { |future| Thread.new { Futures::await future } }
|
46
|
+
begin
|
47
|
+
threads.index ThreadsWait.new( *threads ).wait_next
|
48
|
+
ensure
|
49
|
+
threads.each { |thread| thread.raise RuntimeError }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
AlreadyFulfilledError = Class.new StandardError
|
54
|
+
|
55
|
+
class Promise
|
56
|
+
def initialize
|
57
|
+
@lock = Mutex.new
|
58
|
+
@ready = ConditionVariable.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def future
|
62
|
+
@future ||= Thunk.new self
|
63
|
+
end
|
64
|
+
|
65
|
+
def fulfill( value )
|
66
|
+
@lock.synchronize do
|
67
|
+
if defined? @value
|
68
|
+
raise AlreadyFulfilledError, "promise already fulfilled"
|
69
|
+
end
|
70
|
+
@value = value
|
71
|
+
@ready.broadcast
|
72
|
+
end
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def fulfilled?
|
77
|
+
@lock.synchronize { defined? @value }
|
78
|
+
end
|
79
|
+
|
80
|
+
def value
|
81
|
+
@lock.synchronize do
|
82
|
+
@ready.wait @lock until defined? @value
|
83
|
+
@value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fail( ex )
|
88
|
+
@lock.synchronize do
|
89
|
+
@value = Thunk.new( Thread.new { raise ex } )
|
90
|
+
end
|
91
|
+
self
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Lazy
|
96
|
+
def initialize( &block )
|
97
|
+
@lock = Mutex.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def value
|
101
|
+
@lock.synchronize do
|
102
|
+
if @block
|
103
|
+
Support::TerminateWaitLock.synchronize do
|
104
|
+
@value = Thunk.new Thread.new( &block )
|
105
|
+
@block = nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
@value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def inspect
|
113
|
+
@lock.synchronize do
|
114
|
+
if @block
|
115
|
+
"#<Lazy pending #{ @block.inspect }>"
|
116
|
+
else
|
117
|
+
"#<Lazy requested #{ @value.inspect }>"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def lazy( &block )
|
124
|
+
Thunk.new( Lazy.new( &block ) )
|
125
|
+
end
|
126
|
+
|
127
|
+
class Ref
|
128
|
+
def initialize( value=nil )
|
129
|
+
@lock = Mutex.new
|
130
|
+
@value = value
|
131
|
+
end
|
132
|
+
|
133
|
+
def exchange( value )
|
134
|
+
@lock.synchronize do
|
135
|
+
result = @value
|
136
|
+
@value = value
|
137
|
+
result
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def modify( &block )
|
142
|
+
@lock.synchronize do
|
143
|
+
value = @value
|
144
|
+
@value = Future.async { block.call value }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
#
|
2
|
+
# concurrent/parallel - data-parallel programming for Ruby
|
3
|
+
#
|
4
|
+
# Copyright (C) 2007 MenTaLguY <mental@rydia.net>
|
5
|
+
#
|
6
|
+
# This file is made available under the same terms as Ruby.
|
7
|
+
#
|
8
|
+
|
9
|
+
module Concurrent
|
10
|
+
module Parallel
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Enumerable
|
15
|
+
def parallel_each( n, &block )
|
16
|
+
parallel_subsets( n ).map do |slice|
|
17
|
+
Thread.new { slice.each &block }
|
18
|
+
end.each do |thread|
|
19
|
+
thread.join
|
20
|
+
end
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def parallel_map( n, &block )
|
25
|
+
parallel_subsets( n ).map do |slice|
|
26
|
+
Thread.new { slice.map &block }
|
27
|
+
end.inject( [] ) do |a, thread|
|
28
|
+
a.push *thread.value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def parallel_select( n, &block )
|
33
|
+
parallel_subsets( n ).map do |slice|
|
34
|
+
Thread.new { slice.select &block }
|
35
|
+
end.inject( [] ) do |a, results|
|
36
|
+
a.push *thread.value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parallel_reject( n, &block )
|
41
|
+
parallel_subsets( n ).map do |slice|
|
42
|
+
Thread.new { slice.reject &block }
|
43
|
+
end.inject( [] ) do |a, thread|
|
44
|
+
a.push *thread.value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def parallel_max( n )
|
49
|
+
parallel_subsets( n ).map do |slice|
|
50
|
+
Thread.new { slice.max }
|
51
|
+
end.map { |t| t.value }.max
|
52
|
+
end
|
53
|
+
|
54
|
+
def parallel_min( n )
|
55
|
+
parallel_subsets( n ).map do |slice|
|
56
|
+
Thread.new { slice.min }
|
57
|
+
end.map { |t| t.value }.min
|
58
|
+
end
|
59
|
+
|
60
|
+
def parallel_partition( n, &block )
|
61
|
+
parallel_subsets( n ).map do |slice|
|
62
|
+
Thread.new { slice.partition &block }
|
63
|
+
end.inject( [ [], [] ] ) do |acc, thread|
|
64
|
+
pair = thread.value
|
65
|
+
acc[0].push *pair[0]
|
66
|
+
acc[1].push *pair[1]
|
67
|
+
acc
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def parallel_grep( re, n, &block )
|
72
|
+
parallel_subsets( n ).map do |slice|
|
73
|
+
Thread.new { slice.grep( re, &block ) }
|
74
|
+
end.inject( [] ) do |acc, thread|
|
75
|
+
acc.push *thread.value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def parallel_all?( n, &block )
|
80
|
+
parallel_subsets( n ).map do |slice|
|
81
|
+
Thread.new { slice.all? &block }
|
82
|
+
end.inject( true ) do |acc, thread|
|
83
|
+
acc && thread.value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def parallel_any?( n, &block )
|
88
|
+
parallel_subsets( n ).map do |slice|
|
89
|
+
Thread.new { slice.any? &block }
|
90
|
+
end.inject( false ) do |acc, thread|
|
91
|
+
acc || thread.value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def parallel_include?( n, obj )
|
96
|
+
parallel_subsets( n ).map do |slice|
|
97
|
+
Thread.new { slice.include? obj }
|
98
|
+
end.inject( false ) do |acc, thread|
|
99
|
+
acc || thread.value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def parallel_subsets( n )
|
104
|
+
to_a.parallel_subsets( n )
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Array
|
109
|
+
def parallel_subsets( n )
|
110
|
+
if n > 1
|
111
|
+
slice_size = size / n
|
112
|
+
(0...(( size.to_f / slice_size ).ceil)).map do |i|
|
113
|
+
self[i*slice_size, slice_size]
|
114
|
+
end
|
115
|
+
else
|
116
|
+
[ self ]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
data/test/test_actors.rb
ADDED
data/test/test_all.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'concurrent/futures'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
include Concurrent::Futures
|
6
|
+
|
7
|
+
class FuturesTests < Test::Unit::TestCase
|
8
|
+
def test_promise_fulfill
|
9
|
+
promise = Promise.new
|
10
|
+
assert !promise.fulfilled?
|
11
|
+
promise.fulfill 10
|
12
|
+
assert promise.fulfilled?
|
13
|
+
assert_equal 10, promise.future
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_promise_fulfill_twice
|
17
|
+
promise = Promise.new
|
18
|
+
promise.fulfill 10
|
19
|
+
assert_raise( AlreadyFulfilledError ) do
|
20
|
+
promise.fulfill 20
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_promise_fail
|
25
|
+
promise = Promise.new
|
26
|
+
promise.fail( EOFError.new )
|
27
|
+
value = promise.future
|
28
|
+
assert_raise( EOFError ) do
|
29
|
+
Future.await promise.value
|
30
|
+
end
|
31
|
+
assert_raise( AsyncError ) do
|
32
|
+
value + 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_future
|
37
|
+
f = Future.future { 3 }
|
38
|
+
assert_equal 3, f
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_future_raise
|
42
|
+
f = Future.future { raise EOFError, "blah" }
|
43
|
+
assert_raise( EOFError ) do
|
44
|
+
Future.await f
|
45
|
+
end
|
46
|
+
assert_raise( AsyncError ) do
|
47
|
+
f + 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'concurrent/parallel'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
class TestParallel < Test::Unit::TestCase
|
6
|
+
def check_parallel( enum, n, meth, *args, &block )
|
7
|
+
regular = enum.send( meth, *args, &block )
|
8
|
+
parallel = enum.send( "parallel_#{ meth }", n, *args, &block )
|
9
|
+
assert_equal regular, parallel
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_map
|
13
|
+
check_parallel( 0..100, 2, "map" ) { |x| x * 2 }
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_any?
|
17
|
+
check_parallel( 0..100, 2, "any?" ) { |x| ( ( x + 1 ) % 48 ).zero? }
|
18
|
+
check_parallel( 0..100, 2, "any?" )
|
19
|
+
check_parallel( [ false ] * 100, 2, "any?" )
|
20
|
+
check_parallel( [ true ] * 100, 2, "any?" )
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_all?
|
24
|
+
check_parallel( 0..100, 2, "all?" ) { |x| ( ( x + 1 ) % 48 ).zero? }
|
25
|
+
check_parallel( 0..100, 2, "all?" )
|
26
|
+
check_parallel( [ false ] * 100, 2, "all?" )
|
27
|
+
check_parallel( [ true ] * 100, 2, "all?" )
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_include?
|
31
|
+
check_parallel( 0..100, 2, "include?", 5 )
|
32
|
+
check_parallel( 0..100, 2, "include?", 1000 )
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: concurrent
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.1"
|
7
|
+
date: 2007-05-24 00:00:00 -04:00
|
8
|
+
summary: Omnibus concurrency library for Ruby
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email:
|
12
|
+
homepage:
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: false
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- MenTaLguY <mental@rydia.net>
|
30
|
+
files:
|
31
|
+
- Rakefile
|
32
|
+
- test/test_all.rb
|
33
|
+
- test/test_futures.rb
|
34
|
+
- test/test_actors.rb
|
35
|
+
- test/test_parallel.rb
|
36
|
+
- ext/concurrent/futures/futures.c
|
37
|
+
- ext/concurrent/futures/extconf.rb
|
38
|
+
- java/concurrent/FuturesService.java.rb
|
39
|
+
- lib/concurrent/actors.rb
|
40
|
+
- lib/concurrent/futures.rb
|
41
|
+
- lib/concurrent/parallel.rb
|
42
|
+
- lib/concurrent/core.rb
|
43
|
+
- lib/concurrent/actors/actor.rb
|
44
|
+
- lib/concurrent/actors/mailbox.rb
|
45
|
+
test_files:
|
46
|
+
- test/test_all.rb
|
47
|
+
rdoc_options: []
|
48
|
+
|
49
|
+
extra_rdoc_files: []
|
50
|
+
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions:
|
54
|
+
- ext/concurrent/futures/extconf.rb
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
dependencies: []
|
58
|
+
|