Jump to page: 1 2
Thread overview
Member variables in method are null when called as delegate from thread
Jan 11, 2021
Tim
Jan 11, 2021
Tim
Jan 11, 2021
Arafel
Jan 11, 2021
Tim
Jan 11, 2021
Arafel
Jan 11, 2021
Arafel
Jan 12, 2021
Imperatorn
Jan 11, 2021
Paul Backus
Jan 12, 2021
tsbockman
Jan 12, 2021
tsbockman
Jan 13, 2021
Tim
Jan 13, 2021
tsbockman
Jan 13, 2021
Arafel
January 11, 2021
Hi there,

I have something like this:

class Foo{
    MongoClient db;

    this(){
        db = connectMongoDB("127.0.0.1");
        void delegate()[string] commands = ["start": &this.start];
	MessageService messenger = new MessageService(8081, commands);
    }

    void start(){
        // Do something with db
    }

MessageService is a thread that deals with socket communication. When a command comes in, it calls the appropriate delegate given to it by commands. When MessageService calls the delegate for start, db is null. If I call start() in the Foo constructor it works just fine. Am I missing something here? Do delegates get called outside of their class context? I know I could just pass the db into start but I want to work out exactly why this is happening

Thanks in advance
January 11, 2021
Ok, so it seems that it isn't null now. But I stall can't call db.getCollection().findAndModify() from vibe.d successfully here. Works just fine in the constructor. When it's called form the MessengerService thread it never returns from the function call
January 11, 2021
On 11/1/21 1:43, Tim wrote:
> Hi there,
> 
> I have something like this:
> 
> class Foo{
>      MongoClient db;
> 
>      this(){
>          db = connectMongoDB("127.0.0.1");
>          void delegate()[string] commands = ["start": &this.start];
>      MessageService messenger = new MessageService(8081, commands);
>      }
> 
>      void start(){
>          // Do something with db
>      }
> 
> MessageService is a thread that deals with socket communication. When a command comes in, it calls the appropriate delegate given to it by commands. When MessageService calls the delegate for start, db is null. If I call start() in the Foo constructor it works just fine. Am I missing something here? Do delegates get called outside of their class context? I know I could just pass the db into start but I want to work out exactly why this is happening
> 
> Thanks in advance

Hi,

Member variables are thread-local by default. At the very least you'll need to make `db` `shared` and manually verify that it's safe to use them before casting it away. So your code could end up a bit like this:

```
class Foo{
     shared MongoClient db;

     this(){
         db = cast (shared) connectMongoDB("127.0.0.1");
         void delegate()[string] commands = ["start": &this.start];
     MessageService messenger = new MessageService(8081, commands);
     }

     void start(){
         // Make sure there's no other thread accessing the db
         // If db is a class, you'll be able to use `db_`:
         auto db_ = cast () db;
         // Otherwise you'll be making a copy and will have to use `cast() db` each time, or make a nasty workaround with pointers.
     }
}
```

It's also possible that you'll have to make Foo itself `shared`, or at least convert your constructor into a `shared this ()` to get a shared instance that you can pass to a different thread, but I'm not sure how function pointers / delegates work across threads.

Best,

A.
January 11, 2021
On Monday, 11 January 2021 at 08:21:21 UTC, Arafel wrote:
> It's also possible that you'll have to make Foo itself `shared`, or at least convert your constructor into a `shared this ()` to get a shared instance that you can pass to a different thread, but I'm not sure how function pointers / delegates work across threads.
>
> Best,
>
> A.

Thanks for that. I'll give it a go and see how that fares
January 11, 2021
On 1/11/21 3:21 AM, Arafel wrote:
> On 11/1/21 1:43, Tim wrote:
>> Hi there,
>>
>> I have something like this:
>>
>> class Foo{
>>      MongoClient db;
>>
>>      this(){
>>          db = connectMongoDB("127.0.0.1");
>>          void delegate()[string] commands = ["start": &this.start];
>>      MessageService messenger = new MessageService(8081, commands);
>>      }
>>
>>      void start(){
>>          // Do something with db
>>      }
>>
>> MessageService is a thread that deals with socket communication. When a command comes in, it calls the appropriate delegate given to it by commands. When MessageService calls the delegate for start, db is null. If I call start() in the Foo constructor it works just fine. Am I missing something here? Do delegates get called outside of their class context? I know I could just pass the db into start but I want to work out exactly why this is happening
>>
>> Thanks in advance
> 
> Hi,
> 
> Member variables are thread-local by default. At the very least you'll need to make `db` `shared` and manually verify that it's safe to use them before casting it away. So your code could end up a bit like this:

That isn't exactly true. Member variables are members of the object. If the object is shared, the member variables are shared. If the object is local the variables are local.

Thread local really only applies to *static* variables, such as globals or members declared static. If that were the case, yes, the other thread would not see the object.

I did not respond to the OP because I also don't know why it wouldn't work. But I also don't know what all the code is doing.

-Steve
January 11, 2021
On 11/1/21 14:42, Steven Schveighoffer wrote:
> 
> That isn't exactly true. Member variables are members of the object. If the object is shared, the member variables are shared. If the object is local the variables are local.
> 
> Thread local really only applies to *static* variables, such as globals or members declared static. If that were the case, yes, the other thread would not see the object.
> 
> I did not respond to the OP because I also don't know why it wouldn't work. But I also don't know what all the code is doing.
> 
> -Steve

Out of curiosity, what happens with members that are declared `shared` in a non-shared object?

I thought that declaring an object `shared` "promotes" its members, but that it wasn't strictly needed for a non-shared object to have shared members, but I might be wrong here.

```
struct S {}

class A {
    S s1;
    shared S s2;
}

void main() {
    A a1 = new A();
    pragma(msg, typeof(a1.s1)); // S
    pragma(msg, typeof(a1.s2)); // shared(S)
    shared A a2 = new shared A();
    pragma(msg, typeof(a2.s1)); // shared(S)
    pragma(msg, typeof(a2.s2)); // shared(S)
}
```

https://run.dlang.io/is/skCfvE

Of course I don't know the practical differences in the actual accessibility of the different members beyond the type system.

Is there any way to check if a pointer is actually TLS or global storage? If `a1.s2` can't be properly accessed from different threads, I'd consider that a big bug in the `shared` implementation.

Best,

A.
January 11, 2021
On 1/11/21 10:42 AM, Arafel wrote:
> On 11/1/21 14:42, Steven Schveighoffer wrote:
>>
>> That isn't exactly true. Member variables are members of the object. If the object is shared, the member variables are shared. If the object is local the variables are local.
>>
>> Thread local really only applies to *static* variables, such as globals or members declared static. If that were the case, yes, the other thread would not see the object.
>>
>> I did not respond to the OP because I also don't know why it wouldn't work. But I also don't know what all the code is doing.
>>
> 
> Out of curiosity, what happens with members that are declared `shared` in a non-shared object?

A shared member is a sharable member of the class. It does not put the item in global storage.

There are some... odd rules.

struct S
{
   static int a; // TLS
   shared static int b; // shared data storage
   shared int c; // local variable, but its type is shared(int)
   immutable int d; // local immutable variable, settable only in constructor
   immutable int e = 5; // stored in data segment, not per instance!
   __gshared int f; // stored in global segment, typed as int, not shared(int)
}

> I thought that declaring an object `shared` "promotes" its members, but that it wasn't strictly needed for a non-shared object to have shared members, but I might be wrong here.

There are 2 different things here -- storage and type. shared as a storage class (i.e. without the parentheses) means 2 things:

1. for variables that are declared to be globals (either static or at module level), shared puts it in the shared data segment vs. thread local storage. For variables in all other declaration contexts, they are just stored where declared (either inside the instance or on the stack or whatever).
2. The type is modified to shared(T) instead of T.

> ```
> struct S {}
> 
> class A {
>      S s1;
>      shared S s2;
> }
> 
> void main() {
>      A a1 = new A();
>      pragma(msg, typeof(a1.s1)); // S
>      pragma(msg, typeof(a1.s2)); // shared(S)
>      shared A a2 = new shared A();
>      pragma(msg, typeof(a2.s1)); // shared(S)
>      pragma(msg, typeof(a2.s2)); // shared(S)
> }
> ```
> 
> https://run.dlang.io/is/skCfvE
> 
> Of course I don't know the practical differences in the actual accessibility of the different members beyond the type system.

In the type system, shared basically means "other threads may have access". Right now, shared is kind of useless, because nothing is truly enforced except implicit conversions to/from shared are disallowed. In the future, shared data will be REQUIRED to be cast to unshared for usage.

> 
> Is there any way to check if a pointer is actually TLS or global storage? If `a1.s2` can't be properly accessed from different threads, I'd consider that a big bug in the `shared` implementation.

You are misunderstanding, a1 is stored on the heap, a2 is stored on the heap. In both cases a1.s2 and a2.s2 are stored in the object that is on the heap. The *type* being shared means you can pass its address to another thread (and semantically, shared means "another thread may be using this").

In order to ask the compiler to stick it in TLS or global shared storage, you have to mark it `static`.

-Steve
January 11, 2021
On 11/1/21 17:10, Steven Schveighoffer wrote:
> A shared member is a sharable member of the class. It does not put the item in global storage.
> 
> There are some... odd rules.
> 
> struct S
> {
>     static int a; // TLS
>     shared static int b; // shared data storage
>     shared int c; // local variable, but its type is shared(int)
>     immutable int d; // local immutable variable, settable only in constructor
>     immutable int e = 5; // stored in data segment, not per instance!
>     __gshared int f; // stored in global segment, typed as int, not shared(int)
> }

Thanks for the detailed explanation! I think this mixing of types and storage classes makes a very unfortunate combination:

```
import std;

int i = 0;
shared int j = 0;

struct S {
    int i = 0;
    shared int j = 0;
}

S s;

void main() {
    i = 1;
    j = 1;
    s.i = 1;
    s.j = 1;
    spawn(&f);

}

void f() {
    assert(i == 0); // Expected
    assert(j == 1); // Expected
    assert(s.i == 0); // Expected
    assert(s.j == 0); // Wait, what?
}
```

I agree that once you know the inner workings it makes sense, but a naïve approach might suggest that `s.j` would be... well, shared, just like `j`.
January 11, 2021
On Monday, 11 January 2021 at 16:10:49 UTC, Steven Schveighoffer wrote:
> There are some... odd rules.
>
> struct S
> {
[...]
>    immutable int e = 5; // stored in data segment, not per instance!

Are you sure?

struct S
{
    immutable int n = 123;
    this(int n) { this.n = n; }
}

void main()
{
    S s1;
    S s2 = 456;
    assert(s1.n == 123);
    assert(s2.n == 456);
}
January 12, 2021
On Monday, 11 January 2021 at 00:43:00 UTC, Tim wrote:
> When MessageService calls the delegate for start, db is null. If I call start() in the Foo constructor it works just fine. Am I missing something here? Do delegates get called outside of their class context? I know I could just pass the db into start but I want to work out exactly why this is happening

The compiler and the physical CPU are both allowed to change the order in which instructions are executed to something different from what your code specifies, as long as the visible, "official" results and effects of the chosen order of execution are the same as those of your specified code, FROM THE PERSPECTIVE OF THE EXECUTING THREAD.

This is allowed so that the compiler can optimize to minimize negative "unofficial" effects such as the passage of time and memory consumption.

However, this re-ordering IS permitted to freely alter the behavior of your code from the perspective of OTHER threads. A likely cause of your bug is that the write to db by the constructor's thread is being committed to memory after the read of db by the MessageService thread.

In order to RELIABLY fix this kind of problem, you must correctly use the only commands which the compiler and CPU are NOT allowed to reorder with respect to other threads, namely atomic operations, memory barriers and synchronization primitives. A wide selection of these tools may be found in these D runtime library modules:

    core.sync: http://dpldocs.info/experimental-docs/core.sync.html
    core.atomic: http://dpldocs.info/experimental-docs/core.atomic.html
    core.thread: http://dpldocs.info/experimental-docs/core.thread.html

(I recommend using Adam D. Ruppe's unofficial but superior rendering of the D runtime documentation at dpldocs.info rather than the official dlang.org rendering, as I found some necessary pieces of the documentation are just mysteriously missing from the offical version.)

Be warned that most models of multi-threaded programming are difficult to implement correctly, as opposed to ALMOST correctly with subtle heisen-bugs. You should either stick to one of the known simple models like immutable message passing with GC, or do some studying before writing too much code.

Here are some resources which I have found very helpful in learning to understand this topic, and to avoid its pitfalls:

    Short educational game: https://deadlockempire.github.io/
    Tech talk by C++ expert Herb Sutter (D's core.atomic uses the C++ memory model):
        https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2
        https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2

If you want to seriously dig into this, I suggest reviewing some or all of the content at the links above. If you're still confused about how to apply it in D, feel free to come back and ask for examples or code reviews. I'd rather not start with examples, though, because if you don't understand the rules and principles behind them, it's really easy to unknowingly introduce bugs into otherwise correct examples with seemingly innocent changes.
« First   ‹ Prev
1 2