Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-35813: Tests and docs for shared_memory #11816

Merged
merged 44 commits into from
Feb 24, 2019
Merged

Conversation

applio
Copy link
Member

@applio applio commented Feb 11, 2019

Tests and docs are now present. Discussion around the API for the SharedMemory class and the appropriateness of the shareable_wrap() function is requested in particular.

https://bugs.python.org/issue35813

Copy link
Contributor

@eamanu eamanu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job!!! Please consider review my comments. Please review the CI problems.


This module provides a class, :class:`SharedMemory`, for the allocation
and management of shared memory to be accessed by one or more processes
on a multicore or symmetric multiprocessor (SMP) machine. To assist with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the double whitepace on machine. To assist if for more readable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the resulting html, whether one or two spaces are in the source only one space is presented in the browser. The convention of using two spaces predates patterns of using only one space after a period. As far as I know, both are "correct". Thankfully the output is the same either way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not go looking for this but I accidentally stumbled across it in the dev guide (see devguide.python.org for lots more such goodies):

A sentence-ending period may be followed by one or two spaces;
while reST ignores the second space, it is customarily put in
by some users, for example to aid Emacs’ auto-fill mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh great thanks for the clarification. I had the idea that double space is used just on docstring.


>>> from multiprocessing import shared_memory
>>> shm_a = shared_memory.SharedMemory(None, size=10)
>>> type(shm_b.buf)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you define shm_b?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great spotting that missed detail! Fixed in commit da7731d.

>>> len(buffer)
10
>>> buffer[:4] = bytearray([22, 33, 44, 55]) # Modify multiple at once
>>> buffer[4] = 100 # Modify single byte at a time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if pep8 will fail here. Because it say you that you need have two whitespaces.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this physical alignment of two related comments helps readability significantly. To not do so would make the example much less readable. Pep8 encourages considering this in particular.

>>> array.array('b', shm_b.buf[:5]) # Copy the data into a new array.array
array('b', [22, 33, 44, 55, 100])
>>> shm_b.buf[:5] = b'howdy' # Modify via shm_b using bytes
>>> bytes(shm_a.buf[:5]) # Access via shm_a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as on the prior; I believe this significantly helps readability in a situation where such help is needed.



The following example demonstrates a practical use of the :class:`SharedMemory`
class with `NumPy arrays <https://www.numpy.org/>`_, accessing the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if is necessary show a sample with NumPy, it's just an opinion

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of NumPy arrays with shared memory is anticipated to be one of the most popular use cases for working with shared memory. Demonstrating this combination is likely very important to a large number of people.


A call to :meth:`~multiprocessing.managers.BaseManager.start` on a
:class:`SharedMemoryManager` instance causes a new process to be started.
This new process's sole purpose is to manage the life cycle
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process' ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After double-checking with a technical editor just now, I have been assured that either is correct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@@ -8,34 +8,120 @@

from functools import reduce
import mmap
from .managers import DictProxy, SyncManager, Server
from .managers import dispatch, BaseManager, Server, State, ProcessError, \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about use () on imports? This way you avoid use \

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like both styles. At the moment, I think it best to stay consistent with the pattern that already exists in multiprocessing and its submodules.

Lib/multiprocessing/shared_memory.py Outdated Show resolved Hide resolved
*name* is the unique name for the requested shared memory, specified as
a string. If ``None`` is supplied for the name, a new shared memory
block with a novel name will be created without needing to also specify
``flags``.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it appears name is optional, why not make it a kwarg?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this topic to b.p.o. starting with msg335660 there.

exception will be raised. To request the creation of a new shared
memory block, set to ``O_CREX``. To request the optional creation of a
new shared memory block or attach to an existing one by the same name,
set to ``O_CREAT``.
Copy link
Contributor

@giampaolo giampaolo Feb 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears deciding whether to use O_CREX / O_CREAT can be implemented as an internal detail based on whether the name is provided or not. Are there other use cases for flags argument?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this topic to b.p.o. starting with msg335660 there.

set to ``O_CREAT``.

*mode* controls user/group/all-based read/write permissions on the
shared memory block. Its specification is not enforceable on all platforms.
Copy link
Contributor

@giampaolo giampaolo Feb 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is mode intended to work like the mode of UNIX file? (I see it defaults to 0o600 so it appears it does). What is the use case for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this topic to b.p.o. starting with msg335660 there.

copying of data.


