0.00
0 читателей, 27 топиков

Функции синхронизации

Это объекты ядра системы, которые предоставляют потоком взаимоисключающий доступ к ресурсам. Так как Mutex является системным объектом, он имеет имя и Handle, то доступ к нему есть у всех потоков созданных в системе, не зависимо от того, к какому процессу они принадлежат.

WinApi: Функции синхронизации

Создается Mutex при помощи функции – CreateMutex.
Функция принимает три параметра – указатель на объект структуры securityattributes, устанавливает право потока на работу с Mutex, указатель на строку с именем Mutex. Если Mutexсоздан успешно, функция возвращает Handle, если нет, то 0.

HANDLE CreateMutex
(
	LPSECURITY_ATTRIBUTES lpMutexAttributes,	// атрибутбезопастности
	BOOL bInitialOwner,			// флагначальноговладельца
	LPCTSTR lpName				// имяобъекта
);


OpenMutex — возвращаетHandleсуществующего Mutex.
ReleaseMutex – освобождает Mutexот текущего потока и делает его доступным другим потокам. Один параметр – Handle Mutex.

Wait функции

WinApi: Функции синхронизации

WaitForSingleObject -принимает два параметра – дескриптор объекта синхронизации, время ожидания в миллисекундах, которое поток готов ждать, до освобождения объекта. Если второй параметр не определен, поток ждет бесконечно.
WaitForMultipleObject – позволяет следить сразу за несколькими объектами синхронизации.Первый параметр функции – кол-во, за которым она будет следить.

Семафоры

Семафор в отличии от Mutex, имеет счетчик ресурсов и максимальное значение этого счетчика. При значении счетчика 0, семафор находиться вне сигнальном состоянии. Создается семафор аналогично всем другим объектам – CreateSemaphore.
OpenSemaphore – возвращает Handle существующего объекта семафора.
ReleaseSemaphore – первый параметр Handleсемафора, во втором параметре передается число, на которое необходимо увеличить счетчик.

BOOL WINAPI ReleaseSemaphore(
  _In_       HANDLE hSemaphore,
  _In_       LONG lReleaseCount,
  _Out_opt_  LPLONGlpPreviousCount
);


События
CreateEvent – создания события.
HANDLE CreateEvent
(
	LPSECURITY_ATTRIBUTES lpEventAttributes,	// атрибутзащиты
	BOOL bManualReset,				// типсброса TRUE - ручной
	BOOL bInitialState,			// начальное состояние TRUE - сигнальное
	LPCTSTR lpName				// имя обьекта
);


OpenEvent
HANDLE WINAPI OpenEvent(
  _In_  DWORDdwDesiredAccess,
  _In_  BOOLbInheritHandle,
  _In_  LPCTSTRlpName
);


SeteEvent — устанавливает событие в сигнальное состояние.
ResetEvent –сброс вне сигнальное состояние, те события которые автоматические.
PulseEvent — вызов этой функции эквивалентен этому событию.

Таймер синхронизации

WinApi: Функции синхронизации

Это объект ядра, который самостоятельно переходит в свободное состояние, либо в определенное время, либо через циклический промежуток. Для создания такого объекта необходимо вызвать функцию – CreateWaitableTimer.

HANDLE WINAPI CreateWaitableTimer(
  _In_opt_  LPSECURITY_ATTRIBUTES lpTimerAttributes,//указательнаобъектструктуры.
  _In_      BOOL bManualReset, // Какойтаймерсоздается
  _In_opt_  LPCTSTRlpTimerName//Имя
);

Средства синхронизации предоставленные WinApi

1. Критические секции
2. Мьютексы
3. Семофоры
4. Wait функции

Функции взаимоблокировки
Если требуется всего лишь модифицировать, какое-то целочисленное значение, в этом случае может хватить механизма, который предоставляют функции взаимоблокировки. Они достаточно просты в использовании, дают высокое быстродействие по сравнению с другими методами и не приводят к блокировке потоков.
Interlocked–функции блокирования. Они все начинаются со слова Interlocked.
Задерживают все выполнения потоков, пока выполняется Interlocked.

Средства синхронизации предоставленные WinApi

Если эту функцию запускать в разных потоках выполнения, то нельзя заранее предсказать выполнения работы.

Int N=0;
void f(){
int M=N;
M++;
N=M;
}

InterlockedIncrement

#include<iostream>
usingnamespacestd;
long n = 0;
void f();
void main(){
	setlocale(LC_ALL,"rus");
	for (inti = 0; i< 10;)
	f();
	system("pause");
}
void f(){
	int m = _InterlockedIncrement(&n);
	n = m;
	cout<< n;
}


Критические секции
Это специальный участок кода, который требует монопольного доступа к разделяемым ресурсам. Только один поток может иметь доступ к каким то общим данным. Минусы применения критических секций –применять критические секции можно только в рамках одного процесса.

Средства синхронизации предоставленные WinApi

С помощью этого метода синхронизировать потоки принадлежащие разным размерам невозможно. Что бы использовать критическую секцию, необходимо объявить в глобальной области переменную специального типа – CRITICAL_SECTION. Это структура, объект этой структуры должен быть обязательно проинициализирован при помощи функции InitialCriticalSection. По окончанию использования, его нужно освободить, для этого существует DeleteCriticalSection.

EnterCriticalSection – начало секции.
LeaveCriticalSection – конец секции.

Приоритет потоков

Существует 32 приоритета выполнения потока
SetThreadPriority (хендл потока для установки приоритета, метка приоритета);
СОЗДАНИЕ НОВОГО ПОТОКА ПРОГРАММНЫМ ОБРАЗОМ

CreatThread () -поток в настоящем процессе
(указатель на структура SECURITYATREBUTES, размер стека для нового потока (округление до меньшей степени)(если 0, размер стека 1 МБ), указатель на функцию для запуска в отдельном потоке выполнения, указатель на объект, набор битовых флагов, индетефикатор запущенного потока);
Возвращает не 0 при удачном запуске оно и есть дескриптор Функция которая запускается в отдельном потоке соответствует прототипу

DWORD WinApi TreadProc (LPVOID lpParameter);
Получение завершения кода выхода с потока
GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);

ОСТАНОВКА ПОТОКА ВО ВРЕМЯ ВЫПОЛНЕНИЯ
SuspendThread (хендл потока);

ПОБУДИТЬ ПОТОК
ResumThread(хендл);

ЗАВЕРШЕНИЕ ПОТОКА

ExitThread(код завершения потока);
— для внутренего завершения потока. Завершает сразу процессы

TerminateTread(хендл потока для завершения, код выхода);
-для внешнего выхода, завершает после окончания действия
ЛОКАЛЬНАЯ ПАМЯТЬ ПОТОКА TLS
TlsAlloc возращает индекс или -1 при ошибке

Освобождение индекса

TlsFree( индекс)
	

РАБОТА С ЗНАЧЕНИЯМИ TLS

TlsGetValue() - получить
TlsSetValue() - установить

Снимок системы

Получить снимок системы можно вызвав специальную функцию – CreateToolHelp32Snapshot.
Эта функция принимает два параметра – набор битовых флагов (эти флаги определяют какие параметры будут записаны в снимок); процесс (возвращает Handle полученного снимка).
Если необходимо получить список процессов, используют две функции – Process32First, Process32Next, эти функции принимают одни и те же параметры.

WinApi: Снимок системы

Первый параметр – Handle снимка системы, Второй – адрес структуры PROCESSENTRY32.
OpenProcess– получает Handleпроцесса по его id.
Функция принимает три параметра – набор битовых флагов, которые определяют разрешение для манипулирования полученным дескриптором, разрешения предоставляет система; определяет возможность наследования дескриптора дочерними процессами; Id процесса, дескриптор которого мы хотим получить.

ExitProcess – если необходимо завершить работу приложения не из первичного потока, а в каком-то из дочерних. Она принимает один параметр – код завершения процесса. Эту функцию можно вызывать в любом потоке, того процесса, который надо завершить. Функция относится к текущему процессу.

WinApi: Снимок системы

TerminateProcess – используется когда надо завершить работу любого процесса, а не только текущего. Принимает два параметра – Handleпроцесса, который надо завершить; Код завершения.

EnableWindow(hButton, FALSE);
			HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
			PROCESSENTRY32 pe32;
			memset(&pe32, 0, sizeof(PROCESSENTRY32));
			pe32.dwSize = sizeof(PROCESSENTRY32);
			if (Process32First(hSnapShot, &pe32)){
				wsprintf(BOX, TEXT("   %4u            %5u                 %2u             %s\n"), pe32.th32ProcessID, pe32.cntThreads, pe32.pcPriClassBase, pe32.szExeFile);
				SendMessage(hList, LB_ADDSTRING, 0, LPARAM(BOX));

				while (Process32Next(hSnapShot, &pe32))
				{
					wsprintf(BOX, TEXT("   %4u            %5u                 %2u             %s\n"), pe32.th32ProcessID, pe32.cntThreads, pe32.pcPriClassBase, pe32.szExeFile);
					SendMessage(hList, LB_ADDSTRING, 0, LPARAM(BOX));

				}
			}
  • avatar
  • 0
  • 0

Процессы

CloseHandle – принимает дескриптор и закрывает его.В некоторых случая, для закрытия дескриптора предусматриваются специальные функции, например для закрытия файла – CloseFile.
Процесс в Windows – это виртуальное адресное пространство, которое изолировано от других процессов, то есть другой процесс не может вызвать функцию или обратиться к переменной находящейся в адресном пространстве другого процесса.
Каждый процесс обладает хотя бы одним потоком выполнения. Создавая процесс мы порождаем так называемый первичный потоков процесса. У одного процесса может быть много потоков. Система для каждого процесса имеет счетчик запущенных в нем потоков. Если прекратить выполнение всех потоков процесса, система автоматически уничтожает процесс.

Для программного создания нового процесса в Windows, вызывается функция – CreateProcess, которая создает новый процесс, с единственным потоком. При вызове этой функции необходимо указать имя исполняемого файла.
Принимает 10 параметров:

BOOL WINAPI CreateProcess(
  _In_opt_     LPCTSTR lpApplicationName, - Строка параметра.
  _Inout_opt_  LPTSTR lpCommandLine, - Командная строка.
  _In_opt_     LPSECURITY_ATTRIBUTES lpProcessAttributes, - Дескриптор безопасности. Структура с атрибутами защиты процесса.
  _In_opt_     LPSECURITY_ATTRIBUTES lpThreadAttributes, - Дескриптор безопасности. Структура с атрибутами защиты первичного потока.
_In_         BOOLbInheritHandles, - Определяет признак наследования дескриптора. 
  _In_         DWORDdwCreationFlags, - Набор битовых флагов, которые используются на создании битового процесса.
  _In_opt_     LPVOIDlpEnvironment,-  указатель на новый блок среды
  _In_opt_     LPCTSTRlpCurrentDirectory, - Текущий каталог нового процесса.
  _In_         LPSTARTUPINFOlpStartupInfo, - Указатель на структуру LPSTARTUPINFO. Она определяет оконный терминал, рабочий стол, стандартный дескриптор и вид рабочего окна для нового процесса.
  _Out_        LPPROCESS_INFORMATION lpProcessInformation–Указатель на структуру LPPROCESS_INFORMATION. Записывает информацию.
);

Необходимо строго следить за своевременным закрытием дескриптора потомка и процесса, иначе может создаться утечка ресурсов. Такая ошибка является распространённой.
  • avatar
  • 0
  • 0

Создание и закрытие процесса

Подключаем диалог и в нем создаем процесс по нажатию кнопки с идентификатором IDOK.

#include <windows.h>
#include "resource.h"

BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

HWND hStatic1, hStatic2;
TCHAR szCoordinates[20];
HINSTANCE hInst;
const int LEFT = 15, TOP = 110, WIDTH = 380, HEIGHT = 50;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow)
{
	hInst = hInstance;
	return DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
}

BOOL CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){
	switch (message){
	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK){
			STARTUPINFO cif;
			ZeroMemory(&cif, sizeof(STARTUPINFO));
			PROCESS_INFORMATION pi;
			if (CreateProcess(L"c:\\windows\\notepad.exe", NULL,
				NULL, NULL, FALSE, NULL, NULL, NULL, &cif, &pi) == TRUE){

				Sleep(1000);				// подождать
				TerminateProcess(pi.hProcess, NO_ERROR);	// убрать процесс
			}

			break;
		}
	case WM_CLOSE:
		EndDialog(hWnd, 0); // закрываем модальный диалог
		return TRUE;
		// WM_INITDIALOG - данное сообщение приходит после создания диалогового окна, но перед его отображением на экран

	}
	return FALSE;
}

Динамическое создание меню

CreateMenu – создает главное меню приложения. Меню изначально будет пустым, заполняют его с помощью функций – AppendMenu, InsertMenu.
CreatePopupMenu – выпадающие меню. Меню изначально будет пустым, заполняют его с помощью функций – AppendMenu (используется для добавления пунктов в конец меню, принимает 4 параметра – Handle меню к которому добавляют; флаги внешнего вида и правило поведения пункта, который добавляется; индефикатор нового пункта или дескриптор выпадающего меню; содержимое нового пункта меню. ), InsertMenu.

MF_POPUP — всплывающие меню.
MF_CHEKED – помещает отметку рядом с меню.
MF_DEFAULT – пункт меню установлен как применяемый по умолчанию. Обычно такой пункт выделяется жирным шрифтом.
InsertMenu – вставляет новый пункт в меню в нужной позиции. Принимает пять параметров – дескриптор меню, в который идет вставка; идентификатор или позиция пункта, перед которым идет вставка; интерпретирует второй параметр, а так же указывает внешний вид и правила поведения меню; идентификатор нового пункта или дескриптор выпадающего меню; содержимое нового пункта, зависит от третьего параметра.
ModifyMenu – изменение уже существующего пункта меню. Принимает пять параметров – дескриптор; идентификатор или позиции того пункта, который будем менять; интерпретирует второй параметр, а так же указывает внешний вид и правила поведения меню; идентификатор нового пункта или дескриптор выпадающего меню; содержимое нового пункта, зависит от третьего параметра.
DleteMenu – удаляет указанный пункт из меню. Принимает три параметра – дескриптор меню, чего удаляем; идентификатор или позиция пункта, который удаляется; флаги, которые интерпретирует второй параметр.
После вызова функция необходимо обязательно перерисовать меню с помощью функции – DrawMenuBar.
DestroyMenu – уничтожает меню.
GetMenuString – берет строку текста указанного меню. Принимает пять параметров – дескриптор меню; идентификатор или позиция пункта; указатель на строковый буфер, в который заполнится этот текст; длинна буфера, интерпретация второго параметра.

Акселераторы

УКАЗАНИЯ ГОРЯЧИХ КЛАВИШ
Чтобы добавить в приложение обработку акселераторов выполняют след действие.
1)Необходимо модифицировать пункт обработки меню. добавив к имени каждого дублируeмого пункта инфу про быстрые клавиши
2)Определить таблицу акселераторов в файле описания ресурсов
3)Обеcпечить загрузку таблицы акселераторов в память приложения
4)Изменить цикл обработки сообщений в функции winmain

так как акселераторы ставятся в соответствии командам меню, то их нужно делать с тем же индификатором, что и элементы меню

ДЛЯ ЗАГРУЗКИ АКСЕЛЕРАТОРОВ

LoadAccelerators — хендл приложения, указатель на строку с именем таблицы акселераторов.

Для обработки акселераторов приложение должно перехватывать сообщение клавиатуры, анализировать код сообщения. В случае совпадения кодов в таблице акселераторов направлять сообшение в процедуру главного окна
Для этого:
TranslateAccelerator — хендл окна получателя сообщения, дескриптор таблицы акселераторов, указатель на структуру MSG.

Функция преобразует сообщение от клавиатуры в сообщение WM_COMMAND или в WM_SYSCOMMAND. Сообщение содержит индефикатор акселератора, в младшем wParam
оно направляются в оконную процедуру меняя очередь сообщений. Возврат из функции TranslateAccelerator только после, как оконная процедура обрабатывает
посланное сообщение. Если она вернула не 0 значение, значит обработка сообщения — удачна. Так приложение не обрабатывает повторно комбо клавищ.

#include "AcceleratorsDlg.h"
CAcceleratorsDlg* CAcceleratorsDlg::ptr = NULL;

CAcceleratorsDlg::CAcceleratorsDlg(void){
	ptr = this;
}
void CAcceleratorsDlg::Cls_OnClose(HWND hwnd){
	DestroyWindow(hwnd);
	PostQuitMessage(0);
}
void CAcceleratorsDlg::Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify){
	TCHAR str1[300], str2[50];
	HMENU hMenu = GetMenu(hwnd);
	GetMenuString(hMenu, id, str2, 50, MF_BYCOMMAND);
	if(codeNotify == 1)
		_tcscpy(str1, TEXT("Пункт меню выбран с помощью акселератора\n"));
	else if(codeNotify == 0)
		_tcscpy(str1, TEXT("Пункт меню выбран при непосредственном обращении к меню\n"));
	_tcscat(str1, str2);
	MessageBox(hwnd, str1, TEXT("Меню и акселераторы"), MB_OK | MB_ICONINFORMATION);
	switch (id){
	case ID_EXIT:
		exit(0);
		break;
	default:
		break;
	}
}

BOOL CALLBACK CAcceleratorsDlg::DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){
	switch(message){
		HANDLE_MSG(hwnd, WM_CLOSE, ptr->Cls_OnClose);
		HANDLE_MSG(hwnd, WM_COMMAND, ptr->Cls_OnCommand);
	}
	return FALSE;
}
  • avatar
  • 0
  • 0

Создание элементов общего пользования и прочее

Создание кнопки
HWND hButton;
hButton=CreateWindowA("button", "Press me", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,10, 10, 80, 30, hwnd, (HMENU)10000, hInst, NULL);

Рисунок для кнопки

HBITMAP hBitmap;
hBitmap = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP2));
hButton = GetDlgItem(hDialog, IDC_BUTTON2);
SendMessage(hButton, BM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hBitmap);


Создание статика
HWND hStatic;
hStatic = CreateWindowEx(0, TEXT("STATIC"), 0, WS_CHILD | WS_VISIBLE | WS_BORDER | SS_CENTER | WS_EX_CLIENTEDGE, 100, 100, 100, 100, hwnd, 0, hInst, 0);

Создание Edit Control
HWND hEdit;
hEdit = CreateWindowA("EDIT", "Hello", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE, 150, 150, 100, 100, hwnd, NULL, hInst, NULL);

Установка иконки для диалога
1. Add — Resource — Icon.

HINSTANCE hInst = GetModuleHandle(NULL);
HICON hIcon;
hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1));
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);

Создание файла

HANDLE FileHandle;
FileHandle = CreateFile(L"file1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);
CloseHandle(FileHandle);

Создание дополнительного диалога
1. Resource View — Add — Resource — Dialog.
DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG2), hWnd, DlgProc);

Сохранение текста из EditControl

hEdit1 = GetDlgItem(hwnd, IDC_EDIT1);
int length = SendMessage(hEdit1, WM_GETTEXTLENGTH, 0, 0);
// определим длину текста (названия клуба), введённого в текстовое поле
int length = SendMessage(hEdit1, WM_GETTEXTLENGTH, 0, 0);
// выделим память необходимого размера
TCHAR *pBuffer = new TCHAR[length + 1];
// в выделенную память скопируем текст, введённый в тестовое поле
GetWindowText(hEdit1, pBuffer, length + 1);


Изменение цвета диалогового окна
1. Создаем кисть в WM_INITDIALOG.
case WM_INITDIALOG:{
		hBrush = CreateSolidBrush(RGB(255, 255, 255));
return TRUE;
}

2. Обрабатываем сообщение WM_CTLCOLORDLG.
case WM_CTLCOLORDLG:{
		return (BOOL)hBrush;
	}

Перемещение одного диалогового окна по координатам другого

Данный код поместить в WM_INITDIALOG окна, которое перемещаем.
Параметр hDialog объявить глобально и положить в него Handle главного окна.

RECT r = { 0 };
GetWindowRect(hDialog, &r); // получаем координаты главного диалогового окна
SetWindowPos(hWnd, NULL, r.right, r.top, NULL, NULL, SWP_NOSIZE | SWP_NOZORDER); // смещаем 2-ое  окно

ListBox

Элементами списка могут быть: строчки, картинки, комбинации строчек с картинками.
Отображение выбранного элемента в списке дается с инвертированием цвета. Наиболее употребляемое свойство этого элемента список:
Selection – если имеет свойство single, это единичный выбор элементов.
Sort – автоматическая сортировка элементов.
HorizontalScroll – горизонтальная прокрутка.
Multicolumn – многоколоночный. Возможность сделать список, в котором элементы будут располагаться в нескольких колонках.

Отправление сообщений элементу ListBox
LB_ADDSTRING(wParam 0 lParamszString) – сообщение добавить в список строку. Если свойство sort выключено, строка добавляется в конец списка. Если включена, после добавления строки, происходит сортировка.
LB_FINDSTRING(wParam iStart, lParam szString) –ищет строку начинающуюся с szString. Поиск начинается с элемента с индексом iStart +1.Функция sendmessage, возвращает индекс найденной строки или сообщение LB_ERR.
LB_GETCURSEL(wParam 0, lParam 0) – требует индекс текущего выбранного элемента или LB_ERR, если такого элемента нету. Используется для списка с единичным выбором.
LB_GETSELCOUNT(wParam 0, lParam 0) – получить кол-во выбранных элементов.
LB_GETSELINTEM(wParamnMax, lParampBuf)– заполняет буфер массивом выделенныъ элементов. nMax–задает максимальное кол-во элементов.
LB_INSERTSTRING(wParamiIndexmlParamszString) – применяется для списка с отключённой сортировкой.
LB_SELECTSTRING(wParam iStart, lParam azString) – Аналогично сообщению LB_FINDSTRING, но дополнительно выделяется найденная строка.
LB_SETSEL(wParam, lParamiIndex) – выбрать элемент с индексом iIndexв списке с множественным выборов. Если wParam = true, элемент выбирается и выделяется, если wParam = false, выбор отменяется. Если lParam -1, операция применяется ко всем элементам списка.
LB_GETTEXT(wParamiIndex, lParamszString) – скопировать строку с указанным индексом, в буфер szString.
LB_DELETESTRING(wParam iIndex, lParam 0) – удалить строку индексом указанным iIndex.
LB_GETCOUNT(0,0) – получить кол-во элементов.
LB_GETTEXTLEN(iIndex, 0) – получить длину строки с указанным индексом.
LB_RESETCONTENT(0,0) – удалить все элементы из списка.
LB_SETCURSEL(iIndex, 0) – выбрать элемент с указанным индексом. Используется для списка с единичным выбором.
LB_SERIREMDATA(iIndex, nValue) – установить целочисленное значение nValue, ассоциированное в указанном списке.
LB_GETITEMDATA(iIndex, 0) – получить целочисленное значение, ассоциированное с элементом индекса списка.
LBN_SETFOCUS – список получил фокус.
LBN_KILLFOCUS – список потерял фокус.
LBN_SELCHANGE – текущий выбор был изменен.
LBN_DBLCLICK – на элементе списка, был двойной щелчок.
LBN_ERRSPACE – превышен размер памяти, выделенной под список.
  • avatar
  • 0
  • 0