Skip to content

Commit 153b227

Browse files
committed
Refactor shared library manager to be thread-safe and reorganize code
1 parent 0b2e8da commit 153b227

File tree

3 files changed

+147
-121
lines changed

3 files changed

+147
-121
lines changed

src/axelrod_fortran/player.py

Lines changed: 21 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,37 @@
1-
from collections import defaultdict
2-
from ctypes import cdll, c_int, c_float, byref, POINTER
3-
from ctypes.util import find_library
4-
import os
5-
import platform
1+
from ctypes import byref, c_float, c_int, POINTER
62
import random
7-
import re
8-
import shutil
9-
import subprocess
10-
import tempfile
11-
import uuid
123
import warnings
134

145
import axelrod as axl
156
from axelrod.interaction_utils import compute_final_score
167
from axelrod.action import Action
8+
179
from .strategies import characteristics
10+
from .shared_library_manager import MultiprocessManager, load_library
1811

1912
C, D = Action.C, Action.D
2013
actions = {0: C, 1: D}
2114
original_actions = {C: 0, D: 1}
2215

2316

24-
path_regex = r""".*?\s=>\s(.*?{}.*?)\\"""
25-
2617
self_interaction_message = """
2718
You are playing a match with the same player against itself. However
2819
axelrod_fortran players share memory. You can initialise another instance of an
2920
Axelrod_fortran player with player.clone().
3021
"""
3122

3223

33-
class LibraryManager(object):
34-
"""LibraryManager creates and loads copies of a shared library, which
35-
enables multiple copies of the same strategy to be run without the end user
36-
having to maintain many copies of the shared library.
37-
38-
This works by making a copy of the shared library file and loading it into
39-
memory again. Loading the same file again will return a reference to the
40-
same memory addresses.
41-
42-
Additionally, library manager tracks how many copies of the library have
43-
been loaded, and how many copies there are of each Player, so as to load
44-
only as many copies of the shared library as needed.
45-
"""
46-
47-
def __init__(self, shared_library_name, verbose=False):
48-
self.shared_library_name = shared_library_name
49-
self.verbose = verbose
50-
self.library_copies = []
51-
self.player_indices = defaultdict(set)
52-
self.player_next = defaultdict(set)
53-
# Generate a random prefix for tempfile generation
54-
self.prefix = str(uuid.uuid4())
55-
self.library_path = self.find_shared_library(shared_library_name)
56-
self.filenames = []
57-
58-
def find_shared_library(self, shared_library_name):
59-
# Hack for Linux since find_library doesn't return the full path.
60-
if 'Linux' in platform.system():
61-
output = subprocess.check_output(["ldconfig", "-p"])
62-
for line in str(output).split(r"\n"):
63-
rhs = line.split(" => ")[-1]
64-
if shared_library_name in rhs:
65-
return rhs
66-
raise ValueError("{} not found".format(shared_library_name))
67-
else:
68-
return find_library(
69-
shared_library_name.replace("lib", "").replace(".so", ""))
70-
71-
def load_dll_copy(self):
72-
"""Load a new copy of the shared library."""
73-
# Copy the library file to a new location so we can load the copy.
74-
temp_directory = tempfile.gettempdir()
75-
copy_number = len(self.library_copies)
76-
new_filename = os.path.join(
77-
temp_directory,
78-
"{}-{}-{}".format(
79-
self.prefix,
80-
str(copy_number),
81-
self.shared_library_name)
82-
)
83-
if self.verbose:
84-
print("Loading {}".format(new_filename))
85-
shutil.copy2(self.library_path, new_filename)
86-
self.filenames.append(new_filename)
87-
shared_library = cdll.LoadLibrary(new_filename)
88-
self.library_copies.append(shared_library)
89-
90-
def next_player_index(self, name):
91-
"""Determine the index of the next free shared library copy to
92-
allocate for the player. If none is available then load another copy."""
93-
# Is there a free index?
94-
if len(self.player_next[name]) > 0:
95-
return self.player_next[name].pop()
96-
# Do we need to load a new copy?
97-
player_count = len(self.player_indices[name])
98-
if player_count == len(self.library_copies):
99-
self.load_dll_copy()
100-
return player_count
101-
# Find the first unused index
102-
for i in range(len(self.library_copies)):
103-
if i not in self.player_indices[name]:
104-
return i
105-
raise ValueError("We shouldn't be here.")
106-
107-
def load_library_for_player(self, name):
108-
"""For a given player return a copy of the shared library for use
109-
in a Player class, along with an index for later releasing."""
110-
index = self.next_player_index(name)
111-
self.player_indices[name].add(index)
112-
if self.verbose:
113-
print("allocating {}".format(index))
114-
return index, self.library_copies[index]
115-
116-
def release(self, name, index):
117-
"""Release the copy of the library so that it can be re-allocated."""
118-
self.player_indices[name].remove(index)
119-
if self.verbose:
120-
print("releasing {}".format(index))
121-
self.player_next[name].add(index)
122-
123-
def __del__(self):
124-
"""Cleanup temp files on object deletion."""
125-
for filename in self.filenames:
126-
if os.path.exists(filename):
127-
os.remove(filename)
24+
# Initialize a module-wide manager for loading copies of the shared library.
25+
manager = MultiprocessManager()
26+
manager.start()
27+
shared_library_manager = manager.SharedLibraryManager("libstrategies.so")
12828

