@@ -13,23 +13,34 @@ use crate::future::Future;
13
13
use crate :: task:: { Context , Poll } ;
14
14
use crate :: utils:: abort_on_panic;
15
15
16
+ const LOW_WATERMARK : u64 = 2 ;
16
17
const MAX_THREADS : u64 = 10_000 ;
17
18
18
- static DYNAMIC_THREAD_COUNT : AtomicU64 = AtomicU64 :: new ( 0 ) ;
19
+ // Pool task frequency calculation variables
20
+ static AVR_FREQUENCY : AtomicU64 = AtomicU64 :: new ( 0 ) ;
21
+ static FREQUENCY : AtomicU64 = AtomicU64 :: new ( 0 ) ;
22
+
23
+ // Pool speedup calculation variables
24
+ static SPEEDUP : AtomicU64 = AtomicU64 :: new ( 0 ) ;
25
+
26
+ // Pool size variables
27
+ static EXPECTED_POOL_SIZE : AtomicU64 = AtomicU64 :: new ( LOW_WATERMARK ) ;
28
+ static CURRENT_POOL_SIZE : AtomicU64 = AtomicU64 :: new ( LOW_WATERMARK ) ;
19
29
20
30
struct Pool {
21
31
sender : Sender < async_task:: Task < ( ) > > ,
22
- receiver : Receiver < async_task:: Task < ( ) > > ,
32
+ receiver : Receiver < async_task:: Task < ( ) > >
23
33
}
24
34
25
35
lazy_static ! {
26
36
static ref POOL : Pool = {
27
- for _ in 0 ..2 {
37
+ for _ in 0 ..LOW_WATERMARK {
28
38
thread:: Builder :: new( )
29
39
. name( "async-blocking-driver" . to_string( ) )
30
40
. spawn( || abort_on_panic( || {
31
41
for task in & POOL . receiver {
32
42
task. run( ) ;
43
+ calculate_dispatch_frequency( ) ;
33
44
}
34
45
} ) )
35
46
. expect( "cannot start a thread driving blocking tasks" ) ;
@@ -47,18 +58,30 @@ lazy_static! {
47
58
} ;
48
59
}
49
60
50
- // Create up to MAX_THREADS dynamic blocking task worker threads.
51
- // Dynamic threads will terminate themselves if they don't
52
- // receive any work after between one and ten seconds.
53
- fn maybe_create_another_blocking_thread ( ) {
54
- // We use a `Relaxed` atomic operation because
55
- // it's just a heuristic, and would not lose correctness
56
- // even if it's random.
57
- let workers = DYNAMIC_THREAD_COUNT . load ( Ordering :: Relaxed ) ;
58
- if workers >= MAX_THREADS {
59
- return ;
61
+ fn calculate_dispatch_frequency ( ) {
62
+ // Calculate current message processing rate here
63
+ let previous_freq = FREQUENCY . fetch_sub ( 1 , Ordering :: Relaxed ) ;
64
+ let avr_freq = AVR_FREQUENCY . load ( Ordering :: Relaxed ) ;
65
+ let current_pool_size = CURRENT_POOL_SIZE . load ( Ordering :: Relaxed ) ;
66
+ let frequency = ( avr_freq as f64 + previous_freq as f64 / current_pool_size as f64 ) as u64 ;
67
+ AVR_FREQUENCY . store ( frequency, Ordering :: Relaxed ) ;
68
+
69
+ // Adapt the thread count of pool
70
+ let speedup = SPEEDUP . load ( Ordering :: Relaxed ) ;
71
+ if frequency > speedup {
72
+ // Speedup can be gained. Scale the pool up here.
73
+ SPEEDUP . store ( frequency, Ordering :: Relaxed ) ;
74
+ EXPECTED_POOL_SIZE . store ( current_pool_size + 1 , Ordering :: Relaxed ) ;
75
+ } else {
76
+ // There is no need for the extra threads, schedule them to be closed.
77
+ EXPECTED_POOL_SIZE . fetch_sub ( 2 , Ordering :: Relaxed ) ;
60
78
}
79
+ }
61
80
81
+ // Creates yet another thread to receive tasks.
82
+ // Dynamic threads will terminate themselves if they don't
83
+ // receive any work after between one and ten seconds.
84
+ fn create_blocking_thread ( ) {
62
85
// We want to avoid having all threads terminate at
63
86
// exactly the same time, causing thundering herd
64
87
// effects. We want to stagger their destruction over
@@ -73,25 +96,49 @@ fn maybe_create_another_blocking_thread() {
73
96
. spawn ( move || {
74
97
let wait_limit = Duration :: from_millis ( 1000 + rand_sleep_ms) ;
75
98
76
- DYNAMIC_THREAD_COUNT . fetch_add ( 1 , Ordering :: Relaxed ) ;
99
+ CURRENT_POOL_SIZE . fetch_add ( 1 , Ordering :: Relaxed ) ;
77
100
while let Ok ( task) = POOL . receiver . recv_timeout ( wait_limit) {
78
101
abort_on_panic ( || task. run ( ) ) ;
102
+ calculate_dispatch_frequency ( ) ;
79
103
}
80
- DYNAMIC_THREAD_COUNT . fetch_sub ( 1 , Ordering :: Relaxed ) ;
104
+ CURRENT_POOL_SIZE . fetch_sub ( 1 , Ordering :: Relaxed ) ;
81
105
} )
82
106
. expect ( "cannot start a dynamic thread driving blocking tasks" ) ;
83
107
}
84
108
85
109
// Enqueues work, attempting to send to the threadpool in a
86
- // nonblocking way and spinning up another worker thread if
87
- // there is not a thread ready to accept the work.
110
+ // nonblocking way and spinning up needed amount of threads
111
+ // based on the previous statistics without relying on
112
+ // if there is not a thread ready to accept the work or not.
88
113
fn schedule ( t : async_task:: Task < ( ) > ) {
114
+ // Add up for every incoming task schedule
115
+ FREQUENCY . fetch_add ( 1 , Ordering :: Relaxed ) ;
116
+
117
+ // Calculate the amount of threads needed to spin up
118
+ // then retry sending while blocking. It doesn't spin if
119
+ // expected pool size is above the MAX_THREADS (which is a
120
+ // case won't happen)
121
+ let pool_size = EXPECTED_POOL_SIZE . load ( Ordering :: Relaxed ) ;
122
+ let current_pool_size = CURRENT_POOL_SIZE . load ( Ordering :: Relaxed ) ;
123
+ if pool_size > current_pool_size && pool_size <= MAX_THREADS {
124
+ let needed = pool_size - current_pool_size;
125
+
126
+ // For safety, check boundaries before spawning threads
127
+ if needed > 0 && ( needed < pool_size || needed < current_pool_size) {
128
+ ( 0 ..needed) . for_each ( |_| {
129
+ create_blocking_thread ( ) ;
130
+ } ) ;
131
+ }
132
+ }
133
+
89
134
if let Err ( err) = POOL . sender . try_send ( t) {
90
135
// We were not able to send to the channel without
91
- // blocking. Try to spin up another thread and then
92
- // retry sending while blocking.
93
- maybe_create_another_blocking_thread ( ) ;
136
+ // blocking.
94
137
POOL . sender . send ( err. into_inner ( ) ) . unwrap ( ) ;
138
+ } else {
139
+ // Every successful dispatch, rewarded with negative
140
+ let reward = AVR_FREQUENCY . load ( Ordering :: Relaxed ) as f64 / 2.0_f64 ;
141
+ EXPECTED_POOL_SIZE . fetch_sub ( reward as u64 , Ordering :: Relaxed ) ;
95
142
}
96
143
}
97
144
0 commit comments