Класс rlock() модуля threading в python
Содержание:
- Структура SpinWait
- Определение текущего потока
- Тестируем наш код
- Condition Objects¶
- Перечисления
- Timer Threads¶
- Starting a New Thread
- Делегаты
- Python thread creating using class
- Hands-On on this Python Threading Tutorial
- Extend Thread class to create Threads
- High-level Module Interface¶
- Барьеры памяти
- Блокировки (замки)
- Объект Event
Структура SpinWait
Представьте, что у вас есть у вас есть два процессора, и на нулевом вы выполняете присваивание A = 1, потом устанавливаете барьер памяти, а затем снова выполняете присваивание B = 1.
На первом процессоре вы получаете А, равное 1, а затем вы ждете, пока B не присвоят значение 1. Казалось бы, такие операции должны быстро выполниться и в кэше L1, и даже в кэше L3. Здесь загвоздка в том, что выполнение нулевым процессором может прерваться из-за вытесняющего прерывания, и между операциями из строк 1 и 3 может пройти несколько миллисекунд. способен решать такие проблемы.
Под капотом работает :
- Сначала происходит вызов (не происходит переключение контекста потока). Этот шаг будет пропущен для одноядерных процессоров:
- Затем будет вызван (может быть прерван потоками такого же приоритета)
- Затем, — (может быть прерван потоками любого приоритета)
- Если операция все еще не завершена, то будет вызван (поток засыпает на 1мс)
Без цикл мог бы выполняться бесконечно, потому что если нулевой процессор запустится как фоновый поток, то первый процессор заблокирует этот фоновый поток, до тех пор пока поток на первом процессоре не будет прерван.
Определение текущего потока
Использование аргументов для идентификации потока является трудоемким процессом. Каждый экземпляр Thread имеет имя со значением, присваиваемым по умолчанию. Оно может быть изменено, когда создается поток.
Именование потоков полезно в серверных процессах с несколькими служебными потоками, обрабатывающими различные операции.
import threading import time def worker(): print threading.currentThread().getName(), 'Starting' time.sleep(2) print threading.currentThread().getName(), 'Exiting' def my_service(): print threading.currentThread().getName(), 'Starting' time.sleep(3) print threading.currentThread().getName(), 'Exiting' t = threading.Thread(name='my_service', target=my_service) w = threading.Thread(name='worker', target=worker) w2 = threading.Thread(target=worker) # используем имя по умолчанию w.start() w2.start() t.start()
Программа выводит имя текущего потока в каждой строке. «Thread-1» — это безымянный поток w2.
$ python -u threading_names.py worker Thread-1 Starting my_service Starting Starting Thread-1worker Exiting Exiting my_service Exiting
Большинство программ не используют print для отладки. Модуль logging поддерживает добавление имени потока в каждое сообщение журнала с помощью % (threadName)s. Включение имен потоков в журнал облегчает отслеживание этих сообщений.
import logging import threading import time logging.basicConfig(level=logging.DEBUG, format=' (%(threadName)-10s) %(message)s', ) def worker(): logging.debug('Starting') time.sleep(2) logging.debug('Exiting') def my_service(): logging.debug('Starting') time.sleep(3) logging.debug('Exiting') t = threading.Thread(name='my_service', target=my_service) w = threading.Thread(name='worker', target=worker) w2 = threading.Thread(target=worker) # use default name w.start() w2.start() t.start()
Модуль logging также является поточно-ориентированным, поэтому сообщения из разных потоков сохранятся в выводимых данных.
$ python threading_names_log.py (worker ) Starting (Thread-1 ) Starting (my_service) Starting (worker ) Exiting (Thread-1 ) Exiting (my_service) Exiting
Тестируем наш код
С целью проверить функциональность нашего парсера без запуска браузера и, таким образом, не повторять GET запросы к сайту Hacker News, вы можете загрузить HTML код страницы и сохранить его в папку , а затем парсить его локальную копию. Это поможет избежать блокировки вашего IP-адреса из-за слишком быстрого выполнения большого количества запросов, в ходе отладки и тестирования функций парсинга данных, этот подход также сэкономит ваше время, поскольку вам не нужно запускать браузер при каждом запуске скрипта.
Ниже представлен код файла тестов, который находится в папке test/test_scraper.py:
from pathlib import Path import pytest from scrapers import scraper BASE_DIR = Path(__file__).resolve(strict=True).parent @pytest.fixture(scope="module") def html_output(): with open(Path(BASE_DIR).joinpath("test.html"), encoding="utf-8") as f: html = f.read() yield scraper.parse_html(html) def test_output_is_not_none(html_output): assert html_output def test_output_is_a_list(html_output): assert isinstance(html_output, list) def test_output_is_a_list_of_dicts(html_output): assert all(isinstance(elem, dict) for elem in html_output)
Убедимся, что все работает как надо:
(env)$ python -m pytest test/test_scraper.py ================================ test session starts ================================= platform darwin -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: /Users/michael/repos/testdriven/async-web-scraping collected 3 items test/test_scraper.py ... ================================= 3 passed in 20.10s =================================
Код выполнялся всего 20 секунд. Попробуем имитировать работу функции , исключив отправку GET запроса.
test / test_scraper_mock.py:
from pathlib import Path import pytest from scrapers import scraper BASE_DIR = Path(__file__).resolve(strict=True).parent @pytest.fixture(scope="function") def html_output(monkeypatch): def mock_get_load_time(url): return "mocked!" monkeypatch.setattr(scraper, "get_load_time", mock_get_load_time) with open(Path(BASE_DIR).joinpath("test.html"), encoding="utf-8") as f: html = f.read() yield scraper.parse_html(html) def test_output_is_not_none(html_output): assert html_output def test_output_is_a_list(html_output): assert isinstance(html_output, list) def test_output_is_a_list_of_dicts(html_output): assert all(isinstance(elem, dict) for elem in html_output)
Снова тестируем:
(env)$ python -m pytest test/test_scraper_mock.py ================================ test session starts ================================= platform darwin -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: /Users/michael/repos/testdriven/async-web-scraping collected 3 items test/test_scraper.py ... ================================= 3 passed in 0.37s =================================
Condition Objects¶
A condition variable is always associated with some kind of lock; this can be
passed in or one will be created by default. Passing one in is useful when
several condition variables must share the same lock. The lock is part of
the condition object: you don’t have to track it separately.
A condition variable obeys the :
using the statement acquires the associated lock for the duration of
the enclosed block. The and
methods also call the corresponding methods of
the associated lock.
Other methods must be called with the associated lock held. The
method releases the lock, and then blocks until
another thread awakens it by calling or
. Once awakened,
re-acquires the lock and returns. It is also possible to specify a timeout.
The method wakes up one of the threads waiting for
the condition variable, if any are waiting. The
method wakes up all threads waiting for the condition variable.
Note: the and methods
don’t release the lock; this means that the thread or threads awakened will
not return from their call immediately, but only when
the thread that called or
finally relinquishes ownership of the lock.
The typical programming style using condition variables uses the lock to
synchronize access to some shared state; threads that are interested in a
particular change of state call repeatedly until they
see the desired state, while threads that modify the state call
or when they change
the state in such a way that it could possibly be a desired state for one
of the waiters. For example, the following code is a generic
producer-consumer situation with unlimited buffer capacity:
# Consume one item with cv while not an_item_is_available(): cv.wait() get_an_available_item() # Produce one item with cv make_an_item_available() cv.notify()
The loop checking for the application’s condition is necessary
because can return after an arbitrary long time,
and the condition which prompted the call may
no longer hold true. This is inherent to multi-threaded programming. The
method can be used to automate the condition
checking, and eases the computation of timeouts:
# Consume an item with cv cv.wait_for(an_item_is_available) get_an_available_item()
To choose between and ,
consider whether one state change can be interesting for only one or several
waiting threads. E.g. in a typical producer-consumer situation, adding one
item to the buffer only needs to wake up one consumer thread.
Перечисления
ApartmentState |
Задает апартаментное состояние потока Thread.Specifies the apartment state of a Thread. |
EventResetMode |
Указывает, сбрасывается ли EventWaitHandle автоматически или вручную после получения сигнала.Indicates whether an EventWaitHandle is reset automatically or manually after receiving a signal. |
LazyThreadSafetyMode |
Определяет, как экземпляр Lazy<T> синхронизирует доступ из нескольких потоков.Specifies how a Lazy<T> instance synchronizes access among multiple threads. |
LockRecursionPolicy |
Указывает, можно ли несколько раз войти в блокировку из одного и того же потока.Specifies whether a lock can be entered multiple times by the same thread. |
ThreadPriority |
Задает приоритет выполнения потока Thread.Specifies the scheduling priority of a Thread. |
ThreadState |
Задает состояния выполнения объекта Thread.Specifies the execution states of a Thread. |
Timer Threads¶
One example of a reason to subclass Thread is provided by
Timer, also included in . A Timer
starts its work after a delay, and can be canceled at any point within
that delay time period.
import threading import time import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s', ) def delayed(): logging.debug('worker running') return t1 = threading.Timer(3, delayed) t1.setName('t1') t2 = threading.Timer(3, delayed) t2.setName('t2') logging.debug('starting timers') t1.start() t2.start() logging.debug('waiting before canceling %s', t2.getName()) time.sleep(2) logging.debug('canceling %s', t2.getName()) t2.cancel() logging.debug('done')
Notice that the second timer is never run, and the first timer appears
to run after the rest of the main program is done. Since it is not a
daemon thread, it is joined implicitly when the main thread is done.
Starting a New Thread
To spawn another thread, you need to call following method available in thread module −
thread.start_new_thread ( function, args )
This method call enables a fast and efficient way to create new threads in both Linux and Windows.
The method call returns immediately and the child thread starts and calls function with the passed list of args. When function returns, the thread terminates.
Here, args is a tuple of arguments; use an empty tuple to call function without passing any arguments. kwargs is an optional dictionary of keyword arguments.
Example
#!/usr/bin/python import thread import time # Define a function for the thread def print_time( threadName, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print "%s: %s" % ( threadName, time.ctime(time.time()) ) # Create two threads as follows try: thread.start_new_thread( print_time, ("Thread-1", 2, ) ) thread.start_new_thread( print_time, ("Thread-2", 4, ) ) except: print "Error: unable to start thread" while 1: pass
When the above code is executed, it produces the following result −
Thread-1: Thu Jan 22 15:42:17 2009 Thread-1: Thu Jan 22 15:42:19 2009 Thread-2: Thu Jan 22 15:42:19 2009 Thread-1: Thu Jan 22 15:42:21 2009 Thread-2: Thu Jan 22 15:42:23 2009 Thread-1: Thu Jan 22 15:42:23 2009 Thread-1: Thu Jan 22 15:42:25 2009 Thread-2: Thu Jan 22 15:42:27 2009 Thread-2: Thu Jan 22 15:42:31 2009 Thread-2: Thu Jan 22 15:42:35 2009
Although it is very effective for low-level threading, but the thread module is very limited compared to the newer threading module.
Делегаты
ContextCallback |
Представляет метод, вызываемый в новом контексте.Represents a method to be called within a new context. |
IOCompletionCallback |
Получает код ошибки, количество байтов и тип перекрывающегося значения при завершении операции ввода-вывода в пуле потоков.Receives the error code, number of bytes, and overlapped value type when an I/O operation completes on the thread pool. |
ParameterizedThreadStart |
Представляет метод, который выполняется в отношении Thread.Represents the method that executes on a Thread. |
SendOrPostCallback |
Задает метод, вызываемый при отправке сообщения в контекст синхронизации.Represents a method to be called when a message is to be dispatched to a synchronization context. |
ThreadExceptionEventHandler |
Представляет метод, обрабатывающий событие ThreadExceptionApplication.Represents the method that will handle the ThreadException event of an Application. |
ThreadStart |
Представляет метод, который выполняется в отношении Thread.Represents the method that executes on a Thread. |
TimerCallback |
Представляет метод, обрабатывающий вызовы от события Timer.Represents the method that handles calls from a Timer. |
WaitCallback |
Представляет метод обратного вызова, выполняющегося потоком из пула потоков.Represents a callback method to be executed by a thread pool thread. |
WaitOrTimerCallback |
Представляет метод, который вызывается при получении объектом WaitHandle сигнала или истечении времени ожидания.Represents a method to be called when a WaitHandle is signaled or times out. |
Python thread creating using class
Here, we can see how to create thread using class in python.
Syntax to create thread class:
To create a thread using class in python there are some class methods:
- run() – This method calls the target function that is passed to the object constructor.
- start() – Thread activity is started by calling the start()method, when we call start() It internally invokes the run method and executes the target object.
- join() – This method blocks calling thread until the thread whose join() is called terminates normally or through handle exception.
- getName() – This method returns a name for the thread.
- setName(name) – This method is used to set the thread name, the name in the form of a string, and is used for identification.
- isAlive() – This method returns that the thread is alive or not. The thread is alive at the time when the start() is invoked and lasts until the run() terminates.
- setDaemon(Daemonic) – This method is used to set the daemon flag to Boolean value daemonic. this should be called before the start().
- isDaemon() – This method returns the value of the thread’s daemon flag.
- In this example, I have imported a module called thread from threading and defined a function as a threaded function and an argument is passed.
- The value of __name__ attribute is set to “__main__”. When the module is run as a program. __name__ is the inbuilt variable that determines the name for a current module.
- If the module is running directly from the command line then “__name__” is set to “__main__”.
Example:
You can see in the below screenshot that python guides printed three times as mentioned in the range().
Python thread creating using class
Hands-On on this Python Threading Tutorial
Python threading library
Python has several ways to implement multithreading. The modern way to do it is using the library, which contains the class. As we will see, working with this library is extremely intuitive. We also want to use the library to experiment threads, but this is not strictly needed in production. Thus, at the very beginning of your script, add the following lines.
import threading import time
Now we can start working with threads!
Define a test function to run in threads
First thing, we need to define a function we want to run in a thread. Our function will do almost nothing, but we will use the function to emulate a huge workload. The function makes your program (or thread) stop and wait for a given amount of seconds.
When you have a function that you want to run in a thread, it is best practice to use a thread identifier as the first parameter. This way, you know what thread you are running from inside the function as well. However, you also need to pass this value when creating the thread, but we will get to that. For now, just create this function.
def do_something(id, message=''): time.sleep(4) print("Thread #" + str(id) + " finished with message: " + message)
This function simply waits 4 seconds sand then prints the given message with the thread ID.
Running it synchronously “the standard way”
At this point, we can create a global list of messages that we want to print. Here is an example.
messages =
If we were to run the function with all the messages synchronously we would roughly need 16 seconds (4 seconds per message). In fact, we can do a simple test using the function. This returns the epoch time in seconds, and we can use it twice to see how much time elapsed from the beginning to the end of the script.
start = time.time() for i, msg in enumerate(messages): do_something(i, msg) print("It took " + str(time.time()-start) + " seconds")
And here is the output. The time elapsed between printing lines, of course, was about 4 seconds.
Running it with threads
Now we can dive in the real python threading tutorial. We can rewrite this part of the script to work with threads, and distribute the load among them. Here we need to work with three different functions.
Creating the thread
To create a thread, you need to instantiate a object. The constructor wants to know a target function: the function that you want to run within the thread. It also want to know a list of parameters you want to pass to the function, if you need it. You provide the function name as , and the parameters as a tuple for the parameter. Below, a sample code to create a thread.
thread = threading.Thrad(target=function_name, args=(arg1, arg2, arg3))
From now on, you can perform operation on this thread object you just created.
Starting and joining the thread
Once you have a thread object, you can decide to start it with the function, and to join it with the function, as simple as that. The code is pretty straight forward, as you can see below.
thread.start() thread.join()
In order to call , you need to call first. However, you don’t need to call the two one after the other. In fact, you might want to perform some code after starting the thread, and before joining it. Even more, you may not join a thread all.
The whole script
Combining the commands above, we can create a way more efficient snippet that leverages threads. Here it is.
start = time.time() threads = [] for i, msg in enumerate(messages): threads.append(threading.Thread(target=do_something, args=(i, msg,))) threads.start() for thread in threads: thread.join() print("It took " + str(time.time()-start) + " seconds")
As you can see, we first create and start all the threads. Then, with another loop, we join all of them. We didn’t join each thread just after starting on purpose. If we did, the script would have waited for the first thread to finish before starting the second. Of course, we don’t want that. If we run this script you won’t see any output for about 4 seconds, then all four lines of the output will appear together. It will run in a little more than 4 seconds, take a look.
Extend Thread class to create Threads
Suppose we have a class FileLoaderThread, which simulates the functionality of a file loader and it’s run() method sleeps for around 5 seconds. We can create this class by extending Thread class provided by the threading module i.e.
# A class that extends the Thread class class FileLoaderThread(Thread):
FileLoaderThread
Thread class has a run() method that is invoked whenever we start the thread by calling start() function. Also, run() function in Thread class calls the callable entity (e.g. function) passed in target argument to execute that function in thread. But in our derived class we can override the run() function to our custom implementation like this,
# A class that extends the Thread class class FileLoaderThread(Thread): def __init__(self, fileName, encryptionType): # Call the Thread class's init function Thread.__init__(self) self.fileName = fileName self.encryptionType = encryptionType # Override the run() function of Thread class def run(self): print('Started loading contents from file : ', self.fileName) print('Encryption Type : ', self.encryptionType) for i in range(5): print('Loading ... ') time.sleep(1) print('Finished loading contents from file : ', self.fileName)
FileLoaderThread’s
Now as our class FileLoaderThread extends the Thread class has all it’s power, so we can create a thread by creating the object of this class i.e.
# Create an object of Thread th = FileLoaderThread('users.csv','ABC')
# start the thread th.start() # print some logs in main thread for i in range(5): print('Hi from Main Function') time.sleep(1) # wait for thread to finish th.join()
Started loading contents from file : users.csv Encryption Type : ABC Hi from Main Function Loading ... Loading ... Hi from Main Function Loading ... Hi from Main Function Loading ... Hi from Main Function Hi from Main Function Loading ... Finished loading contents from file : users.csv
FileLoaderThread
Complete example is as follows,
from threading import Thread import time # A class that extends the Thread class class FileLoaderThread(Thread): def __init__(self, fileName, encryptionType): # Call the Thread class's init function Thread.__init__(self) self.fileName = fileName self.encryptionType = encryptionType # Override the run(0 function of Thread class def run(self): print('Started loading contents from file : ', self.fileName) print('Encryption Type : ', self.encryptionType) for i in range(5): print('Loading ... ') time.sleep(1) print('Finished loading contents from file : ', self.fileName) def main(): # Create an object of Thread th = FileLoaderThread('users.csv','ABC') # start the thread th.start() # print some logs in main thread for i in range(5): print('Hi from Main Function') time.sleep(1) # wait for thread to finish th.join() if __name__ == '__main__': main()
Output:
Started loading contents from file : users.csv Encryption Type : ABC Hi from Main Function Loading ... Loading ... Hi from Main Function Loading ... Hi from Main Function Loading ... Hi from Main Function Hi from Main Function Loading ... Finished loading contents from file : users.csv
High-level Module Interface¶
-
An int containing the default buffer size used by the module’s buffered I/O
classes. uses the file’s blksize (as obtained by
) if possible.
- (file, mode=’r’, buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
-
This is an alias for the builtin function.
This function raises an with
arguments , and . The and
arguments may have been modified or inferred from the original call.
- (path)
-
Opens the provided file with mode . This function should be used
when the intent is to treat the contents as executable code.should be a and an absolute path.
The behavior of this function may be overridden by an earlier call to the
. However, assuming that is a
and an absolute path, should always behave
the same as . Overriding the behavior is intended for
additional validation or preprocessing of the file.New in version 3.8.
- exception
-
This is a compatibility alias for the builtin
exception.
- exception
-
An exception inheriting and that is raised
when an unsupported operation is called on a stream.
Барьеры памяти
Помимо оптимизации процессора, существует ещё оптимизация компилятора и среды выполнения. В контексте данной статьи под компилятором и средой я буду понимать JIT и CLR. Среда может кэшировать значения в регистрах процессора, переупорядочить операции и объединять операции записи (coalesce writes).
Например, для цикла for CLR может решить заранее рассчитать значение и вынести локальную переменную за пределы цикла. Во избежание этого, конечно же, можно использовать ключевое слово volatile, но бывают и более сложные случаи.
Иногда бывает нужно, чтобы весь код был точно выполнен до какой-либо определенной инструкции, и для этого используются барьеры памяти. Барьер памяти — это инструкция, которая реализуется процессором. В .NET она доступна с помощью вызова . И что же он делает? Вызов этого метода гарантирует, что все операции перед этим методом точно завершились.
Давайте снова вернемся к программе с двумя переменными. Предположим, что эти процессоры выполняют команды «сохранить» A и «загрузить» B. Если между этими двумя командами находится барьер памяти, то процессор сначала отправит запрос «сохранить» B, подождет, пока она не выполнится. Барьеры памяти дают возможность поставить что-то вроде чекпоинта или коммита, как в базе данных.
Есть еще одна причина использовать барьеры памяти. Если вы пишете код на 32-битной машине с использованием long, DateTime или struct, то атомарность выполнения операций может нарушаться. Это значит, что даже когда в коде записана одна инструкция, на самом деле могут произойти две операции вместо одной, причем они могут выполняться в разное время.
Блокировки (замки)
Блокировки – это фундаментальный механизм синхронизации, который предоставлен модулем threading Python. Замок может удерживаться одним потоком в любое время, или без потока вообще. Если поток попытается удержать один замок, который уже удерживается другим потоком, выполнение первого потока будет остановлена, пока не будет снята блокировка. Замки обычно используются для синхронизации доступа к общим ресурсам. Для каждого такого источника создается объект Lock. Когда вам нужно получить доступ к ресурсу, вызовите acquire для того, чтобы поставить блок, после чего вызовете release:
Python
lock = Lock()
lock.acquire() # Выполнит блокировку данного участка кода
… доступ к общим ресурсам
lock.release()
1 2 3 4 5 |
lock=Lock() lock.acquire()# Выполнит блокировку данного участка кода …доступкобщимресурсам lock.release() |
Для корректной работы важно снять блок, даже если что-то идет не так при доступе к ресурсу. Вы можете использовать try-finally для этой цели:. Python
lock.acquire()
try:
..
доступ к общим ресурсам
finally:
lock.release() # освобождаем блокировку независимо от результата
Python
lock.acquire()
try:
… доступ к общим ресурсам
finally:
lock.release() # освобождаем блокировку независимо от результата
1 2 3 4 5 |
lock.acquire() try …доступкобщимресурсам finally lock.release()# освобождаем блокировку независимо от результата |
В Python 2.5 и в поздних версиях, вы можете также использовать оператор with. В работе с блоком, данный оператор автоматически получает доступ к замку перед входом в блок, и отпускает его после выхода из блока:
Python
from __future__ import with_statement # 2.5 версия!!!
with lock:
… доступ к общим ресурсам
1 2 3 4 |
from__future__importwith_statement# 2.5 версия!!! withlock …доступкобщимресурсам |
Метод acquire принимает опциональный флаг ожидания, который может быть использоваться для того, чтобы обойти блокировку, если замок удерживается той или иной частью кода. Если вы укажите False, то метод не будет блокироваться, но вернет False, если замок уже висит:
Python
if not lock.acquire(False):
… не удалось заблокировать ресурс
else:
try:
… доступ к ресурсам
finally:
lock.release()
1 2 3 4 5 6 7 |
ifnotlock.acquire(False) …неудалосьзаблокироватьресурс else try …доступкресурсам finally lock.release() |
Вы можете использовать метод locked, чтобы проверить, работает ли замок
Обратите внимание на то, что вы не можете использовать этот метод, чтобы определить, блокируется вызов к acquire или нет. Какой-либо другой поток может получить доступ к замку между вызовом метода и следующим оператором
Python
if not lock.locked():
# Другой поток может выполниться прежде чем
# мы выполним следующею строку
lock.acquire() # может заблокировать все равно
1 2 3 4 |
ifnotlock.locked() # Другой поток может выполниться прежде чем # мы выполним следующею строку lock.acquire()# может заблокировать все равно |
Объект Event
Объект event – это простой объект синхронизации. Он представляет собой внутренний флаг, так что все потоки могут ожидать, пока флаг будет установлен, задавать, или убирать его.
Python
import threading
event = threading.Event()
# поток-клиент ожидает установки флага
event.wait()
# поток-сервер может установить флаг или снять его.
event.set()
event.clear()
1 2 3 4 5 6 7 8 9 10 |
importthreading event=threading.Event() event.wait() event.set() event.clear() |
Если флаг был задан, метод wait не будет делать ничего. Если флаг был убран, wait будет блокировать, пока его снова не установят. Любое количество потоков может дожидаться одного и того же объекта event.