12929

13030
class Player(axl.Player):
13131

13232
classifier = {"stochastic": True}
133-
library_manager = None
13433

135-
def __init__(self, original_name,
136-
shared_library_name='libstrategies.so'):
34+
def __init__(self, original_name):
13735
"""
13836
Parameters
13937
----------
@@ -144,17 +42,16 @@ def __init__(self, original_name,
14442
A instance of an axelrod Game
14543
"""
14644
super().__init__()
147-
if not Player.library_manager:
148-
Player.library_manager = LibraryManager(shared_library_name)
149-
self.index, self.shared_library = \
150-
self.library_manager.load_library_for_player(original_name)
45+
self.index, self.shared_library_filename = \
46+
shared_library_manager.get_filename_for_player(original_name)
47+
self.shared_library = load_library(self.shared_library_filename)
15148
self.original_name = original_name
15249
self.original_function = self.original_name
153-
15450
is_stochastic = characteristics[self.original_name]['stochastic']
15551
if is_stochastic is not None:
15652
self.classifier['stochastic'] = is_stochastic
15753

54+
15855
def __enter__(self):
15956
return self
16057

@@ -216,15 +113,21 @@ def strategy(self, opponent):
216113
my_last_move)
217114
return actions[original_action]
218115

116+
def _release_shared_library(self):
117+
try:
118+
shared_library_manager.release(self.original_name, self.index)
119+
except FileNotFoundError:
120+
pass
121+
219122
def reset(self):
220-
# Release the library before rest, which regenerates the player.
221-
self.library_manager.release(self.original_name, self.index)
123+
# Release the shared library since the object is rebuilt on reset.
124+
self._release_shared_library()
222125
super().reset()
223126
self.original_function = self.original_name
224127

225128
def __del__(self):
226129
# Release the library before deletion.
227-
self.library_manager.release(self.original_name, self.index)
130+
self._release_shared_library()
228131

