I'm trying to understand how the actor model works by modeling a bank. First, here's some code illustrating why we need models for concurrent systems:
import time
from threading import Thread
bank = {'joe': 100}
class Withdrawal(Thread):
"""
Models a concurrent withdrawal for 'joe'. In this example, 'bank'
is a shared resource not protected and accessible from any thread.
Args:
amount (double) how much to withdraw
sleep (bool) config to sleep the thread during the withdrawal
"""
def __init__(self, amount, sleep = False):
self.amount = amount
self.sleep = sleep
Thread.__init__(self)
def run(self):
"""
Overrides method in Thread.
Returns: void
"""
balance = bank['joe']
if balance >= self.amount:
if self.sleep:
time.sleep(5)
bank['joe'] -= self.amount
t1 = Withdrawal(80, True)
t2 = Withdrawal(80)
t1.start()
t2.start()
After running the code, the balance for 'joe' should be -60 after five seconds. This is because bank is unprotected from concurrent access, and pausing for five seconds during concurrent execution means that we can't guarantee that the data won't be accessed at different states. In this case, the first thread accesses the bank after the second thread has finished withdrawing but doesn't check that a withdrawal is still possible. As a result the account goes negative.
If we model the bank and withdrawals as actors, we can protect access to the account since its state is managed on a different thread that's separate from those trying to withdraw from it.
from queue import Queue
from threading import Thread
import time
import random
class Actor(Thread):
"""
Models an actor in the actor model for concurrent computation
see https://en.wikipedia.org/wiki/Actor_model for theoretical overview
Args:
handles (dict) mapping of public methods that are callable
on message data after message has been read
"""
def __init__(self, handles):
self.handles = handles
self.mailbox = Queue()
Thread.__init__(self, daemon=True)
def run(self):
"""
Overrides method in Thread. Once the thread has started,
we listen for messages and process one by one when they are received
Returns: void
"""
self.read_messages()
def send(self, actor, message):
"""
Puts a Message in the recipient actor's mailbox
Args:
actor (Actor) to receive message
message (Message) object to send actor
Returns: void
"""
actor.mailbox.put(message)
def read_messages(self):
"""
Reads messages one at a time and calls the target class handler
Returns: void
"""
while 1:
message = self.mailbox.get()
action = message.target
if action in self.handles:
self.handles[action](message.data)
class Message:
"""
Models a message in the actor model
Args:
sender (Actor) instance that owns the message
data (dict) message data that can be consumed
target (string) function in the recipient Actor to we'd like run when read
"""
def __init__(self, sender, data, target):
self.sender = sender
self.data = data
self.target = target
class Bank(Actor):
"""
Models a bank. Can be used in concurrent computations.
Args:
bank (dict) name to amount mapping that models state of Bank
"""
def __init__(self, bank):
self.bank = bank
Actor.__init__(self, {'withdraw': lambda data: self.withdraw(data)})
def withdraw(self, data):
"""
Action handler for 'withdraw' messages. Withdraw
if we can cover the requested amount
Args:
data (dict) message data
Returns: void
"""
name, amount = data['name'], data['amount']
if self.bank[name] >= amount:
if data['sleep']:
time.sleep(2)
self.bank[name] -= amount
class Withdrawal(Actor):
"""
Models a withdrawal. Can be used in concurrent computations.
Args:
bank (Bank) shared resource to transact with
sleep (bool) config to request that the bank sleep during a withdrawal
"""
def __init__(self, bank, sleep=False):
self.bank = bank
self.sleep = sleep
Actor.__init__(self, {})
def withdraw(self, name, amount):
"""
Wrapper for sending a withdrawl message
Args:
name (string) owner of the account in our bank
amount (double) amount we'd like to withdraw
Returns: void
"""
data = {'sleep': self.sleep, 'name': name, 'amount': amount}
Actor.send(self, self.bank, Message(self, data, 'withdraw'))
Let's now test:
bank = Bank({'joe': 100})
bank.start()
actors = []
for _ in range(100):
a = Withdrawal(bank, random.randint(0, 1))
a.start()
actors.append(a)
for a in actors:
a.withdraw('joe', 15)
Is this understanding correct? Even though the bank sleeps during withdrawals, no simultaneous withdrawal can corrupt the data because it's managed on a different thread than the withdrawals.
Simultaneous withdrawal can no longer occur, true, but it's because the withdraw messages are handled serially, not concurrently, by the single Bank thread inside the Bank.read_messages loop. This means that the sleep commands are also executed serially; the entire message queue is going to stall and yield control for 2 seconds whenever the bank has to sleep during a withdrawal. (Given the Bank's modeled action, this is essentially unavoidable).
If access to an object is isolated to a single thread it is generally considered thread safe.
The other actors cannot access the bank's storage directly but only send messages requesting a withdrawal, so updates only occur in the bank thread and the check-and-set race condition in the original design is eliminated.
Related
I am simulating people movements and their elevator usage. An elevator can take up multiple persons before moving to another floor. The default process has a capacity parameter, however, these indicate the number of processes and not the number of people using the elevator at the same time.
I have tried to use multiple of the resources available, such as Container, Store, and Base. The elevator should be requested and these objects do not have the functionality to be requested. Hence, the only suitable solution is to inherent from the base.Resource class. I have tried to create a subclass Elevator, implementing from base.Resource and adjusting the function _do_get to take multiple elements from the queue. I am pretty confident that this is not the proper way to implement it and it gives an error as well: RuntimeError: <Request() object at 0x1ffb4474be0> has already been triggered. I have no clue which files to adjust to make Simpy happy. Could someone point me in the right direction?
#dataclass
class Elevator(simpy.Resource):
current_floor: int = 0
available_floors: List[int] = field(default_factory=lambda: [0, 1])
capacity: int = 3
# load_carriers: List[LoadCarrier] = field(default_factory=list)
move_time: int = 5
def __init__(self, env: Environment, capacity: int = 1, elevator_capacity: int = 1):
self.elevator_capacity = elevator_capacity
if capacity <= 0:
raise ValueError('"capacity" must be > 0.')
super().__init__(env, capacity)
self.users: List[Request] = []
"""List of :class:`Request` events for the processes that are currently
using the resource."""
self.queue = self.put_queue
"""Queue of pending :class:`Request` events. Alias of
:attr:`~simpy.resources.base.BaseResource.put_queue`.
"""
#property
def count(self) -> int:
"""Number of users currently using the resource."""
return len(self.users)
if TYPE_CHECKING:
def request(self) -> Request:
"""Request a usage slot."""
return Request(self)
def release(self, request: Request) -> Release:
"""Release a usage slot."""
return Release(self, request)
else:
request = BoundClass(Request)
release = BoundClass(Release)
def _do_put(self, event: Request) -> None:
if len(self.users) < self.capacity:
self.users.append(event)
event.usage_since = self._env.now
event.succeed()
def _do_get(self, event: Release) -> None:
for i in range(min(self.elevator_capacity, len(self.users))):
try:
event = self.users.pop(0)
event.succeed()
# self.users.remove(event.request) # type: ignore
except ValueError:
pass
# event.succeed()
So here is the solution I came up with. The tricky bit is I chained two events together. When you queue up for the elevator you get a event that fires when the elevator arrives. This event also returns a second event that fires when you get to your destination floor. This second event is a common event shared by all the passengers that are on the elevator and going to the same floor. Firing this one event notifies a bunch of passengers. This subscribe broadcast pattern can greatly reduce the number of events the model needs to process which in turn improves performance. I use the chained events because if you are in a queue, and the guy in front of you gets on and you do not, then that guy is also going to get off before you, requiring a different destination arrive event. Put another way, I do not know when you will get off until you get on, so I need to defer that part till you actually get onto the elevator.
"""
Simple elevator demo using events to implements a subscribe, broadcast pattern to let passengers know when
they have reached there floor. All the passengers getting off on the same floor are waiting on the
same one event.
Programmer: Michael R. Gibbs
"""
import simpy
import random
class Passenger():
"""
simple class with unique id per passenger
"""
next_id = 1
#classmethod
def get_next_id(cls):
id = cls.next_id
cls.next_id += 1
return id
def __init__(self):
self.id = self.get_next_id()
class Elevator():
""""
Elevator that move people from floor to floor
Has a max compatity
Uses a event to notifiy passengers when they can get on the elevator
and when they arrive at their destination floor
"""
class Move_Goal():
"""
wrapps passengers so we can track where they are going to
"""
def __init__(self, passenger, start_floor, dest_floor, onboard_event):
self.passenger = passenger
self.start_floor = start_floor
self.dest_floor = dest_floor
self.onboard_event = onboard_event
self.arrive_event = None
def __init__(self,env, passenger_cap, floors):
self.env = env
self.passenger_cap = passenger_cap
self.floors = floors
self.on_floor = 0
self.move_inc = 1
# list of passengers on elevator, one per floor
self.on_board = {f:[] for f in range(1,floors + 1)}
# queue for passengers waitting to get on elevator, one queue per floor
self.boarding_queues = {f:[] for f in range(1,floors + 1)}
# events to notify passengers when they have arrived at their floor, one per floor
self.arrive_events = {f: simpy.Event(env) for f in range(1, floors + 1)}
# start elevator
env.process(self._move_next_floor())
def _move_next_floor(self):
"""
Moves the elevator up and down
Elevator stops at every floor
"""
while True:
# move time to next floor
yield self.env.timeout(5)
# update floor elevator is at
self.on_floor = self.on_floor + self.move_inc
# check if elevator needs to change direction
if self.on_floor == self.floors:
self.move_inc = -1
elif self.on_floor == 1:
self.move_inc = 1
# unload and notify passengers that want to get of at this floor
arrive_event = self.arrive_events[self.on_floor]
self.arrive_events[self.on_floor] = simpy.Event(self.env)
arrive_event.succeed()
self.on_board[self.on_floor] = []
# load new passengers
# get open capacity
used_cap = 0
for p in self.on_board.values():
used_cap += len(p)
open_cap = self.passenger_cap - used_cap
# get boarding passengers
boarding = self.boarding_queues[self.on_floor][:open_cap]
self.boarding_queues[self.on_floor] = self.boarding_queues[self.on_floor][open_cap:]
# sort bording into dest floors
for p in boarding:
# give passenger common event for arriving at destination floor
p.arrive_event = self.arrive_events[p.dest_floor]
# notify passeger that they are onboard the elevator
p.onboard_event.succeed()
self.on_board[p.dest_floor].append(p)
def move_to(self, passenger, from_floor, to_floor):
"""
Return a event that fires when the passenger gets on the elevator
The event returns another event that fires when the passager
arrives at their destination floor
(uses the env.process() to convert a process to a event)
"""
return self.env.process(self._move_to(passenger, from_floor, to_floor))
def _move_to(self, passenger, from_floor, to_floor):
"""
Puts the passenger into a queue for the elevator
"""
# creat event to notify passenger when they can get onto the elemator
onboard_event = simpy.Event(self.env)
# save move data in a wrapper and put passenger into queue
move_goal = self.Move_Goal(passenger, from_floor, to_floor, onboard_event)
self.boarding_queues[from_floor].append(move_goal)
# wait for elevator to arrive, and have space for passenger
yield onboard_event
# get destination arrival event
dest_event = self.arrive_events[to_floor]
move_goal.arrive_event = dest_event
return dest_event
def use_elevator(env, elevator, passenger, start_floor, end_floor):
"""
process for using a elevator to move from one floor to another
"""
print(f'{env.now:.2f} passenger {passenger.id} has queued on floor {start_floor}')
arrive_event = yield elevator.move_to(passenger, start_floor, end_floor)
print(f'{env.now:.2f} passenger {passenger.id} has boarded on floor {start_floor}')
yield arrive_event
print(f'{env.now:.2f} passenger {passenger.id} has arrived on floor {end_floor}')
def gen_passengers(env, elevator):
"""
creates passengers to use a elevatore
"""
floor_set = {f for f in range(1, elevator.floors + 1)}
while True:
# time between arrivals
yield env.timeout(random.uniform(0,5))
# get passenger and where they want to go
passenger = Passenger()
start_floor, end_floor = random.sample(floor_set, 2)
# use the elevator to get there
env.process(use_elevator(env, elevator, passenger, start_floor, end_floor))
# boot up
env = simpy.Environment()
elevator = Elevator(env, 20, 3)
env.process(gen_passengers(env, elevator))
env.run(100)
is it possible in Dart to instantiate a class in an isolate, and then send message to this isolate to receive a return value from its methods (instead of spawning a new isolate and re instantiate the same class every time)? I have a class with a long initialization, and heavy methods. I want to initialize it once and then access its methods without compromising the performance of the main isolate.
Edit: I mistakenly answered this question thinking python rather than dart. snakes on the brain / snakes on a plane
I am not familiar with dart programming, but it would seem the concurrency model has a lot of similarities (isolated memory, message passing, etc..). I was able to find an example of 2 way message passing with a dart isolate. There's a little difference in how it gets set-up, and the streams are a bit simpler than python Queue's, but in general the idea is the same.
Basically:
Create a port to receive data from the isolate
Create the isolate passing it the port it will send data back on
Within the isolate, create the port it will listen on, and send the other end of it back to main (so main can send messages)
Determine and implement a simple messaging protocol for remote method call on an object contained within the isolate.
This is basically duplicating what a multiprocessing.Manager class does, however it can be helpful to have a simplified example of how it can work:
from multiprocessing import Process, Lock, Queue
from time import sleep
class HeavyObject:
def __init__(self, x):
self._x = x
sleep(5) #heavy init
def heavy_method(self, y):
sleep(.2) #medium weight method
return self._x + y
def HO_server(in_q, out_q):
ho = HeavyObject(5)
#msg format for remote method call: ("method_name", (arg1, arg2, ...), {"kwarg1": 1, "kwarg2": 2, ...})
#pass None to exit worker cleanly
for msg in iter(in_q.get, None): #get a remote call message from the queue
out_q.put(getattr(ho, msg[0])(*msg[1], **msg[2])) #call the method with the args, and put the result back on the queue
class RMC_helper: #remote method caller for convienience
def __init__(self, in_queue, out_queue, lock):
self.in_q = in_queue
self.out_q = out_queue
self.l = lock
self.method = None
def __call__(self, *args, **kwargs):
if self.method is None:
raise Exception("no method to call")
with self.l: #isolate access to queue so results don't pile up and get popped off in possibly wrong order
print("put to queue: ", (self.method, args, kwargs))
self.in_q.put((self.method, args, kwargs))
return self.out_q.get()
def __getattr__(self, name):
if not name.startswith("__"):
self.method = name
return self
else:
super().__getattr__(name)
def child_worker(remote):
print("child", remote.heavy_method(5)) #prints 10
sleep(3) #child works on something else
print("child", remote.heavy_method(2)) #prints 7
if __name__ == "__main__":
in_queue = Queue()
out_queue = Queue()
lock = Lock() #lock is used as to not confuse which reply goes to which request
remote = RMC_helper(in_queue, out_queue, lock)
Server = Process(target=HO_server, args=(in_queue, out_queue))
Server.start()
Worker = Process(target=child_worker, args=(remote, ))
Worker.start()
print("main", remote.heavy_method(3)) #this will *probably* start first due to startup time of child
Worker.join()
with lock:
in_queue.put(None)
Server.join()
print("done")
B)The BankAccount class should then have two child classes, named SavingsAccount and NonTaxFilerAccount. The SavingsAccount class should have a ZakatDeduction( )function which deducts an amount of 2.5% of the current account balancewhen called. ο‚·The NonTaxFilerAccount class should have the withdraw function from the parent class overwritten. Where a 2% withholding tax is deducted from the account every timea withdrawal is made.
i did the first part but not getting the second one it keeps giving me attribute error
class BankAccount:
def __init__(self, init_bal):
"""Creates an account with the given balance."""
self.init_bal = init_bal
self.account = init_bal
def deposit(self, amount):
"""Deposits the amount into the account."""
self.amount = amount
self.account += amount
def withdraw(self, amount):
self.account -= amount
def balance(self):
print (self.account)
class SavingsAccount(BankAccount) :
def ZakatDeduction(self,amount):
self.account=amount*0.25
print(self.account)
class NonTaxFilerAccount(BankAccount):
def withdraw(self, amount):
self.account -= amount*(0.2)
print(self.account)
x = BankAccount(700)
x.balance()
y=BankAccount
y.SavingsAccount(700)
z=BankAccount
z.withdraw(70)
I think you have several problems. Basically your implementation of the BankAccount, SavingsAccount, and NonTaxFilerAccount classes are structurally correct. However:
Since the instructions say to reduce the account balance by 2.5% every time the ZakatDeduction is called, you should update the method to remove the amount as follows:
def ZakatDeduction(self):
self.account -= self.account*0.025
print(self.account)
Since the instructions say to reduce the account balance by an additional 2% of the withdrawn amount when a NonTaxFiler makes a withdraw3al you should update the NonTaxFiler withdraw method as follows:
def withdraw(self, amount):
self.account -= amount*(1.02)
print(self.account)
Employing these classes to create separate accounte with 700 balance should be as follows:
BA = BankAccount(700) #Create a base account
SA = SavingAccount(700) #Create a Savings Account
NTF = NonTaxFiler(700) #Create a NonTaxFilerAccount
performing the following then yields:
BA.withdraw(25)
BA.balance()
675
SA = SavingsAccount(700)
SA.ZakatDeduction()
682.5
NTF = NonTaxFilerAccount(700)
NTF.withdraw(25)
674.5
The attribute error is correct, and in fact its message tells you the issue. Your classes are ok. The error is in the usage. You have:
y=BankAccount
y.SavingsAccount(700)
This means that the y variable now refers to the BankAccount class. The next line attempts to call y.SavingsAccount, and the BankAccount class does not have a method called SavingsAccount.
Did you perhaps mean:
y = SavingsAccount(700)
Note that python is whitespace specific. While technically valid, for readability, you should use the same level of indentation everywhere, but some of your methods are indented by 4, while others by 3
I've been reading up on the Observer Design Pattern.
Take the below code from the following article:
https://sourcemaking.com/design_patterns/observer/python/1
I understand how the code works and how the two classes, subject and observer, interrelate. What I can't quite get my head round is how this would all work in reality.
In the real world examples of the observer pattern I have read about, the interaction between the subject and observer tends to be open ended.
For example, if you subscribe to a question on Quora and get notified of answers via email, theoretically, you could receive updates indefinitely.
How then if the code below was applied to a real world scenario (like Quora) do the classes persist? Are the classes and their states stored somewhere on the server?
import abc
class Subject:
"""
Know its observers. Any number of Observer objects may observe a
subject.
Send a notification to its observers when its state changes.
"""
def __init__(self):
self._observers = set()
self._subject_state = None
def attach(self, observer):
observer._subject = self
self._observers.add(observer)
def detach(self, observer):
observer._subject = None
self._observers.discard(observer)
def _notify(self):
for observer in self._observers:
observer.update(self._subject_state)
#property
def subject_state(self):
return self._subject_state
#subject_state.setter
def subject_state(self, arg):
self._subject_state = arg
self._notify()
class Observer(metaclass=abc.ABCMeta):
"""
Define an updating interface for objects that should be notified of
changes in a subject.
"""
def __init__(self):
self._subject = None
self._observer_state = None
#abc.abstractmethod
def update(self, arg):
pass
class ConcreteObserver(Observer):
"""
Implement the Observer updating interface to keep its state
consistent with the subject's.
Store state that should stay consistent with the subject's.
"""
def update(self, arg):
self._observer_state = arg
# ...
def main():
subject = Subject()
concrete_observer = ConcreteObserver()
subject.attach(concrete_observer)
subject.subject_state = 123
if __name__ == "__main__":
main()
You do not need to persist the classes. All you need to do is persist the data. In your real-world example, Quora just stores the list of subscribers for each questions. If there is any activity, it just needs to call some Notification service, passing along the list, which eventually will send the mail to subscribed users.
I am trying to learn python ( and programming in general). For now I am trying to make a simple bank where users can send/deposit/withdraw money.
I have already created the deposit and withdraw functions and are working. Now I am totally confused on how to write the send function as the user will be sending money and the other will be receiving the money.
Should I write 2 seperate functions fo send and receive, but then how to trigger both in the same time? ( another function containing both) ?
I hope you can help me with this,
so far this is my code:
Classes:
class Account(object):
def __init__(self, name, account_number, initial_amount):
self.name = name
self.no = account_number
self.balance = initial_amount
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
def dump(self):
s = '%s, %s, balance: %s' % \
(self.name, self.no, self.balance)
print s
def get_balance(self):
print(self.balance)
def send(self, sender, receiver, amount):
self.sender = sender
self.receiver = receiver
self.balance -= amount
main.py:
from classes.Account import Account
a1 = Account('John Doe', '19371554951', 20000)
a2 = Account('Jenny Doe', '19371564761', 20000)
a1.deposit(1000)
a1.withdraw(4000)
a2.withdraw(10500)
a2.withdraw(3500)
a1.get_balance()
I know this may be basic, but I hope I can get help here.
Thank you
You already have deposit and withdraw methods so you might as well use them.
Transferring money is essentially withdrawing from one account and depositing it in another.
This can be implemented with a static method that accepts 2 accounts and the amount which will encapsulate the idea of "transfer":
class Account:
.
.
.
#staticmethod
def transfer(from_account, to_account, amount):
from_account.withdraw(amount)
to_account.deposit(amount)
# TODO perhaps you will want to use a try-except block
# to implement a transaction: if either withdrawing or depositing
# fails you will want to rollback the changes.
usage:
from classes.Account import Account
a1 = Account('John Doe', '19371554951', 20000)
a2 = Account('Jenny Doe', '19371564761', 20000)
print(a1.balance)
print(a2.balance)
Account.transfer(a1, a2, 10)
print(a1.balance)
print(a2.balance)
# 20000
# 20000
# 19990
# 20010