C++ - Should data passed to a thread be volatile?

2019-06-22 15:15发布

问题:

In Microsoft Visual C++ I can call CreateThread() to create a thread by starting a function with one void * parameter. I pass a pointer to a struct as that parameter, and I see a lot of other people do that as well.

My question is if I am passing a pointer to my struct how do I know if the structure members have been actually written to memory before CreateThread() was called? Is there any guarantee they won't be just cached? For example:

struct bigapple { string color; int count; } apple;
apple.count = 1;
apple.color = "red";
hThread = CreateThread( NULL, 0, myfunction, &apple, 0, NULL );

DWORD WINAPI myfunction( void *param )
{
    struct bigapple *myapple = (struct bigapple *)param;

    // how do I know that apple's struct was actually written to memory before CreateThread?
    cout << "Apple count: " << myapple->count << endl; 
}

This afternoon while I was reading I saw a lot of Windows code on this website and others that passes in data that is not volatile to a thread, and there doesn't seem to be any memory barrier or anything else. I know C++ or at least older revisions are not "thread aware" so I'm wondering if maybe there's some other reason. My guess would be the compiler sees that I've passed a pointer &apple in a call to CreateThread() so it knows to write out members of apple before the call.

Thanks

回答1:

No. The relevant Win32 thread functions all take care of the necessary memory barriers. All writes prior to CreateThread are visible to the new thread. Obviously the reads in that newly created thread cannot be reordered before the call to CreateThread.

volatile would not add any extra useful constraints on the compiler, and merely slow down the code. In practice thiw wouldn't be noticeable compared to the cost of creating a new thread, though.



回答2:

No, it should not be volatile. At the same time you are pointing at the valid issue. Detailed operation of the cache is described in the Intel/ARM/etc papers.

Nevertheless you can safely assume that the data WILL BE WRITTEN. Otherwise too many things will be broken. Several decades of experience tell that this is so.

If thread scheduler will start thread on the same core, the state of the cache will be fine, otherwise, if not, kernel will flush the cache. Otherwise, nothing will work.

Never use volatile for interaction between threads. It is an instruction on how to handle data inside the thread only (use a register copy or always reread, etc).



回答3:

First, I think optimizer cannot change the order at expense of the correctness. CreateThread() is a function, parameter binidng for function calls happens before the call is made.

Secondly, volatile is not very helpful for the purpose you intend. Check out this article.



回答4:

You're struggling into a non-problem, and are creating at least other two...

  1. Don't worry about the parameter given to CreateThread: if they exist at the time the thread is created they exist until CreateThread returns. And since the thread who creates them does not destroy them, they are also available to the other thread.
  2. The problem now becomes who and when they will be destroyed: You create them with new so they will exist until a delete is called (or until the process terminates: good memory leak!)
  3. The process terminate when its main thread terminate (and all other threads will also be terminated as well by the OS!). And there is nothing in your main that makes it to wait for the other thread to complete.
  4. Beware when using low level API like CreateThread form languages that have thir own library also interfaced with thread. The C-runtime has _beginthreadex. It call CreateThread and perform also other initialization task for the C++ library you will otherwise miss. Some C (and C++) library function may not work properly without those initializations, that are also required to properly free the runtime resources at termination. Unsing CreateThread is like using malloc in a context where delete is then used to cleanup.

The proper main thread bnehavior should be

// create the data
// create the other thread
// // perform othe task
// wait for the oter thread to terminate
// destroy the data

What the win32 API documentation don't say clearly is that every HANDLE is waitable, and become signaled when the associate resource is freed. To wait for the other thread termination, you main thread will just have to call

WaitForSingleObject(hthread,INFINITE);

So the main thread will be more properly:

{
    data* pdata = new data;
    HANDLE hthread = (HANDLE)_beginthreadex(0,0,yourprocedure, pdata,0,0);
    WaitForSingleObject(htread,INFINITE);
    delete pdata;
}

or even

{
    data d;
    HANDLE hthread = (HANDLE)_beginthreadex(0,0,yourprocedure, &d,0,0);
    WaitForSingleObject(htread,INFINITE);
}


回答5:

I think the question is valid in another context. As others have pointed out using a struct and the contents is safe (although access to the data should by synchronized).

However I think that the question is valid if you hav an atomic variable (or a pointer to one) that can be changed outside the thread. My opinion in that case would be that volatile should be used in this case.

Edit: I think the examples on the wiki page are a good explanation http://en.wikipedia.org/wiki/Volatile_variable