.. class:: SharedMemory(name, flags=None, mode=0o600, size=0, read_only=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand your code samples it seems this class/object can be returned via the SharedMemoryManager as in:

>>> mgr = SharedMemoryManager()
>>> mgr.start()
>>> raw_shm = smm.SharedMemory(size=128) 

As such, what's the use case for using SharedMemory directly? Also, what's the use case for the name parameter? I understand it you can use it for attaching to a pre-existing instance of SharedMemory, but this kind of diverges from the base SyncManager which tipically allows sharing objects by passing them as arguments for the worker. E.g., with a plain dict:

import multiprocessing
import multiprocessing.managers

def worker(d):
    print(d)  # prints {'foo': 1}
    d['bar'] = 2  # I can also change the dict from here (child process)

manager = multiprocessing.managers.SyncManager()
manager.start()
d = manager.dict()
d['foo'] = 1
proc = multiprocessing.Process(target=worker, args=(d, ))
proc.start()
proc.join()

I wonder if this (the name) can be made transparent (aka an implementation detail).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this topic to b.p.o. starting with msg335622 there.


*read_only* controls whether a shared memory block is to be available
for only reading or for both reading and writing. Its specification is
not enforceable on all platforms.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should mention what platforms implement this and what happens when it's not and True is passed. Also, since this appears not portable across platform, perhaps it's better to not support it at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed -- with the simpler API introduced in commit 0d3d06f, we no longer expose users to such complications.


def setUp(self):
if not HAS_SHMEM:
self.skipTest("requires multiprocessing.shared_memory")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can decorate the class instead

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the other tests in _test_multiprocessing.py use this (potentially older) style rather than the decorator, but this is no longer universally true. I will adopt the decorator style for new tests added to the testing module. Changed in commit 1e5341e as well.


finally:
# Prevent test failures from leading to a dangling segment.
sms.unlink()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use self.addCleanup(sms.unlink) right after creating sms, spare this long try/finally block and save one indentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought. Changed in commit 1e5341e.

sms.close()

finally:
sms.unlink()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use self.addCleanup(sms.unlink) right after creating sms, spare this long try/finally block and save one indentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought -- same as prior comment.

sms_uno.close()

finally:
sms_uno.unlink() # A second shm_unlink() call is bad.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here (could use addCleanup)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this one needs to stay as it is, because this second call to unlink() is needed to trigger the exception that we are testing for.


def test_joinable_queue(self):
self.test_queue("JoinableQueue")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why you need to move these 2 methods from the previous tests. The logic appears split between the mixing class and this one for no apparent reason (but I may be missing it). If you need a different logic for certain TestSharedMemoryManagerTypes methods/tests you can simply override them (e.g. if you want to skip them).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Queue and JoinableQueue are implemented as distributed shared memory (a queue.Queue object existing in one process and an AutoProxy object existing in the local process). For the same reasons mentioned in my b.p.o. comment, I believe it is much clearer to not include these on SharedMemoryManager by default and to instead document how they may be added on-demand with a single line of code (by calling register()).

There is also the potential for a POSIX shared memory message queue (not currently implemented in shared_memory.py). This is also something worth considering for the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: if we expose them they are not subject to the speedup, correct? Would it be the same as using them via the SyncManager?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, exposing Queue or JoinableQueue here would not make them faster; they would be the same as if using them via SyncManager.

We could simply encourage users to employ a SyncManager anytime they want to use a Queue or Lock but this forces the creation of two managers with a server behind each and therefore two processes instead of only one. I think it will be easier to explain, "create one manager to oversee work and provide cleanup at the end."

Although my use of empty() in this example is admittedly fragile, this example shows augmenting SyncManager with Queue is really only one line of code:

from multiprocessing import shared_memory
import queue
shared_memory.SharedMemoryManager.register('Queue', queue.Queue)
with shared_memory.SharedMemoryManager() as smm:
    sl = smm.ShareableList("feb")  # fast, zero-copy shared memory
    q = smm.Queue()                # slow, copy+message distributed shared memory
    for letter in sl:
        q.put(letter.upper())
    while not q.empty():
        print(q.get(timeout=1))

Copy link
Contributor

@giampaolo giampaolo Feb 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we should get rid of Queue, Lock, etc. Just for confirmation: ShareableList and AsyncManager.Lock can be safely mixed/used together, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed -- I have now gotten rid of Queue, Lock, etc. in commit 395709b.

Yes, confirmed: ShareableList and SyncManager.Lock can be safely mixed/used together.

I was sure you meant SyncManager when your wrote AsyncManager but I still went searching just to make sure there was not some AsyncManager hiding inside asyncio somewhere that I was not thinking of. =)

@bedevere-bot
Copy link

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

And if you don't make the requested changes, you will be put in the comfy chair!

Copy link

@osvenskan osvenskan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice simplifications! This looks good to me.

