mirror of
https://gitlab.com/fbb-git/cppannotations
synced 2024-11-16 07:48:44 +01:00
WIP on stl's condition_variable sections
This commit is contained in:
parent
dad3acc040
commit
f83d210530
4 changed files with 220 additions and 133 deletions
|
@ -120,7 +120,7 @@ includefile(stl/threading)
|
|||
subsect(Synchronization (mutexes))
|
||||
includefile(stl/mutex)
|
||||
|
||||
subsect(Locks and lock handling)
|
||||
lsubsect(LOCKS)(Locks and lock handling)
|
||||
includefile(stl/locks)
|
||||
|
||||
subsubsect(Deadlocks)
|
||||
|
@ -129,10 +129,10 @@ includefile(stl/threading)
|
|||
subsect(Event handling (condition variables))
|
||||
includefile(stl/events)
|
||||
|
||||
subsubsect(The class 'condition_variable')
|
||||
lsubsubsect(CONDVAR1)(The class 'condition_variable')
|
||||
includefile(stl/conditionvar)
|
||||
|
||||
subsubsect(The class 'condition_variable_any')
|
||||
lsubsubsect(CONDVAR2)(The class 'condition_variable_any')
|
||||
includefile(stl/conditionany)
|
||||
|
||||
lsubsubsect(CONDEX)(An example using condition variables)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
Different from the class tt(condition_variable) the class
|
||||
ti(condition_variable_any) can be used with any (e.g., user-supplied) lock
|
||||
type, and not just with the stl-provided tt(unique_lock<mutex>).
|
||||
hi(condition_variable_any)tt(std::condition_variable_any) can be used with
|
||||
any (e.g., user supplied) lock type, and not just with the stl-provided
|
||||
tt(unique_lock<mutex>).
|
||||
|
||||
Before using the class tt(condition_variable_any) the tthi(condition_variable)
|
||||
header file must have been included.
|
||||
|
@ -8,8 +9,8 @@ header file must have been included.
|
|||
The functionality that is offered by tt(condition_variable_any) is identical
|
||||
to the functionality offered by the class tt(condition_variable), albeit that
|
||||
the lock-type that is used by tt(condition_variable_any) is not
|
||||
predefined. The class tt(condition_variable_any) requires specification of the
|
||||
lock-type that must be used by its objects.
|
||||
predefined. The class tt(condition_variable_any) therefore requires the
|
||||
specification of the lock-type that must be used by its objects.
|
||||
|
||||
In the interface shown below this lock-type is referred to as ti(Lock). Most
|
||||
of tt(condition_variable_any's) members are defined as member templates,
|
||||
|
@ -25,7 +26,7 @@ instead of just tt(unique_lock) to corresponding members), the reader is
|
|||
referred to the previous section for a description of the semantics of the
|
||||
class members.
|
||||
|
||||
Like tt(condition_variable), the class tt(condition_variable_any) merely
|
||||
Like tt(condition_variable), the class tt(condition_variable_any) only
|
||||
offers a default constructor. No copy constructor or overloaded assignment
|
||||
operator are provided.
|
||||
|
||||
|
@ -38,8 +39,19 @@ Note that, in addition to tt(Lock), the types tt(Clock, Duration, Period,
|
|||
Predicate,) and tt(Rep) are template types, defined just like the identically
|
||||
named types mentioned in the previous section.
|
||||
|
||||
The class tt(condition_variable_any's) members are:
|
||||
Assuming that tt(MyMutex) is a user defined mutex type, and that tt(MyLock) is
|
||||
a user defined lock-type (cf. section ref(LOCKS) for details about
|
||||
lock-types), then a tt(condition_variable_any) object can be defined and used
|
||||
like this:
|
||||
verb(
|
||||
MyMutex mut;
|
||||
MyLock<MyMutex> ul(mut);
|
||||
condition_variable_any cva;
|
||||
|
||||
cva.wait(ul);
|
||||
)
|
||||
|
||||
Here are the class tt(condition_variable_any's) members:
|
||||
itemization(
|
||||
itht(notify_one)(void notify_one() noexcept;)
|
||||
itht(notify_all)(void notify_all() noexcept;)
|
||||
|
@ -58,3 +70,5 @@ The class tt(condition_variable_any's) members are:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,93 +1,147 @@
|
|||
The class ti(condition_variable) merely offers a default constructor. No copy
|
||||
constructor or overloaded assignment operator are provided.
|
||||
The class tt(std::condition_variable)hi(condition_variable) merely offers a
|
||||
default constructor. No copy constructor or overloaded assignment operator are
|
||||
provided.
|
||||
|
||||
Before using the class tt(condition_variable) the tthi(condition_variable)
|
||||
header file must have been included.
|
||||
|
||||
The class's destructor requires that no thread is blocked by the current
|
||||
thread. This implies that all other (waiting) threads must have been notified;
|
||||
those threads may, however, subsequently block on the lock specified in their
|
||||
tt(wait) calls.
|
||||
thread. This implies that all threads waiting on a tt(condition_variable) must
|
||||
have been notified before a tt(condition_variable) object's lifetime
|
||||
ends. Calling tt(notify_all) (see below) before a tt(condition_variable)
|
||||
ceases to exists takes care of that.
|
||||
|
||||
In the following member-descriptions a type tt(Predicate) indicates that the
|
||||
provided tt(Predicate) argument can be called as a function without arguments,
|
||||
returning a tt(bool). Also, other member functions are frequently referred
|
||||
to. It is tacitly assumed that all members were called using the same
|
||||
condition variable object.
|
||||
to. It is tacitly assumed that all member referred to below were called using
|
||||
the same condition variable object.
|
||||
|
||||
The class tt(condition_variable) supports several tt(wait) members, which will
|
||||
block the thread until notified by another thread (or after a configurabel
|
||||
waiting time). However, these tt(wait) members may also spuriously unblock,
|
||||
without having reacquired the lock. Therefore, returning from these tt(wait)
|
||||
members threads should verify that the required data condition has actually
|
||||
been met. If not, again calling tt(wait) may be appropriate, as illustrated by
|
||||
the next piece of pseudo code:
|
||||
verb(
|
||||
while (conditionNotYetMet())
|
||||
condVariable.wait(&uniqueLock);
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
The class tt(condition_variable)'s members are:
|
||||
itemization(
|
||||
ithtq(notify_one)(void notify_one() noexcept)
|
||||
(one tt(wait) member called by other threads returns. Which one
|
||||
actually returns cannot be predicted.)
|
||||
|
||||
ithtq(notify_all)(void notify_all() noexcept)
|
||||
(all tt(wait) members called by other threads unblock their wait
|
||||
states. Of course, only one of them will subsequently succeed in
|
||||
reacquiring the condition variable's lock object.)
|
||||
ithtq(wait)(void wait(unique_lock<mutex>& lockObject))
|
||||
(the current thread is blocked until it (usually) has obtained the lock
|
||||
of tt(lockObject). However, tt(wait) may also spuriously unblock,
|
||||
without having locked tt(lockObject). Therefore, returning from
|
||||
tt(wait) threads should always verify that they have obtained the
|
||||
lock. If not, again calling tt(wait) may be appropriate.)
|
||||
ittq(void wait(unique_lock<mutex>& lock, Predicate pred))
|
||||
(This is a member template, defining the template header tt(template
|
||||
<typename Predicate>). As long as `tt(pred())' returns tt(false)
|
||||
tt(wait(lock)) is called.)
|
||||
ithtq(wait_for)(cv_status wait_for(unique_lock<mutex> &lockObject,
|
||||
chrono::duration<Rep, Period> const &relTime))
|
||||
(This member is defined as a member template, using the template header
|
||||
tt(template <typename Rep, typename Period>). The tt(Rep) and
|
||||
tt(Period) types are derived from the actual tt(relTime) argument
|
||||
that is passed to this member, and should not explicitly be specified.
|
||||
|
||||
The tt(lockObject) must be locked by the current thread and either no
|
||||
other thread is waiting on this tt(condition_variable) object, or
|
||||
tt(lock.mutex()) returns the same value for each of the tt(lockObject)
|
||||
arguments supplied by all currently waiting threads.
|
||||
ithtq(wait)(void wait(unique_lock<mutex>& uniqueLock))
|
||||
(before calling tt(wait) the current thread must have acquired the lock
|
||||
of tt(uniqueLock). Calling tt(wait) releases the lock, and the current
|
||||
thread is blocked until it has received a notification from another
|
||||
thread, and has reacquired the lock.
|
||||
|
||||
This member calls tt(lockObject.unlock) and the current thread is
|
||||
blocked. It unblocks when receiving a signal through a tt(notify)
|
||||
member, when an interval specified by tt(relTime) has passed, or
|
||||
spuriously. Once it unblocks it tries to reacquire the lock
|
||||
on tt(lockObject). Before this member returns the current thread
|
||||
has acquired the lock on tt(lockObject). If returning due to a
|
||||
timeout, tt(cv_status::timeout) is returned, otherwise
|
||||
tt(cv_status::no_timeout) is returned.)
|
||||
ittq(bool wait_for(unique_lock<mutex> &lockObject,
|
||||
Threads should verify that the required data condition has been met
|
||||
after tt(wait) has returned.)
|
||||
|
||||
ittq(void wait(unique_lock<mutex>& uniqueLock, Predicate pred))
|
||||
(this is a member template, using the template header tt(template
|
||||
<typename Predicate>).
|
||||
The template's type is automatically derived from the function's
|
||||
argument type and does not have to be specified explicitly.
|
||||
|
||||
Before calling tt(wait) the current thread must have acquired the lock
|
||||
of tt(uniqueLock). As long as `tt(pred)' returns tt(false)
|
||||
tt(wait(lock)) is called.
|
||||
|
||||
Threads should verify that the required data condition has been met
|
||||
after tt(wait) has returned.)
|
||||
|
||||
ithtq(wait_for)(cv_status wait_for(unique_lock<mutex> &uniqueLock,
|
||||
std::chrono::duration<Rep, Period> const &relTime))
|
||||
(this member is defined as a member template, using the template header
|
||||
tt(template <typename Rep, typename Period>).
|
||||
The template's types are automatically derived from the typs of the
|
||||
function's arguments and do not have to be specified explicitly.
|
||||
E.g., to wait for at most 5 seconds tt(wait_for) can be called like
|
||||
this:
|
||||
verb(
|
||||
cond.wait_for(&unique_lock, std::chrono::seconds(5));
|
||||
)
|
||||
This member returns when being notified or when the time interval
|
||||
specified by tt(relTime) has passed. When returning due to a timeout,
|
||||
tt(std::chrono::cv_status::timeout) is returned, otherwise
|
||||
tt(std::chrono::cv_status::no_timeout) is returned.
|
||||
|
||||
Threads should verify that the required data condition has been met
|
||||
after tt(wait_for) has returned.)
|
||||
|
||||
ittq(bool wait_for(unique_lock<mutex> &uniqueLock,
|
||||
chrono::duration<Rep, Period> const &relTime, Predicate
|
||||
pred))
|
||||
(this member is also defined as a member template, using the template
|
||||
(this member is defined as a member template, using the template
|
||||
header tt(template <typename Rep, typename Period, typename
|
||||
Predicate>). The template types are automatically derived from the
|
||||
types of the arguments that passed to this member.
|
||||
Predicate>).
|
||||
The template's types are automatically derived from the typs of the
|
||||
function's arguments and do not have to be specified explicitly.
|
||||
|
||||
As long as tt(pred()) returns false, the previous member is called. If
|
||||
the previous member returns tt(cv_status::timeout), then tt(pred()) is
|
||||
returned, otherwise tt(true).)
|
||||
ithtq(wait_until)(cv_status wait_until(unique_lock<mutex>& lockObject,
|
||||
As long as tt(pred) returns false, the previous tt(wait_for) member is
|
||||
called. If the previous member returns tt(cv_status::timeout), then
|
||||
tt(pred) is returned, otherwise tt(true).
|
||||
|
||||
Threads should verify that the required data condition has been met
|
||||
after tt(wait_for) has returned.)
|
||||
|
||||
ithtq(wait_until)(cv_status wait_until(unique_lock<mutex>& uniqueLock,
|
||||
chrono::time_point<Clock, Duration> const &absTime))
|
||||
(This member is also defined as a member template, using the template
|
||||
header tt(template <typename Clock, typename Duration>). The template
|
||||
types are derived from the types of the arguments that are passed to
|
||||
this member and do not have to be specified explicitly.
|
||||
|
||||
(this member is defined as a member template, using the template
|
||||
header tt(template <typename Clock, typename Duration>).
|
||||
The template's types are automatically derived from the typs of the
|
||||
function's arguments and do not have to be specified explicitly.
|
||||
E.g., to wait until 5 minutes after the current time tt(wait_until) can
|
||||
be called like this:
|
||||
verb(
|
||||
cond.wait_until(&unique_lock, chrono::system_clock::now() +
|
||||
std::chrono::minutes(5));
|
||||
)
|
||||
This function acts identically to the tt(wait_for(unique_lock<mutex>
|
||||
&lockObject, chrono::duration<Rep, Period> const &relTime)) member,
|
||||
but uses an absolute point in time, rather than a relative time
|
||||
specification. If returning due to a timeout, tt(cv_status::timeout)
|
||||
is returned, otherwise tt(cv_status::no_timeout) is returned.)
|
||||
&uniqueLock, chrono::duration<Rep, Period> const &relTime)) member
|
||||
described earlier, but uses an absolute point in time, rather than a
|
||||
relative time specification.
|
||||
|
||||
This member returns when being notified or when the time interval
|
||||
specified by tt(relTime) has passed. When returning due to a timeout,
|
||||
tt(std::chrono::cv_status::timeout) is returned, otherwise
|
||||
tt(std::chrono::cv_status::no_timeout) is returned.
|
||||
|
||||
Threads should verify that the required data condition has been met
|
||||
after tt(wait_until) has returned.)
|
||||
|
||||
ittq(bool wait_until(unique_lock<mutex> &lock,
|
||||
chrono::time_point<Clock, Duration> const &absTime,
|
||||
Predicate pred))
|
||||
(this member is also defined as a member template, using the template
|
||||
header tt(template <typename Clock, typename Duration, typename
|
||||
Predicate>). The template types are derived from the types of the
|
||||
arguments that are passed to this member and do not have to be
|
||||
specified explicitly.
|
||||
(this member is defined as a member template, using the template header
|
||||
tt(template <typename Clock, typename Duration, typename Predicate>).
|
||||
The template's types are automatically derived from the types of the
|
||||
function's arguments and do not have to be specified explicitly.
|
||||
|
||||
As long as tt(pred()) returns false, the previous member is called. If
|
||||
the previous member returns tt(cv_status::timeout), then tt(pred()) is
|
||||
returned, otherwise tt(true). )
|
||||
As long as tt(pred) returns false, the previous tt(wait_until) member
|
||||
is called. If the previous member returns tt(cv_status::timeout), then
|
||||
tt(pred) is returned, otherwise tt(true).
|
||||
|
||||
Threads should verify that the required data condition has been met
|
||||
after tt(wait_until) has returned.)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,101 +1,118 @@
|
|||
In this section em(condition variables) are introduced, allowing programs to
|
||||
synchronize threads on the em(states) of data, rather than on the em(access)
|
||||
to data, which is realized using mutexes.
|
||||
In this section em(condition variables), as defined by the standard template
|
||||
library, are introduced. Condition variables allow programs to synchronize
|
||||
threads using the em(states) of data, rather than simply locking the
|
||||
em(access) to data (which is realized using mutexes).
|
||||
|
||||
Before using condition variables the tthi(condition_variable) header file must
|
||||
have been included.
|
||||
Before condition variables can be used the tthi(condition_variable) header
|
||||
file must have been included.
|
||||
|
||||
To start our discussion, we consider a classic producer-consumer scenario: the
|
||||
producer generates items to be consumed by a consumer. The producer can only
|
||||
produce a certain number of items before its storage capacity has filled up
|
||||
and the client cannot consume more items than the producer has produced.
|
||||
To start our discussion, consider a classic producer-consumer scenario: the
|
||||
producer generates items which are consumed by a consumer. The producer can
|
||||
only produce a certain number of items before its storage capacity has filled
|
||||
up and the client cannot consume more items than the producer has produced.
|
||||
|
||||
At some point the producer has to wait until the client has consumed enough,
|
||||
thus creating space in the producer's storage. Similarly, the consumer cannot
|
||||
start consuming until the producer has at least produced some items.
|
||||
At some point the producer's storage capacity has filled to the brim, and the
|
||||
producer has to wait until the client has at least consumed some items,
|
||||
thereby creating space in the producer's storage. Similarly, the consumer
|
||||
cannot start consuming until the producer has at least produced some items.
|
||||
|
||||
Mutexes (data locking) don't result in elegant solutions of producer-consumer
|
||||
types of problems, as using mutexes requires repeated locking and polling the
|
||||
amount of available items/storage. This isn't a very attractive option as it
|
||||
wastes resources. Polling forces threads to wait until they own the mutex,
|
||||
even though continuation might already be possible. The polling interval could
|
||||
be reduced, but that too isn't an attractive option, as it results in
|
||||
needlessly increasing the overhead associated with handling the associated
|
||||
mutexes.
|
||||
Implementing this scenario only using mutexes (data locking) is not an
|
||||
attractive option, as merely using mutexes forces a program to implement the
|
||||
scenario using em(polling): processes must continuously (re)acquire the
|
||||
mutex's lock, determine whether they can perform some action, followed by the
|
||||
release of the lock. Often there's no action to perform, and the process is
|
||||
busy acquiring and releasing the mutex's lock. Polling forces threads to wait
|
||||
until they can lock the mutex, even though continuation might already be
|
||||
possible. The polling interval could be reduced, but that too isn't an
|
||||
attractive option, as it results in needlessly increasing the overhead
|
||||
associated with handling the associated mutexes (a situation also known as
|
||||
`busy waiting').
|
||||
|
||||
On the other hand, condition variables allow you to avoid polling by
|
||||
synchronizing threads using the em(states) (e.g., em(values)) of data.
|
||||
Contrary to merely using mutexes, polling can be prevented using condition
|
||||
variables. Using condition variables threads may em(notify) waiting threads
|
||||
that there is something for them to do. Thus threads synchronized on the
|
||||
em(states) (e.g., em(values)) of data.
|
||||
|
||||
As the the states of the data may be modified by multiple threads, threads
|
||||
still have to use mutexes, but merely to control access to the data. However,
|
||||
condition variables allow threads to release ownership of the mutex until a
|
||||
certain state has been reached, until a preset amount of time has been passed,
|
||||
or until a preset point in time has been reached.
|
||||
As the states of data may be modified by multiple threads, threads still need
|
||||
to use mutexes, but merely to control access to the data. In addition,
|
||||
however, condition variables allow threads to release ownership of mutexes
|
||||
until a certain state has been reached, until a preset amount of time has been
|
||||
passed, or until a preset point in time has been reached.
|
||||
|
||||
The prototypical setup of these kinds of programs look like this:
|
||||
The prototypical setup of threads using condition variables looks like this:
|
||||
itemization(
|
||||
it() consumer thread(s) act like this:
|
||||
verb(
|
||||
obtain ownership of the used mutex
|
||||
lock the mutex
|
||||
while the required condition is false:
|
||||
release the ownership and wait until being notified
|
||||
continue processing now that the condition is true
|
||||
release ownership of the mutex
|
||||
wait until being notified
|
||||
(automatically releasing the mutex's lock).
|
||||
the mutex's lock has been reacquired, and the condition is true:
|
||||
process the data
|
||||
release the mutex's lock.
|
||||
)
|
||||
it() producer thread(s) act like this:
|
||||
it() producer thread(s) act similarly:
|
||||
verb(
|
||||
obtain ownership of the used mutex
|
||||
lock the mutex
|
||||
while the condition is false:
|
||||
work towards changing the condition to true
|
||||
signal other waiting threads that the condition is now true
|
||||
release ownership of the mutex
|
||||
process the data
|
||||
notify waiting threads that the condition is true
|
||||
release the mutex's lock.
|
||||
)
|
||||
)
|
||||
) No matter which thread starts, the thread holding the mutex's lock will
|
||||
at some point release the lock, allowing the other process to (re)acquire
|
||||
it. If the consumer starts it immediately releases the lock once it enters its
|
||||
waiting state; if the producer starts it releases the lock once the condition
|
||||
is true. There is a slight initial synchronization requirement, though. The
|
||||
producer's notification will be missed if the consumer hasn't yet entered its
|
||||
waiting state. So waiting (consumer) threads should start before notifying
|
||||
(producer) threads. One the threads have started, no assumptions can be made
|
||||
about the order in which any of the tt(notify_one, notify_all, wait,
|
||||
wait_for), and tt(wait_until) members are executed.
|
||||
|
||||
|
||||
Condition variables come in two flavors: objects of the class
|
||||
hi(condition_variable)tt(std::condition_variable) are used in combination
|
||||
with objects of type tt(unique_lock<mutex>). This combination allows
|
||||
with objects of type tt(unique_lock<mutex>). This allows for certain
|
||||
optimizations resulting in an increased efficiency compared to the efficiency
|
||||
that can be obtained with objects of the class
|
||||
hi(condition_variable_any)tt(std::condition_variable_any) that can be used
|
||||
with any (e.g., user-supplied) lock type.
|
||||
hi(condition_variable_any)tt(std::condition_variable_any), which may be
|
||||
used with any (e.g., user supplied) lock type.
|
||||
|
||||
The condition variable classes offer members like tt(wait, wait_for,
|
||||
Condition variable classes offer members like tt(wait, wait_for,
|
||||
wait_until, notify_one) and tt(notify_all) that may concurrently be called.
|
||||
The notify members are always atomically executed. Execution of the
|
||||
The notifying members are always atomically executed. Execution of the
|
||||
tt(wait) members consists of three atomic parts:
|
||||
itemization(
|
||||
it() the mutex's release, and subsequent entry into the waiting state;
|
||||
it() unblocking the wait state;
|
||||
it() reacquisition of the lock.
|
||||
it() the mutex is released, and the thread is suspended until its
|
||||
notification;
|
||||
it() Once the notification has been received, the lock is reacquired
|
||||
it() The wait state ends (and processing continues beyond the tt(wait)
|
||||
call).
|
||||
)
|
||||
Therefore, returning from tt(wait)-members the thread calling wait owns
|
||||
the lock.
|
||||
|
||||
Programs using condition variables cannot make any assumption about the order
|
||||
in which any of the tt(notify_one, notify_all, wait, wait_for), and
|
||||
tt(wait_until) members are executed.
|
||||
So, returning from tt(wait)-members the previously waiting thread
|
||||
has reacquired the mutex's lock.
|
||||
|
||||
In addition to the condition variable classes the following free function and
|
||||
tt(enum) type are provided:
|
||||
tt(enum) type is provided:
|
||||
itemization(
|
||||
itht(notify_all_at_thread_exit)(void
|
||||
ithtq(notify_all_at_thread_exit)(void
|
||||
std::notify_all_at_thread_exit+OPENPARcondition_variable &cond,)
|
||||
linebreak()tt(unique_lock<mutex> lockObject+CLOSEPAR:)
|
||||
quote(once the current thread has ended, all other threads waiting on
|
||||
tt(cond) will be notified. It is good practice to exit the thread as
|
||||
linebreak()tt(unique_lock<mutex> lockObject+CLOSEPAR)
|
||||
(once the current thread has ended, all other threads waiting on
|
||||
tt(cond) are notified. It is good practice to exit the thread as
|
||||
soon as possible after calling linebreak()
|
||||
tt(notify_all_at_thread_exit).
|
||||
|
||||
Waiting threads must verify that the thread they were waiting for has
|
||||
indeed ended. This is usually implemented by obtaining the lock on
|
||||
tt(lockObject), after which these threads verify that the condition
|
||||
they were waiting for is true, and that the lock was not released and
|
||||
indeed ended. This is usually realized by first obtaining the lock on
|
||||
tt(lockObject), followed by verifying that the condition
|
||||
they were waiting for is true and that the lock was not
|
||||
reacquired before tt(notify_all_at_thread_exit) was called.)
|
||||
it() hi(cv_status)
|
||||
quote(
|
||||
ithtq(cv_status)(std::cv_status)
|
||||
(
|
||||
The tt(cv_status) enum is used by several member functions of the
|
||||
condition variable classes covered below:
|
||||
condition variable classes (cf. sections ref(CONDVAR1) and ref(CONDVAR2)):
|
||||
verb(
|
||||
namespace std
|
||||
{
|
||||
|
@ -106,5 +123,7 @@ namespace std
|
|||
};
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue