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