import ctypes
from ctypes import wintypes

kernel32 = ctypes.windll.kernel32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be kernel32 = ctypes.WinDLL('kernel32', use_last_error=True). With ctypes.windll, it's using a globally cached WinDLL instance. This in turn caches function pointers, which leads to conflicting prototypes between packages. Also, use_last_error is required for ctypes.get_last_error(), which is more reliable than FFI-based ctypes.GetLastError().

Is this ctypes usage just temporary for experimenting until the API is finalized? Ultimately I hope this uses _winapi instead of ctypes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, eryksun -- you rock. Changed both in commit 868b83d.

I believe the API is close enough to being finalized that we can move this to _winapi now. I would very much appreciate your help reviewing that move, if I can talk you into that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved this over to use _winapi now, changed in commit 9d83b06.

If you are up for helping review this too, it would again be very much appreciated.

name
)
try:
last_error_code = kernel32.GetLastError()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should call ctypes.get_last_error(), which is being called elsewhere. But for it to work, we need use_last_error=True, as mentioned above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strike that. kernel32.CreateFileMappingW._errcheck is set to _errcheck_bool, which should raise FileExistsError here. Maybe this was added due to a misunderstanding. Using ctypes.windll.kernel32 renders the _errcheck function useless, since ctypes.get_last_error isn't enabled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When kernel32.CreateFileMappingW is passed the name of an existing anonymous memory block in this way, it succeeds and it does not raise FileExistsError (_errcheck_bool receives a non-zero result value). This motivates the ctypes.get_last_error() call to check for a return value of 183.

This seems to match what is described on docs.microsoft.com, regarding the return value from CreateFileMappingA:

    If the object exists before the function call, the function
    returns a handle to the existing object (with its current
    size, not the specified size), and GetLastError returns
    ERROR_ALREADY_EXISTS.

FWIW, this technique is leveraged in the Boost library for this same purpose. The following is a snippet from boost/interprocess/windows_shared_memory.hpp where create_file_mapping is a wrapper for CreateFileMappingW:

   switch(type){
      case ipcdetail::DoOpen:
         m_handle = winapi::open_file_mapping(map_access, filename);
      break;
      case ipcdetail::DoCreate:
      case ipcdetail::DoOpenOrCreate:
      {
         m_handle = winapi::create_file_mapping
            ( winapi::invalid_handle_value, protection, size, filename
            , (winapi::interprocess_security_attributes*)perm.get_permissions());
      }
      break;
      default:
         {
            error_info err = other_error;
            throw interprocess_exception(err);
         }
   }

   if(!m_handle || (type == ipcdetail::DoCreate && winapi::get_last_error() == winapi::error_already_exists)){
      error_info err = system_error_code();
      this->priv_close();
      throw interprocess_exception(err);
   }

I would be happier if CreateFileMappingW behaved as you suggested it might.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it succeeds and it does not raise FileExistsError (_errcheck_bool receives a non-zero result value). This motivates the ctypes.get_last_error() call to check for a return value of 183.

Sorry it slipped my mind that Windows normally uses the OBJ_OPENIF disposition when creating a named kernel object. If this flag weren't used, then the underlying NtCreateSection system call would fail with STATUS_OBJECT_NAME_COLLISION, which maps to Windows ERROR_ALREADY_EXISTS. Instead it succeeds, and CreateFileMapping manually sets the last error. It's one of a small number of cases in which a WINAPI call succeeds with an error set.

PAGE_READONLY = 0x02
PAGE_EXECUTE_READWRITE = 0x04
INVALID_HANDLE_VALUE = -1
FILE_ALREADY_EXISTS = 183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constant shouldn't be necessary once other mistakes are corrected. But it should be named ERROR_ALREADY_EXISTS if it stays. It's confusing to readers if we invent our own names for well-known error codes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this detail about the appropriate name. Changed in commit 868b83d (at least until it is no longer needed).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These have now been (re-)moved to _winapi per your later suggestion as part of commit 6878533.

