summaryrefslogtreecommitdiffstats
path: root/doc/design-thoughts/thread-group.txt
blob: e845230fab8ae46b394956435b45e9163135e705 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
Thread groups
#############

2021-07-13 - first draft
==========

Objective
---------
- support multi-socket systems with limited cache-line bouncing between
  physical CPUs and/or L3 caches

- overcome the 64-thread limitation

- Support a reasonable number of groups. I.e. if modern CPUs arrive with
  core complexes made of 8 cores, with 8 CC per chip and 2 chips in a
  system, it makes sense to support 16 groups.


Non-objective
-------------
- no need to optimize to the last possible cycle. I.e. some algos like
  leastconn will remain shared across all threads, servers will keep a
  single queue, etc. Global information remains global.

- no stubborn enforcement of FD sharing. Per-server idle connection lists
  can become per-group; listeners can (and should probably) be per-group.
  Other mechanisms (like SO_REUSEADDR) can already overcome this.

- no need to go beyond 64 threads per group.


Identified tasks
================

General
-------
Everywhere tid_bit is used we absolutely need to find a complement using
either the current group or a specific one. Thread debugging will need to
be extended as masks are extensively used.


Scheduler
---------
The global run queue and global wait queue must become per-group. This
means that a task may only be queued into one of them at a time. It
sounds like tasks may only belong to a given group, but doing so would
bring back the original issue that it's impossible to perform remote wake
ups.

We could probably ignore the group if we don't need to set the thread mask
in the task. the task's thread_mask is never manipulated using atomics so
it's safe to complement it with a group.

The sleeping_thread_mask should become per-group. Thus possibly that a
wakeup may only be performed on the assigned group, meaning that either
a task is not assigned, in which case it be self-assigned (like today),
otherwise the tg to be woken up will be retrieved from the task itself.

Task creation currently takes a thread mask of either tid_bit, a specific
mask, or MAX_THREADS_MASK. How to create a task able to run anywhere
(checks, Lua, ...) ?

Profiling -> completed
---------
There should be one task_profiling_mask per thread group. Enabling or
disabling profiling should be made per group (possibly by iterating).
-> not needed anymore, one flag per thread in each thread's context.

Thread isolation
----------------
Thread isolation is difficult as we solely rely on atomic ops to figure
who can complete. Such operation is rare, maybe we could have a global
read_mostly flag containing a mask of the groups that require isolation.
Then the threads_want_rdv_mask etc can become per-group. However setting
and clearing the bits will become problematic as this will happen in two
steps hence will require careful ordering.

FD
--
Tidbit is used in a number of atomic ops on the running_mask. If we have
one fdtab[] per group, the mask implies that it's within the group.
Theoretically we should never face a situation where an FD is reported nor
manipulated for a remote group.

There will still be one poller per thread, except that this time all
operations will be related to the current thread_group. No fd may appear
in two thread_groups at once, but we can probably not prevent that (e.g.
delayed close and reopen). Should we instead have a single shared fdtab[]
(less memory usage also) ? Maybe adding the group in the fdtab entry would
work, but when does a thread know it can leave it ? Currently this is
solved by running_mask and by update_mask. Having two tables could help
with this (each table sees the FD in a different group with a different
mask) but this looks overkill.

There's polled_mask[] which needs to be decided upon. Probably that it
should be doubled as well. Note, polled_mask left fdtab[] for cacheline
alignment reasons in commit cb92f5cae4.

If we have one fdtab[] per group, what *really* prevents from using the
same FD in multiple groups ? _fd_delete_orphan() and fd_update_events()
need to check for no-thread usage before closing the FD. This could be
a limiting factor. Enabling could require to wake every poller.

Shouldn't we remerge fdinfo[] with fdtab[] (one pointer + one int/short,
used only during creation and close) ?

Other problem, if we have one fdtab[] per TG, disabling/enabling an FD
(e.g. pause/resume on listener) can become a problem if it's not necessarily
on the current TG. We'll then need a way to figure that one. It sounds like
FDs from listeners and receivers are very specific and suffer from problems
all other ones under high load do not suffer from. Maybe something specific
ought to be done for them, if we can guarantee there is no risk of accidental
reuse (e.g. locate the TG info in the receiver and have a "MT" bit in the
FD's flags). The risk is always that a close() can result in instant pop-up
of the same FD on any other thread of the same process.

Observations: right now fdtab[].thread_mask more or less corresponds to a
declaration of interest, it's very close to meaning "active per thread". It is
in fact located in the FD while it ought to do nothing there, as it should be
where the FD is used as it rules accesses to a shared resource that is not
the FD but what uses it. Indeed, if neither polled_mask nor running_mask have
a thread's bit, the FD is unknown to that thread and the element using it may
only be reached from above and not from the FD. As such we ought to have a
thread_mask on a listener and another one on connections. These ones will
indicate who uses them. A takeover could then be simplified (atomically set
exclusivity on the FD's running_mask, upon success, takeover the connection,
clear the running mask). Probably that the change ought to be performed on
the connection level first, not the FD level by the way. But running and
polled are the two relevant elements, one indicates userland knowledge,
the other one kernel knowledge. For listeners there's no exclusivity so it's
a bit different but the rule remains the same that we don't have to know
what threads are *interested* in the FD, only its holder.

Not exact in fact, see FD notes below.

activity
--------
There should be one activity array per thread group. The dump should
simply scan them all since the cumuled values are not very important
anyway.

applets
-------
They use tid_bit only for the task. It looks like the appctx's thread_mask
is never used (now removed). Furthermore, it looks like the argument is
*always* tid_bit.

CPU binding
-----------
This is going to be tough. It will be needed to detect that threads overlap
and are not bound (i.e. all threads on same mask). In this case, if the number
of threads is higher than the number of threads per physical socket, one must
try hard to evenly spread them among physical sockets (e.g. one thread group
per physical socket) and start as many threads as needed on each, bound to
all threads/cores of each socket. If there is a single socket, the same job
may be done based on L3 caches. Maybe it could always be done based on L3
caches. The difficulty behind this is the number of sockets to be bound: it
is not possible to bind several FDs per listener. Maybe with a new bind
keyword we can imagine to automatically duplicate listeners ? In any case,
the initially bound cpumap (via taskset) must always be respected, and
everything should probably start from there.

Frontend binding
----------------
We'll have to define a list of threads and thread-groups per frontend.
Probably that having a group mask and a same thread-mask for each group
would suffice.

Threads should have two numbers:
  - the per-process number (e.g. 1..256)
  - the per-group number (1..64)

The "bind-thread" lines ought to use the following syntax:
  - bind 45      ## bind to process' thread 45
  - bind 1/45    ## bind to group 1's thread 45
  - bind all/45  ## bind to thread 45 in each group
  - bind 1/all   ## bind to all threads in group 1
  - bind all     ## bind to all threads
  - bind all/all ## bind to all threads in all groups (=all)
  - bind 1/65    ## rejected
  - bind 65      ## OK if there are enough
  - bind 35-45   ## depends. Rejected if it crosses a group boundary.

The global directive "nbthread 28" means 28 total threads for the process. The
number of groups will sub-divide this. E.g. 4 groups will very likely imply 7
threads per group. At the beginning, the nbgroup should be manual since it
implies config adjustments to bind lines.

There should be a trivial way to map a global thread to a group and local ID
and to do the opposite.


Panic handler + watchdog
------------------------
Will probably depend on what's done for thread_isolate

Per-thread arrays inside structures
-----------------------------------
- listeners have a thr_conn[] array, currently limited to MAX_THREADS. Should
  we simply bump the limit ?
- same for servers with idle connections.
=> doesn't seem very practical.
- another solution might be to point to dynamically allocated arrays of
  arrays (e.g. nbthread * nbgroup) or a first level per group and a second
  per thread.
=> dynamic allocation based on the global number

Other
-----
- what about dynamic thread start/stop (e.g. for containers/VMs) ?
  E.g. if we decide to start $MANY threads in 4 groups, and only use
  one, in the end it will not be possible to use less than one thread
  per group, and at most 64 will be present in each group.


FD Notes
--------
  - updt_fd_polling() uses thread_mask to figure where to send the update,
    the local list or a shared list, and which bits to set in update_mask.
    This could be changed so that it takes the update mask in argument. The
    call from the poller's fork would just have to broadcast everywhere.

  - pollers use it to figure whether they're concerned or not by the activity
    update. This looks important as otherwise we could re-enable polling on
    an FD that changed to another thread.

  - thread_mask being a per-thread active mask looks more exact and is
    precisely used this way by _update_fd(). In this case using it instead
    of running_mask to gauge a change or temporarily lock it during a
    removal could make sense.

  - running should be conditioned by thread. Polled not (since deferred
    or migrated). In this case testing thread_mask can be enough most of
    the time, but this requires synchronization that will have to be
    extended to tgid.. But migration seems a different beast that we shouldn't
    care about here: if first performed at the higher level it ought to
    be safe.

In practice the update_mask can be dropped to zero by the first fd_delete()
as the only authority allowed to fd_delete() is *the* owner, and as soon as
all running_mask are gone, the FD will be closed, hence removed from all
pollers. This will be the only way to make sure that update_mask always
refers to the current tgid.

However, it may happen that a takeover within the same group causes a thread
to read the update_mask late, while the FD is being wiped by another thread.
That other thread may close it, causing another thread in another group to
catch it, and change the tgid and start to update the update_mask. This means
that it would be possible for a thread entering do_poll() to see the correct
tgid, then the fd would be closed, reopened and reassigned to another tgid,
and the thread would see its bit in the update_mask, being confused. Right
now this should already happen when the update_mask is not cleared, except
that upon wakeup a migration would be detected and that would be all.

Thus we might need to set the running bit to prevent the FD from migrating
before reading update_mask, which also implies closing on fd_clr_running() == 0 :-(

Also even fd_update_events() leaves a risk of updating update_mask after
clearing running, thus affecting the wrong one. Probably that update_mask
should be updated before clearing running_mask there. Also, how about not
creating an update on a close ? Not trivial if done before running, unless
thread_mask==0.

Note that one situation that is currently visible is that a thread closes a
file descriptor that it's the last one to own and to have an update for. In
fd_delete_orphan() it does call poller.clo() but this one is not sufficient
as it doesn't drop the update_mask nor does it clear the polled_mask. The
typical problem that arises is that the close() happens before processing
the last update (e.g. a close() just after a partial read), thus it still
has *at least* one bit set for the current thread in both update_mask and
polled_mask, and it is present in the update_list. Not handling it would
mean that the event is lost on update() from the concerned threads and
that some resource might leak. Handling it means zeroing the update_mask
and polled_mask, and deleting the update entry from the update_list, thus
losing the update event. And as indicated above, if the FD switches twice
between 2 groups, the finally called thread does not necessarily know that
the FD isn't the same anymore, thus it's difficult to decide whether to
delete it or not, because deleting the event might in fact mean deleting
something that was just re-added for the same thread with the same FD but
a different usage.

Also it really seems unrealistic to scan a single shared update_list like
this using write operations. There should likely be one per thread-group.
But in this case there is no more choice than deleting the update event
upon fd_delete_orphan(). This also means that poller->clo() must do the
job for all of the group's threads at once. This would mean a synchronous
removal before the close(), which doesn't seem ridiculously expensive. It
just requires that any thread of a group may manipulate any other thread's
status for an FD and a poller.

Note about our currently supported pollers:

  - epoll: our current code base relies on the modern version which
           automatically removes closed FDs, so we don't have anything to do
           when closing and we don't need the update.

  - kqueue: according to https://www.freebsd.org/cgi/man.cgi?query=kqueue, just
            like epoll, a close() implies a removal. Our poller doesn't perform
            any bookkeeping either so it's OK to directly close.

  - evports: https://docs.oracle.com/cd/E86824_01/html/E54766/port-dissociate-3c.html
             says the same, i.e. close() implies a removal of all events. No local
             processing nor bookkeeping either, we can close.

  - poll: the fd_evts[] array is global, thus shared by all threads. As such,
          a single removal is needed to flush it for all threads at once. The
          operation is already performed like this.

  - select: works exactly like poll() above, hence already handled.

As a preliminary conclusion, it's safe to delete the event and reset
update_mask just after calling poller->clo(). If extremely unlucky (changing
thread mask due to takeover ?), the same FD may appear at the same time:
  - in one or several thread-local fd_updt[] arrays. These ones are just work
    queues, there's nothing to do to ignore them, just leave the holes with an
    outdated FD which will be ignored once met. As a bonus, poller->clo() could
    check if the last fd_updt[] points to this specific FD and decide to kill
    it.

  - in the global update_list. In this case, fd_rm_from_fd_list() already
    performs an attachment check, so it's safe to always call it before closing
    (since no one else may be in the process of changing anything).


###########################################################

Current state:


Mux / takeover / fd_delete() code                |||  poller code
-------------------------------------------------|||---------------------------------------------------
                                                 \|/
mux_takeover():                                   | fd_set_running():
   if (fd_takeover()<0)                           |    old = {running, thread};
     return fail;                                 |    new = {tid_bit, tid_bit};
   ...                                            |
fd_takeover():                                    |    do {
   atomic_or(running, tid_bit);                   |       if (!(old.thread & tid_bit))
   old = {running, thread};                       |          return -1;
   new = {tid_bit, tid_bit};                      |       new = { running | tid_bit, old.thread }	 
   if (owner != expected) {                       |    } while (!dwcas({running, thread}, &old, &new));
      atomic_and(running, ~tid_bit);              |
      return -1; // fail                          | fd_clr_running():
   }                                              |    return atomic_and_fetch(running, ~tid_bit);
                                                  |
   while (old == {tid_bit, !=0 })                 | poll():
      if (dwcas({running, thread}, &old, &new)) { |    if (!owner)
         atomic_and(running, ~tid_bit);           |       continue;
         return 0; // success                     |
      }                                           |    if (!(thread_mask & tid_bit)) {
   }                                              |       epoll_ctl_del();
                                                  |       continue;
   atomic_and(running, ~tid_bit);                 |    }
   return -1; // fail                             |
                                                  |    // via fd_update_events()
fd_delete():                                      |    if (fd_set_running() != -1) {
   atomic_or(running, tid_bit);                   |       iocb();
   atomic_store(thread, 0);                       |       if (fd_clr_running() == 0 && !thread_mask)
   if (fd_clr_running(fd) = 0)                    |         fd_delete_orphan();
        fd_delete_orphan();                       |    }


The idle_conns_lock prevents the connection from being *picked* and released
while someone else is reading it. What it does is guarantee that on idle
connections, the caller of the IOCB will not dereference the task's context
while the connection is still in the idle list, since it might be picked then
freed at the same instant by another thread. As soon as the IOCB manages to
get that lock, it removes the connection from the list so that it cannot be
taken over anymore. Conversely, the mux's takeover() code runs under that
lock so that if it frees the connection and task, this will appear atomic
to the IOCB. The timeout task (which is another entry point for connection
deletion) does the same. Thus, when coming from the low-level (I/O or timeout):
  - task always exists, but ctx checked under lock validates; conn removal
    from list prevents takeover().
  - t->context is stable, except during changes under takeover lock. So
    h2_timeout_task may well run on a different thread than h2_io_cb().

Coming from the top:
  - takeover() done under lock() clears task's ctx and possibly closes the FD
    (unless some running remains present).

Unlikely but currently possible situations:
  - multiple pollers (up to N) may have an idle connection's FD being
    polled, if the connection was passed from thread to thread. The first
    event on the connection would wake all of them. Most of them would
    see fdtab[].owner set (the late ones might miss it). All but one would
    see that their bit is missing from fdtab[].thread_mask and give up.
    However, just after this test, others might take over the connection,
    so in practice if terribly unlucky, all but 1 could see their bit in
    thread_mask just before it gets removed, all of them set their bit
    in running_mask, and all of them call iocb() (sock_conn_iocb()).
    Thus all of them dereference the connection and touch the subscriber
    with no protection, then end up in conn_notify_mux() that will call
    the mux's wake().

  - multiple pollers (up to N-1) might still be in fd_update_events()
    manipulating fdtab[].state. The cause is that the "locked" variable
    is determined by atleast2(thread_mask) but that thread_mask is read
    at a random instant (i.e. it may be stolen by another one during a
    takeover) since we don't yet hold running to prevent this from being
    done. Thus we can arrive here with thread_mask==something_else (1bit),
    locked==0 and fdtab[].state assigned non-atomically.

  - it looks like nothing prevents h2_release() from being called on a
    thread (e.g. from the top or task timeout) while sock_conn_iocb()
    dereferences the connection on another thread. Those killing the
    connection don't yet consider the fact that it's an FD that others
    might currently be waking up on.

###################

pb with counter:

users count doesn't say who's using the FD and two users can do the same
close in turn. The thread_mask should define who's responsible for closing
the FD, and all those with a bit in it ought to do it.


2021-08-25 - update with minimal locking on tgid value
==========

  - tgid + refcount at once using CAS
  - idle_conns lock during updates
  - update:
    if tgid differs => close happened, thus drop update
    otherwise normal stuff. Lock tgid until running if needed.
  - poll report:
    if tgid differs => closed
    if thread differs => stop polling (migrated)
    keep tgid lock until running
  - test on thread_id:
    if (xadd(&tgid,65536) != my_tgid) {
      // was closed
      sub(&tgid, 65536)
      return -1
    }
    if !(thread_id & tidbit) => migrated/closed
    set_running()
    sub(tgid,65536)
  - note: either fd_insert() or the final close() ought to set
    polled and update to 0.

2021-09-13 - tid / tgroups etc.
==========

  * tid currently is the thread's global ID. It's essentially used as an index
    for arrays. It must be clearly stated that it works this way.

  * tasklets use the global thread id, and __tasklet_wakeup_on() must use a
    global ID as well. It's capital that tinfo[] provides instant access to
    local/global bits/indexes/arrays

  - tid_bit makes no sense process-wide, so it must be redefined to represent
    the thread's tid within its group. The name is not much welcome though, but
    there are 286 of it that are not going to be changed that fast.
    => now we have ltid and ltid_bit in thread_info. thread-local tid_bit still
       not changed though. If renamed we must make sure the older one vanishes.
       Why not rename "ptid, ptid_bit" for the process-wide tid and "gtid,
       gtid_bit" for the group-wide ones ? This removes the ambiguity on "tid"
       which is half the time not the one we expect.

  * just like "ti" is the thread_info, we need to have "tg" pointing to the
    thread_group.

  - other less commonly used elements should be retrieved from ti->xxx. E.g.
    the thread's local ID.

  - lock debugging must reproduce tgid

  * task profiling must be made per-group (annoying), unless we want to add a
    per-thread TH_FL_* flag and have the rare places where the bit is changed
    iterate over all threads if needed. Sounds preferable overall.

  * an offset might be placed in the tgroup so that even with 64 threads max
    we could have completely separate tid_bits over several groups.
    => base and count now

2021-09-15 - bind + listen() + rx
==========

  - thread_mask (in bind_conf->rx_settings) should become an array of
    MAX_TGROUP longs.
  - when parsing "thread 123" or "thread 2/37", the proper bit is set,
    assuming the array is either a contiguous bitfield or a tgroup array.
    An option RX_O_THR_PER_GRP or RX_O_THR_PER_PROC is set depending on
    how the thread num was parsed, so that we reject mixes.
  - end of parsing: entries translated to the cleanest form (to be determined)
  - binding: for each socket()/bind()/listen()... just perform one extra dup()
    for each tgroup and store the multiple FDs into an FD array indexed on
    MAX_TGROUP. => allows to use one FD per tgroup for the same socket, hence
    to have multiple entries in all tgroup pollers without requiring the user
    to duplicate the bind line.

2021-09-15 - global thread masks
==========

Some global variables currently expect to know about thread IDs and it's
uncertain what must be done with them:
  - global_tasks_mask  /* Mask of threads with tasks in the global runqueue */
    => touched under the rq lock. Change it per-group ? What exact use is made ?

  - sleeping_thread_mask /* Threads that are about to sleep in poll() */
    => seems that it can be made per group

  - all_threads_mask: a bit complicated, derived from nbthread and used with
    masks and with my_ffsl() to wake threads up. Should probably be per-group
    but we might miss something for global.

  - stopping_thread_mask: used in combination with all_threads_mask, should
    move per-group.

  - threads_harmless_mask: indicates all threads that are currently harmless in
    that they promise not to access a shared resource. Must be made per-group
    but then we'll likely need a second stage to have the harmless groups mask.
    threads_idle_mask, threads_sync_mask, threads_want_rdv_mask go with the one
    above. Maybe the right approach will be to request harmless on a group mask
    so that we can detect collisions and arbiter them like today, but on top of
    this it becomes possible to request harmless only on the local group if
    desired. The subtlety is that requesting harmless at the group level does
    not mean it's achieved since the requester cannot vouch for the other ones
    in the same group.

In addition, some variables are related to the global runqueue:
  __decl_aligned_spinlock(rq_lock); /* spin lock related to run queue */
  struct eb_root rqueue;      /* tree constituting the global run queue, accessed under rq_lock */
  unsigned int grq_total;     /* total number of entries in the global run queue, atomic */
  static unsigned int global_rqueue_ticks;  /* insertion count in the grq, use rq_lock */

And others to the global wait queue:
  struct eb_root timers;      /* sorted timers tree, global, accessed under wq_lock */
  __decl_aligned_rwlock(wq_lock);   /* RW lock related to the wait queue */
  struct eb_root timers;      /* sorted timers tree, global, accessed under wq_lock */


2022-06-14 - progress on task affinity
==========

The particularity of the current global run queue is to be usable for remote
wakeups because it's protected by a lock. There is no need for a global run
queue beyond this, and there could already be a locked queue per thread for
remote wakeups, with a random selection at wakeup time. It's just that picking
a pending task in a run queue among a number is convenient (though it
introduces some excessive locking). A task will either be tied to a single
group or will be allowed to run on any group. As such it's pretty clear that we
don't need a global run queue. When a run-anywhere task expires, either it runs
on the current group's runqueue with any thread, or a target thread is selected
during the wakeup and it's directly assigned.

A global wait queue seems important for scheduled repetitive tasks however. But
maybe it's more a task for a cron-like job and there's no need for the task
itself to wake up anywhere, because once the task wakes up, it must be tied to
one (or a set of) thread(s). One difficulty if the task is temporarily assigned
a thread group is that it's impossible to know where it's running when trying
to perform a second wakeup or when trying to kill it. Maybe we'll need to have
two tgid for a task (desired, effective). Or maybe we can restrict the ability
of such a task to stay in wait queue in case of wakeup, though that sounds
difficult. Other approaches would be to set the GID to the current one when
waking up the task, and to have a flag (or sign on the GID) indicating that the
task is still queued in the global timers queue. We already have TASK_SHARED_WQ
so it seems that antoher similar flag such as TASK_WAKE_ANYWHERE could make
sense. But when is TASK_SHARED_WQ really used, except for the "anywhere" case ?
All calls to task_new() use either 1<<thr, tid_bit, all_threads_mask, or come
from appctx_new which does exactly the same. The only real user of non-global,
non-unique task_new() call is debug_parse_cli_sched() which purposely allows to
use an arbitrary mask.

 +----------------------------------------------------------------------------+
 | => we don't need one WQ per group, only a global and N local ones, hence   |
 |    the TASK_SHARED_WQ flag can continue to be used for this purpose.       |
 +----------------------------------------------------------------------------+

Having TASK_SHARED_WQ should indicate that a task will always be queued to the
shared queue and will always have a temporary gid and thread mask in the run
queue.

Going further, as we don't have any single case of a task bound to a small set
of threads, we could decide to wake up only expired tasks for ourselves by
looking them up using eb32sc and adopting them. Thus, there's no more need for
a shared runqueue nor a global_runqueue_ticks counter, and we can simply have
the ability to wake up a remote task. The task's thread_mask will then change
so that it's only a thread ID, except when the task has TASK_SHARED_WQ, in
which case it corresponds to the running thread. That's very close to what is
already done with tasklets in fact.


2021-09-29 - group designation and masks
==========

Neither FDs nor tasks will belong to incomplete subsets of threads spanning
over multiple thread groups. In addition there may be a difference between
configuration and operation (for FDs). This allows to fix the following rules:

  group  mask   description
    0     0     bind_conf: groups & thread not set. bind to any/all
                task: it would be nice to mean "run on the same as the caller".

    0    xxx    bind_conf: thread set but not group: thread IDs are global
                FD/task: group 0, mask xxx

    G>0   0     bind_conf: only group is set: bind to all threads of group G
                FD/task: mask 0 not permitted (= not owned). May be used to
                mention "any thread of this group", though already covered by
                G/xxx like today.

    G>0  xxx    bind_conf: Bind to these threads of this group
                FD/task: group G, mask xxx

It looks like keeping groups starting at zero internally complicates everything
though. But forcing it to start at 1 might also require that we rescan all tasks
to replace 0 with 1 upon startup. This would also allow group 0 to be special and
be used as the default group for any new thread creation, so that group0.count
would keep the number of unassigned threads. Let's try:

  group  mask   description
    0     0     bind_conf: groups & thread not set. bind to any/all
                task: "run on the same group & thread as the caller".

    0    xxx    bind_conf: thread set but not group: thread IDs are global
                FD/task: invalid. Or maybe for a task we could use this to
                mean "run on current group, thread XXX", which would cover
                the need for health checks (g/t 0/0 while sleeping, 0/xxx
                while running) and have wake_expired_tasks() detect 0/0 and
                wake them up to a random group.

    G>0   0     bind_conf: only group is set: bind to all threads of group G
                FD/task: mask 0 not permitted (= not owned). May be used to
                mention "any thread of this group", though already covered by
                G/xxx like today.

    G>0  xxx    bind_conf: Bind to these threads of this group
                FD/task: group G, mask xxx

With a single group declared in the config, group 0 would implicitly find the
first one.


The problem with the approach above is that a task queued in one group+thread's
wait queue could very well receive a signal from another thread and/or group,
and that there is no indication about where the task is queued, nor how to
dequeue it. Thus it seems that it's up to the application itself to unbind/
rebind a task. This contradicts the principle of leaving a task waiting in a
wait queue and waking it anywhere.

Another possibility might be to decide that a task having a defined group but
a mask of zero is shared and will always be queued into its group's wait queue.
However, upon expiry, the scheduler would notice the thread-mask 0 and would
broadcast it to any group.

Right now in the code we have:
  - 18 calls of task_new(tid_bit)
  - 17 calls of task_new_anywhere()
  - 2 calls with a single bit

Thus it looks like "task_new_anywhere()", "task_new_on()" and
"task_new_here()" would be sufficient.