229132
def __repr__(self):
230133
return self.original_name
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from collections import defaultdict
2+
from ctypes import cdll
3+
from ctypes.util import find_library
4+
from multiprocessing.managers import BaseManager
5+
import os
6+
import platform
7+
import shutil
8+
import subprocess
9+
import tempfile
10+
import uuid
11+
12+
13+
def load_library(filename):
14+
"""Loads a shared library."""
15+
lib = None
16+
if os.path.exists(filename):
17+
lib = cdll.LoadLibrary(filename)
18+
return lib
19+
20+
21+
class SharedLibraryManager(object):
22+
"""LibraryManager creates (and deletes) copies of a shared library, which
23+
enables multiple copies of the same strategy to be run without the end user
24+
having to maintain many copies of the shared library.
25+
26+
This works by making a copy of the shared library file and loading it into
27+
memory again. Loading the same file again will return a reference to the
28+
same memory addresses. To be thread-safe, this class just passes filenames
29+
back to the Player class (which actually loads a reference to the library),
30+
ensuring that multiple copies of a given player type do not use the same
31+
copy of the shared library.
32+
"""
33+
34+
def __init__(self, shared_library_name, verbose=False):
35+
self.shared_library_name = shared_library_name
36+
self.verbose = verbose
37+
self.filenames = []
38+
self.player_indices = defaultdict(set)
39+
self.player_next = defaultdict(set)
40+
# Generate a random prefix for tempfile generation
41+
self.prefix = str(uuid.uuid4())
42+
self.library_path = self.find_shared_library(shared_library_name)
43+
44+
def find_shared_library(self, shared_library_name):
45+
# Hack for Linux since find_library doesn't return the full path.
46+
if 'Linux' in platform.system():
47+
output = subprocess.check_output(["ldconfig", "-p"])
48+
for line in str(output).split(r"\n"):
49+
rhs = line.split(" => ")[-1]
50+
if shared_library_name in rhs:
51+
return rhs
52+
raise ValueError("{} not found".format(shared_library_name))
53+
else:
54+
return find_library(
55+
shared_library_name.replace("lib", "").replace(".so", ""))
56+
57+
def load_dll_copy(self):
58+
"""Load a new copy of the shared library."""
59+
# Copy the library file to a new location so we can load the copy.
60+
temp_directory = tempfile.gettempdir()
61+
copy_number = len(self.filenames)
62+
new_filename = os.path.join(
63+
temp_directory,
64+
"{}-{}-{}".format(
65+
self.prefix,
66+
str(copy_number),
67+
self.shared_library_name)
68+
)
69+
if self.verbose:
70+
print("Loading {}".format(new_filename))
71+
shutil.copy2(self.library_path, new_filename)
72+
self.filenames.append(new_filename)
73+
74+
def next_player_index(self, name):
75+
"""Determine the index of the next free shared library copy to
76+
allocate for the player. If none is available then load another copy."""
77+
# Is there a free index?
78+
if len(self.player_next[name]) > 0:
79+
return self.player_next[name].pop()
80+
# Do we need to load a new copy?
81+
player_count = len(self.player_indices[name])
82+
if player_count == len(self.filenames):
83+
self.load_dll_copy()
84+
return player_count
85+
# Find the first unused index
86+
for i in range(len(self.filenames)):
87+
if i not in self.player_indices[name]:
88+
return i
89+
raise ValueError("We shouldn't be here.")
90+
91+
def get_filename_for_player(self, name):
92+
"""For a given player return a copy of the shared library for use
93+
in a Player class, along with an index for later releasing."""
94+
index = self.next_player_index(name)
95+
self.player_indices[name].add(index)
96+
if self.verbose:
97+
print("allocating {}".format(index))
98+
return index, self.filenames[index]
99+
100+
def release(self, name, index):
101+
"""Release the copy of the library so that it can be re-allocated."""
102+
self.player_indices[name].remove(index)
103+
if self.verbose:
104+
print("releasing {}".format(index))
105+
self.player_next[name].add(index)
106+
107+
def __del__(self):
108+
"""Cleanup temp files on object deletion."""
109+
for filename in self.filenames:
110+
if os.path.exists(filename):
111+
if self.verbose:
112+
print("deleting", filename)
113+
os.remove(filename)
114+
115+
116+
# Setup up thread safe library manager.
117+
class MultiprocessManager(BaseManager):
118+
pass
119+
120+
121+
MultiprocessManager.register('SharedLibraryManager', SharedLibraryManager)

tests/test_player.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
from ctypes import c_int, c_float, POINTER, CDLL
2+
import itertools
3+
4+
import pytest
5+
16
from axelrod_fortran import Player, characteristics, all_strategies
27
from axelrod import (Alternator, Cooperator, Defector, Match, MoranProcess,
38
Game, basic_strategies, seed)
49
from axelrod.action import Action
5-
from ctypes import c_int, c_float, POINTER, CDLL
610

7-
import itertools
8-
import pytest
911

1012
C, D = Action.C, Action.D
1113

0 commit comments

Comments
 (0)