try:
last_error_code = _winapi.GetLastError()
if last_error_code == _winapi.ERROR_ALREADY_EXISTS:
raise FileExistsError(f"File exists: {name!r}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to include the errno and winerror values for completeness: FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), name, _winapi.ERROR_ALREADY_EXISTS).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point -- added in commit 6878533.

# Attempt to dynamically determine the existing named shared
# memory block's size which is likely a multiple of mmap.PAGESIZE.
try:
h_map = _winapi.OpenFileMappingW(PAGE_READONLY, False, name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should request FILE_MAP_READ (4) access. PAGE_READONLY (2) is a page protection for CreateFileMapping. It happens to be the same constant value as FILE_MAP_WRITE access.

CreateFileMapping determines the desired access for the section object from the given page protection. OTOH, with OpenFileMapping we have to supply the desired access explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Changed to _winapi.FILE_MAP_READ in commit 6878533.

except OSError:
raise FileNotFoundError(name)
try:
p_buf = _winapi.MapViewOfFile(h_map, PAGE_READONLY, 0, 0, 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should request FILE_MAP_READ (4) access.

It's the other way around in the underlying NT API NtMapViewOfSection. It takes the desired page protection and determines the required section access from the protection. So MapViewOfFile[Ex] actually reverses this to get the page protection in order to call NtMapViewOfSection.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also changed to _winapi.FILE_MAP_READ in commit 6878533.

h_map = _winapi.CreateFileMappingW(
INVALID_HANDLE_VALUE,
_winapi.NULL,
PAGE_EXECUTE_READWRITE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do use PAGE_EXECUTE_READWRITE protection here? mmap defaults to PAGE_READWRITE.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember mentally debating this choice but not the details. Your point that mmap defaults to PAGE_READWRITE should have dictated what was used but I somehow missed it.

Good catch -- changed to _winapi.PAGE_READWRITE in commit 6878533.

Py_END_ALLOW_THREADS

if (handle == NULL) {
PyErr_SetFromWindowsErr(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using PyErr_SetFromWindowsErrWithUnicodeFilename(0, name).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- changed in commit 6878533.

@@ -464,6 +474,41 @@ _winapi_CreateFile_impl(PyObject *module, LPCTSTR file_name,
return handle;
}

/*[clinic input]
_winapi.CreateFileMappingW -> HANDLE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'm not against using the real name with the "W" suffix. But thus far this module has used the macro names such as _winapi.CreateFileMapping.

Actually, we don't define UNICODE, and we have a few functions in _winapi that link to the ANSI version and use LPCTSTR -> LPCSTR (i.e. const char *). This includes _winapi.CreateFile. It should instead call CreateFileW and use an LPCWSTR name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making a point of this. I would prefer to stick with the established naming pattern unless there was a real reason otherwise. I have changed it to _winapi.CreateFileMapping as you suggest and likewise dropped the "W" on _winapi.OpenFileMapping in commit 6878533.


PAGE_READONLY = 0x02
PAGE_EXECUTE_READWRITE = 0x04
INVALID_HANDLE_VALUE = -1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add INVALID_HANDLE_VALUE and the memory API constants to _winapi.

WINAPI_CONSTANT(F_HANDLE, INVALID_HANDLE_VALUE);
WINAPI_CONSTANT(F_DWORD, FILE_MAP_READ);
WINAPI_CONSTANT(F_DWORD, PAGE_READWRITE);

Maybe add the rest, too, because why not.

WINAPI_CONSTANT(F_DWORD, FILE_MAP_COPY);
WINAPI_CONSTANT(F_DWORD, FILE_MAP_WRITE);
WINAPI_CONSTANT(F_DWORD, FILE_MAP_EXECUTE);
WINAPI_CONSTANT(F_DWORD, FILE_MAP_ALL_ACCESS);
WINAPI_CONSTANT(F_DWORD, SEC_IMAGE);
WINAPI_CONSTANT(F_DWORD, SEC_RESERVE);
WINAPI_CONSTANT(F_DWORD, SEC_COMMIT);
WINAPI_CONSTANT(F_DWORD, SEC_NOCACHE);
WINAPI_CONSTANT(F_DWORD, SEC_WRITECOMBINE);
WINAPI_CONSTANT(F_DWORD, SEC_LARGE_PAGES);
WINAPI_CONSTANT(F_DWORD, PAGE_NOACCESS);
WINAPI_CONSTANT(F_DWORD, PAGE_READONLY);
WINAPI_CONSTANT(F_DWORD, PAGE_WRITECOPY);
WINAPI_CONSTANT(F_DWORD, PAGE_EXECUTE);
WINAPI_CONSTANT(F_DWORD, PAGE_EXECUTE_READ);
WINAPI_CONSTANT(F_DWORD, PAGE_EXECUTE_READWRITE);
WINAPI_CONSTANT(F_DWORD, PAGE_EXECUTE_WRITECOPY);
WINAPI_CONSTANT(F_DWORD, PAGE_GUARD);
WINAPI_CONSTANT(F_DWORD, PAGE_NOCACHE);
WINAPI_CONSTANT(F_DWORD, PAGE_WRITECOMBINE);
WINAPI_CONSTANT(F_DWORD, MEM_COMMIT);
WINAPI_CONSTANT(F_DWORD, MEM_RESERVE);
WINAPI_CONSTANT(F_DWORD, MEM_FREE);
WINAPI_CONSTANT(F_DWORD, MEM_PRIVATE);
WINAPI_CONSTANT(F_DWORD, MEM_MAPPED);
WINAPI_CONSTANT(F_DWORD, MEM_IMAGE);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds plenty good to me -- all have been added to _winapi in commit 6878533. Thanks!

Py_END_ALLOW_THREADS

if (handle == NULL) {
PyErr_SetFromWindowsErr(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use PyErr_SetFromWindowsErrWithUnicodeFilename(0, name).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in commit caf0a5d.

I should have thought to change this when you suggested similar on CreateFileMapping. Thanks!

Copy link
Contributor

@giampaolo giampaolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better overall. I added some inline comments.

@@ -138,34 +130,99 @@ def unlink(self):
pass


class PosixSharedMemory(_PosixSharedMemory):
# FreeBSD (and perhaps other BSDs) limit names to 14 characters.
_SHM_SAFE_NAME_LENGTH = 14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this limit still exists and affects at least Linux, Solaris and AIX. I have code in psutil which takes this into account.

handle = INVALID_HANDLE_VALUE;
}

return handle;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you return NULL instead of INVALID_HANDLE_VALUE in case of error? It appears this way you'll return an int instead of causing an exception.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HANDLE_return_converter checks for INVALID_HANDLE_VALUE with an error set. For NULL it returns None, which supports GetStdHandle since a standard-handle value may be NULL. Also, since an error must also be set, GetCurrentProcess works with the same converter even though the value it returns is equivalent to INVALID_HANDLE_VALUE.

It's a bit confusing if you're not aware of how it's implemented. It would probably be better to have a special handle type for functions that fail with INVALID_HANDLE_VALUE, since by far the majority of WINAPI functions that return a handle (directly or as an out parameter) either use NULL or FALSE to indicate an error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eryksun: Would it make more sense to handle the change you're suggesting by opening a separate issue and PR for it?

I'm hesitant to make such a "code maintenance" change impacting unrelated code as part of this PR which is an "enhancement".

@eryksun: I will offer to help supply the patch if you will review it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a minor change, but @zooba should approve it in a new issue before going forward with a PR. I'd rename the current type to HANDLE_I and add a new HANDLE type that uses NULL for errors.

Another maintenance issue is removing the use of the TCHAR API and unintentional linking with ANSI functions such as CreateFileA, which I think would be fine to fix here if you want. The internal use of the affected functions hasn't had a problem thus far because they use ASCII strings, e.g. for pipe names, but it may eventually cause an issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's capture all of that in a new issue on b.p.o.

handle = INVALID_HANDLE_VALUE;
}

return handle;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Py_END_ALLOW_THREADS

if (size_of_buf == 0)
PyErr_SetFromWindowsErr(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

called once (and only once) across all processes which have access
to the shared memory block."""
if _USE_POSIX and self.name:
_posixshmem.shm_unlink(self.name)


encoding = "utf8"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used by ShareableList. Why not make it a class attribute of that class instead? Also not sure if there are use-cases where one may want to change this (if not it's probably better to set it private).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though it did not get included, the plan was to also supply a ShareableDict and encoding was to be used across both. Anticipating that a ShareableDict will still land in the future, let's keep it outside the class but I agree it makes sense to make this private.

Changed to private in commit 12c097d.



class PosixSharedMemory(_PosixSharedMemory):
O_CREX = os.O_CREAT | os.O_EXCL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making this private (_O_CREX)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Changed to private in commit 12c097d.


if (fd < 0) {
if (!async_err)
PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like you want to Py_DECREF(path) here?

Copy link
Member Author

@applio applio Feb 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I am missing something... It looks like PyUnicode_AsUTF8 does not create a new reference so there is nothing to decrement?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you are right.

Modules/_multiprocessing/posixshmem.c Show resolved Hide resolved
@applio applio merged commit e895de3 into python:master Feb 24, 2019
@bedevere-bot
Copy link

@applio: Please replace # with GH- in the commit message next time. Thanks!

@@ -8,7 +8,8 @@
# Licensed to PSF under a Contributor Agreement.
#

__all__ = [ 'BaseManager', 'SyncManager', 'BaseProxy', 'Token' ]
__all__ = [ 'BaseManager', 'SyncManager', 'BaseProxy', 'Token',
'SharedMemoryManager' ]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to this comment, sounds like adding SharedMemoryManager to __all__ should be handled to the try...except below.

PS: Nice work on this btw!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants