Разделяемое состояние

Последнее обновление: 16.09.2023

Иногда необходимо предоставить множеству клиентов/потоков доступ к одному и тому же состоянию, которое зависит от клиента. То есть каждый клиент может создавать объект состояния. Однако клиенты также могут работать с уже имеющимся состоянием, которое созадали другие клиенты.

В этом случае решением будет хранение состояния внутри модуля в виде списка объектов (поскольку каждый клиент может создавать свой объет состояния). Модуль также предоставляет клиенту некоторый API - набор функций для работы с состоянием. Клиент создает новый объект состояния или получает уже имеющийся и передает его в функции API для работы с ним. Причем при создании объекта состояния клиент должен предоставить некоторый идентификатор, который сохраняется в объекте состояния. Благодаря идентификатору можно разграничить данные объект состояния от других и проверить его наличие. Если объект с идентификатором уже есть, то клиент просто получает уже имеющийся объект. Если объект с идентификатором отсутствует, то он создается.

Формально паттерн можно представить следующим образом:

/* API модуля (заголовочный файл) */
// структура, которая представляет состояние
struct INSTANCE
{
  int x;
  int y;
};

// некоторый набор идентификаторов для объектов состояния
#define INSTANCE_TYPE_A 1
#define INSTANCE_TYPE_B 2
#define INSTANCE_TYPE_C 3

// Получем объект состояния, для этого в функцию передаем идентификатор объекта
struct INSTANCE* openInstance(int id);

// некоторая работа с состоянием
void operateOnInstance(struct INSTANCE* inst);

// освобождаем обхект
void closeInstance(struct INSTANCE* inst);

/* Реализация в модуле*/
// максимальное число объектов состояния
#define MAX_INSTANCES 4

struct INSTANCELIST
{
  struct INSTANCE* inst;
  int count;
};
// список, который хранит все объекты состояния
static struct INSTANCELIST list[MAX_INSTANCES];

// создаем объект состояния
struct INSTANCE* openInstance(int id)
{
  if(list[id].count == 0)
  {
    list[id].inst =  malloc(sizeof(struct INSTANCE));
  }
  list[id].count++;
  return list[id].inst;
}

void operateOnInstance(struct INSTANCE* inst)
{
  // некоторая работа с состоянием
}
// получаем состояние по id
static int getInstanceId(struct INSTANCE* inst)
{
  int i;
  for(i=0; i<MAX_INSTANCES; i++)
  {
    if(inst == list[i].inst)
    {
      break;
    }
  }
  return i;
}
// удаляем состояние
void closeInstance(struct INSTANCE* inst)
{
  int id = getInstanceId(inst);
  list[id].count--;
  if(list[id].count == 0)
  {
    free(inst);
  }
}

/* Возможные клиенты, которые работают с состоянием */
// клиент 1
struct INSTANCE* inst = openInstance(INSTANCE_TYPE_B); // получаем объект
operateOnInstance(inst);    // работа с объектом
closeInstance(inst);        // освобождение объекта

// клиент 2
struct INSTANCE* inst = openInstance(INSTANCE_TYPE_B); // получаем объект
operateOnInstance(inst); // работа с объектом
closeInstance(inst);        // освобождение объекта

Клиент получает объект в указатель INSTANCE, вызывая фукцию openInstance. Если объект INSTANCE не существует, то он создается. Если существует, то возвращается. Причем функция фукцию openInstance также увеличивает счетчик клиентов, которые в текущий момент обращаются к объекту состояния. Получив объект INSTANCE, для работы с ним клиент передает его в функции API. Завершив работу, клиент удаляетобъект с помощью вызова функции closeInstance. Функция closeInstance учитывает количество обративнихся к этому объекту клиентов. И если клиентов, которые работают с этим объектом уже нет, то объект удаляется.

Таким образом, множество клиентов могут одновременно обращаться к единому разделяемому состоянию. Такой паттерн еще называют Shared Instance

Примером применения паттерна являются функции для работы с файлами в stdio.h. Так, функция fopen открывает файл, а функция fclose() закрывает файл. Между вызовами этих функцию могут вызываться функции для работы с файлом, в частности, запись или чтение файла.

Рассмотрим небольшой пример применения. Допустим, нам надо создать программу для работы с базами данных. Поскольку создание подключения к базу данных и последующее его закрытие может быть затратно в плане производительности, и в то же время с одной базой данных могут одновременно работать несколько клиентов, то в этой ситуации мы как раз можем применить данный паттерн. Для начала создадим заголовочный файл database.h и определим в нем API:

struct Database;        // условная база данных
struct Database* openDatabase(size_t id);   // открытие базы данных
void closeDatabase(struct Database* db);    // закрытие базы данных

void setUser(struct Database* db, size_t index, char* name);    // изменение данных
void printUsers(struct Database* db);   // получение данных

Здесь структура Database представляет условную базу данных - состояние модуля, которое зависит от клиента и которое может использоваться несколькими клиентами. Причем детали реализации этого состояния неизвестны, они скрыты от клиента, и клиент не знает, какие именно поля содержит структура Database.

Для работы с состоянием определены 4 функции. Функция openDatabase получает идентификатор базы данных (индекс в пуле подключений) и возращает объект Database, который затем используется клиентом. Для простоты здесь базы данных идентифицируются по числовым идентификатором, но в реальности можно было бы передавать и имя базы данных.

Функция closeDatabase получает объект Database и закрывает его. И также для непосредственной работы с базой данных определена функция setUser, которая получает объект базы данных, индекс (id) изменяемых данных и новое значение данных. И также определена функция printUsers, которая будет получать все данные и выводит их на консоль.

Далее создадим файл database.c и определим в нем реализацию API модуля:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "database.h" 
 
#define DATA_MAX_COUNT 5   // максимальное количество данных в условной базе данных
#define CON_MAX_COUNT 4 // максимальное количество подключений

// база данных - структура представляет разделяемое состояние
struct Database{
    size_t id;                     // индекс подключения
    char* data[DATA_MAX_COUNT];// условные данные
};
// подключение - структура для управления состоянием
struct Connection
{
  struct Database* db;  // подключение
  size_t count;            // количество обратившийся клиентов
};
// пул подключений
static struct Connection pool[CON_MAX_COUNT];

// открытие условной базы данных
struct Database* openDatabase(size_t id)
{
  // проверяем индекс базы данных
  assert(id<CON_MAX_COUNT && "Invalid database id");
  // если еще нет клиентов, которые обращаются к бд
  // значит база данных отсутствует, и ее надо создать
  if(pool[id].count==0) 
  {
    pool[id].db =  malloc(sizeof(struct Database)); // создаем базу данных
    pool[id].db->id = id;   // устанавливаем индекс подключения
  }
  pool[id].count++;     // увеличиваем счетчик клиентов
  return pool[id].db;   // возвращаем указатель на базу данных
}
// удаляем базу данных
void closeDatabase(struct Database* db)
{
  pool[db->id].count--;     // уменьшаем счетчик клиентов
  if(pool[db->id].count == 0)   // если клиентов больше нет
  {
    free(db);         // то освобождаем память
  }
}
// изменяем данные
void setUser(struct Database* db, size_t index, char* name)
{
    if(index < DATA_MAX_COUNT)
      db->data[index]=name;
    else
        printf("User not found\n");
}
// выводим данные на консоль
void printUsers(struct Database* db)
{
    for(size_t i =0; i < DATA_MAX_COUNT; i++)
    {   
        printf("%s\n",db->data[i]);
    }
}

Разделяемое состояние - структура Database состоит из числового идентификатора и массива data, который собственно представляет данные - некоторый набор строк.

Для управления состоянием определена дополнительная структура - Connection или условно подключение к базе данных, которая хранит указатель на объект Database и количество клиентов, которые в текущий момент работают с базой данных. И также для управления состоянием определен условный пул подключений - массив структур Connection.

Реализация функции openDatabase получает идентификатор базы данных, в качестве которого для простоты будет служить индекс соответствующего подключения в пуле подключений. Если база данных с переданным идентификатором еще не создана (то есть нет обращающихся к ней клиентов), то она создается. Иначе функция возвращает уже ранее созданный объект Database. И также увеличивается счетчик работающих в базой данных клиентов.

Функция closeDatabase уменьшает счетчик клиентов, и если клиентов больше нет, освобождает память объекта Database.

Функция setUser изменяет значение одного объекта в базе данных по индексу (Для простоты здесь просто присваиваем строку и не учитываем, что присваиваемая строка может быть выделена в динамической памяти)

Функция printUsers выводит все данные из базы данных на консоль.

Таким образом, грубо говоря, при первом обращении к базе данных создается подключение к базе данных, выделяется память. При последующих обращениях, чтобы из пула подключений используется уже ранее открытое подключение. Если все клиенты завершили работу, то закрываем подключение - освобождаем память.

В данном случае идентификаторы (названия баз данных) для простоты жестко привязаны к индексам структур Connection в массиве pool. Но при желании можно создавать структуры Database с независимыми идентификаторами с дополнительной логикой поиска подключений к определенным базам данных в пуле подключений.

Для тестирования модуля database.c определим следующий файл app.c:

#include <stdio.h>
#include "database.h"

int main(void)
{
    // условный 1-й клиент создает базу данных с id 2
    struct Database* db1 = openDatabase(2); // 1-й клиент открывает базу данных
    // устанавливаем данные
    setUser(db1, 0, "Tom"); 
    setUser(db1, 1, "Bob");
    setUser(db1, 2, "Sam");
    printf("Users List for 1 client\n");
    printUsers(db1);

    // условный 2-й клиент создает базу данных с id 2
    struct Database* db2 = openDatabase(2);// 2-й клиент открывает базу данных
    setUser(db2, 3, "Alice");
    printf("\nUsers List for 2 client\n");
    printUsers(db2);
    
    closeDatabase(db1); // 1-й клиент закрывает базу данных
    closeDatabase(db2); // 2-й клиент закрывает базу данных
}

Здесь условно определены два клиента. Первый клиент работает с переменной db1, а второй клиент - с переменной db2. Каждый клиент сначала вызывает функцию openDatabase. Причем оба клиента обращаются к базе данных с одним и тем же идентификатором - 2. Однако в первом случае эта функция создаст новый объект Database, а во втором случае вернет уже существующий.

struct Database* db1 = openDatabase(2); // 1-й клиент открывает бд - создается новая БД

// .......................

// условный 2-й клиент создает базу данных с id 2
struct Database* db2 = openDatabase(2);// 2-й клиент открывает бд - возвращается уже существующая БД

После того, как каждый клиент получил объект Database, идет некоторая работа. Первый клиент устанавливает первые три объекта в базе данных и выводит список объектов на консоль. Второй клиент устанавливает один объект в базе данных и также выводит список объектов на консоль. Но так как они работают с одной базой данных, то и список объектов для них будет одним и тем же.

После завершения работы клиенты должны закрыть базу данных с помощью функции closeDatabase:

closeDatabase(db1); // 1-й клиент закрывает базу данных
closeDatabase(db2); // 2-й клиент закрывает базу данных - память освобождается

Но в первом случае функция closeDatabase просто уменьшает счетчик клиентов, а во втором освобождает память. После этого подключение закрыто.

Пример компиляции и работы программы:

c:\C>gcc -Wall -pedantic app.c database.c -o app & app
Users List for 1 client
Tom
Bob
Sam

Users List for 2 client
Tom
Bob
Sam
Alice

c:\C>
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850