Опубликован: 06.12.2004 | Доступ: свободный | Студентов: 1087 / 122 | Оценка: 4.76 / 4.29 | Длительность: 20:58:00
ISBN: 978-5-9556-0021-5
Лекция 2:

Средства синхронизации потоков управления

Блокировки чтение-запись

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

Типичный пример использования блокировок чтение-записьсинхронизация доступа к буферизованным в памяти фрагментам файловой системы, особенно к каталогам (чем ближе к корню, тем чаще их читают и реже изменяют). Важно отметить в этой связи, что, в отличие от блокировок, реализуемых функцией fcntl() (см. курс [1]), блокировки чтение-запись могут применяться и там, где файловая система отсутствует (например, в минимальных конфигурациях, функционирующих под управлением соответствующей подпрофилю стандарта POSIX операционной системы реального времени).

Применительно к блокировкам чтение-запись предоставляются следующие группы функций.

  • инициализация и разрушение блокировок: pthread_rwlock_init(), pthread_rwlock_destroy() (см. листинг 2.20);
    #include <pthread.h>
    
    int pthread_rwlock_init (
      pthread_rwlock_t
        *restrict rwlock, 
      const pthread_rwlockattr_t
        *restrict attr);
    
    int pthread_rwlock_destroy (
      pthread_rwlock_t *rwlock);
    Листинг 2.20. Описание функций инициализации и разрушения блокировок чтение-запись.
  • установка блокировки на чтение: pthread_rwlock_rdlock(), pthread_rwlock_tryrdlock(), pthread_rwlock_timedrdlock() (см. листинги 2.21 и 2.22);
    #include <pthread.h>
    
    int pthread_rwlock_rdlock (
      pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_tryrdlock (
      pthread_rwlock_t *rwlock);
    Листинг 2.21. Описание функций установки блокировки на чтение.
    #include <pthread.h>
    #include <time.h>
    int pthread_rwlock_timedrdlock (
      pthread_rwlock_t *restrict rwlock, 
      const struct timespec *restrict abstime);
    Листинг 2.22. Описание функции установки блокировки на чтение с ограниченным ожиданием.
  • установка блокировки на запись: pthread_rwlock_wrlock(), pthread_rwlock_trywrlock(), pthread_rwlock_timedwrlock() (см. листинги 2.23 и 2.24);
    #include <pthread.h>
    
    int pthread_rwlock_wrlock (
      pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_trywrlock (
      pthread_rwlock_t *rwlock);
    Листинг 2.23. Описание функций установки блокировки на запись.
    #include <pthread.h>
    #include <time.h>
    int pthread_rwlock_timedwrlock (
      pthread_rwlock_t *restrict rwlock,
      const struct timespec *restrict abstime);
    Листинг 2.24. Описание функции установки блокировки на запись с ограниченным ожиданием.
  • снятие блокировки чтение-запись: pthread_rwlock_unlock() (см. листинг 2.25);
    #include <pthread.h>
    int pthread_rwlock_unlock (
      pthread_rwlock_t *rwlock);
    Листинг 2.25. Описание функции снятия блокировки чтение-запись.
  • инициализация и разрушение атрибутных объектов блокировок: pthread_rwlockattr_init(), pthread_rwlockattr_destroy() (см. листинг 2.26);
    #include <pthread.h>
    
    int pthread_rwlockattr_init (
      pthread_rwlockattr_t *attr);
    
    int pthread_rwlockattr_destroy (
      pthread_rwlockattr_t *attr);
    Листинг 2.26. Описание функций инициализации и разрушения атрибутных объектов блокировок.
  • опрос и установка атрибутов блокировок в атрибутных объектах: pthread_rwlockattr_getpshared(), pthread_rwlockattr_setpshared() (см. листинг 2.27).
    #include <pthread.h>
    
    int pthread_rwlockattr_getpshared (
      const pthread_rwlockattr_t 
      *restrict attr, int *restrict pshared);
    
    int pthread_rwlockattr_setpshared (
      pthread_rwlockattr_t *attr, 
      int pshared);
    Листинг 2.27. Описание функций опроса и установки атрибутов блокировок в атрибутных объектах.

Тонким вопросом, связанным с блокировками чтение-запись, является взаимодействие читателей и писателей. Согласно стандарту POSIX-2001, если речь не идет о приложениях реального времени, то от реализации зависит, будет ли приостановлен поток управления при попытке установить блокировку на чтение при наличии ожидающих освобождения той же блокировки писателей. (Как правило, чтобы воспрепятствовать зависанию писателей, читателей в подобной ситуации "тормозят", иначе они так и будут подхватывать блокировку друг у друга. Для политик планирования реального времени преимущество получает более приоритетный поток.)

Естественно, если блокировку установил писатель, читателю придется подождать. Если этим писателем оказался тот же поток, ждать придется долго...

Параллельно одним или несколькими потоками управления может быть установлено несколько блокировок на чтение. Сколько раз устанавливали такую блокировку, столько же раз ее необходимо снять. (Установка блокировки писателем возможна только после последнего снятия.) Максимально допустимое число одновременно установленных блокировок на чтение зависит от реализации.

Когда снимают блокировку, на которую претендуют и читатели, и писатели, как правило, преимущество получают писатели, но, строго говоря, решение зависит от реализации.

Разумеется, снимать блокировку может только тот поток управления, который ее устанавливал; в противном случае поведение неопределено.

В качестве иллюстрации применения блокировок чтение-запись приведем набор функций для работы со списками в многопотоковой среде. На листинге 2.28 показан заголовочный файл, содержащий необходимые описания, на листинге 2.29 – C-файл с реализацией функций. Листинг 2.30 содержит текст тестовой программы, листинг 2.31 – результаты ее работы. Обратим внимание на использование блокировок без приостановки выполнения и на проверку статуса завершения попыток их установить.

/* * * * * * * * * * * * * * */
/* Типы, структуры и функции     */
/* для многопотоковой работы     */
/* со списками */
/* * * * * * * * * * * * * * */

#ifndef g_LIST
#define g_LIST

/* Указатель на процедуру */
typedef void (*aproc) (void *, void *);

/* Указатель на целочисленную функцию */
typedef int (*afun) (void *, void *);

/* Элемент списка указателей */
/* на объекты произвольного типа */
typedef struct g_link {
    /* Ссылка на следующий элемент списка */
    struct g_link *pnext;
    /* Ссылка на объект */
    void *pvalue;             
} *g_linkP;

/* Список указателей на объекты */
/* произвольного типа */
typedef struct g_list {
    /* Голова списка */
    g_linkP head; 
  /* Блокировка, ассоциированная */
  pthread_rwlock_t lk;    
  /* со списком */
} *g_listP;

/* Инициализировать список */
extern void g_list_init (g_listP);

/* Завершить работу со списком */
extern void g_list_destroy (g_listP);

/* Вставить новый элемент */
/* в конец списка */
extern void g_list_ins_last (
  g_listP, void *);
/* Вставить новый элемент в */
/* список перед первым,     */
/* удовлетворяющим заданному */
/* свойству (или в конец)     */
extern void g_list_ins_fun (
  g_listP, void *, afun);

/* Удалить элемент из списка */
extern void g_list_del (
  g_listP, void *);

/* Применить процедуру ко всем */
/* элементам списка */
extern void g_list_forall_x (
  g_listP, aproc, void *);

/* Выбрать подходящий элемент списка */
extern void *g_list_suchas_x (
  g_listP, afun, void *);

/* Выдать элемент по номеру */
extern void *g_list_get_bynum (
  g_listP, int);

#endif
Листинг 2.28. Заголовочный файл g_list.h для функций работы со списками в многопотоковой среде.
/* * * * * * * * * * * * * * * * * * * * */
/* Реализация функций для многопотоковой */
/* работы со списками. */
/* Применяются блокировки чтение-запись */
/* без приостановки выполнения */
/* * * * * * * * * * * * * * * * * * * * */

#define _XOPEN_SOURCE 600

#include <pthread.h>
#include <stdlib.h>
#include <errno.h>
#include "g_list.h"

/* * * * * * * * * * * * * */
/* Инициализировать список     */
/* * * * * * * * * * * * * */
void g_list_init (g_listP plist) {
    plist->head = NULL;
    errno = pthread_rwlock_init (
      &plist->lk, NULL);
}
/* * * * * * * * * * * * * * * */
/* Завершить работу со списком     */
/* * * * * * * * * * * * * * * */
void g_list_destroy (g_listP plist) {
    errno = pthread_rwlock_destroy (
      &plist->lk);
}

/* * * * * * * * * * * * * * * * * * * * */
/* Вставить новый элемент в конец списка */
/* * * * * * * * * * * * * * * * * * * * */
void g_list_ins_last (g_listP plist,
   void *pval) {
    register g_linkP pt;
    register g_linkP *p;

    if ((errno = pthread_rwlock_trywrlock (
      &plist->lk)) != 0) {
        return;
    }

    for (p = &plist->head; 
        *p != NULL; p = &(*p)->pnext)
        ;
if ((pt = (g_linkP) malloc (sizeof (*pt)))
   != NULL) {
        pt->pnext = NULL;
        pt->pvalue = pval;
        *p = pt;
    }

    (void) pthread_rwlock_unlock (
      &plist->lk);
}

/* * * * * * * * * * * * * * * * * */
/* Вставить новый элемент в список */
/* перед первым, */
/* удовлетворяющим заданному свойству */
/* (или в конец) */
/* * * * * * * * * * * * * * * * */
void g_list_ins_fun (g_listP plist,
   void *pval, afun fun) {
    register g_linkP pt;
    register g_linkP *p;

    if ((errno = pthread_rwlock_trywrlock (
      &plist->lk)) != 0) {
        return;
    }

    for (p = &plist->head;
            (*p != NULL) 
              && (fun (pval, 
         (*p)->pnext->pvalue)
                   == 0);
            p = &(*p)->pnext)
        ;

    if ((pt = (g_linkP) malloc (
      sizeof (*pt))) != NULL) {
        pt->pnext = NULL;
        pt->pvalue = pval;
        *p = pt;
    }

    (void) pthread_rwlock_unlock (
      &plist->lk);
}
    
/* * * * * * * * * * * * * * */
/* Удалить элемент из списка */
/* * * * * * * * * * * * * * */
void g_list_del (g_listP plist,
   void *pval) {
    register g_linkP pt;
    register g_linkP *p;

    if ((errno = pthread_rwlock_trywrlock
      (&plist->lk)) != 0) {
        return;
    }

    for (p = &plist->head; *p != NULL;
       p = &(*p)->pnext) {
        if ((*p)->pvalue == pval) {
            pt = *p;
            *p = pt->pnext;
            free (pt);
            errno = 
              pthread_rwlock_unlock(
              &plist->lk);
            return;
        }
    }

    /* Пытаемся удалить */
    /* несуществующий элемент */
    (void) pthread_rwlock_unlock (
      &plist->lk);
    errno = EINVAL;
}


/* * * * * * * * * * * * * */
/* Применить процедуру ко */
/* всем элементам списка */
/* * * * * * * * * * * * */
void g_list_forall_x (g_listP plist,
  aproc proc, void *extobj) {
    register g_linkP pt;

    /* Устанавливаем блокировку на запись, */
    /* поскольку, возможно, процедура */
    /* модифицирует     */
    /* объекты, ссылки на которые */
    /* хранятся в списке */
    if ((errno = pthread_rwlock_trywrlock(
      &plist->lk)) != 0) {
        return;
    }

    for (pt = plist->head; pt != NULL;
       pt = pt->pnext) {
        proc (extobj, pt->pvalue);
    }

    (void) pthread_rwlock_unlock(
      &plist->lk);
}

/* * * * * * * * * * * * * * * * * * */
/* Выбрать подходящий элемент списка     */
/* * * * * * * * * * * * * * * * * * */
void *g_list_suchas_x (g_listP plist,
  afun fun, void *extobj) {
  register g_linkP pt;

    /* Устанавливаем блокировку на */
    /* чтение, так как     */
    /* считаем, что функция не */
    /* модифицирует объекты,     */
    /* ссылки на которые хранятся */
    /* в списке     */
    if ((errno = pthread_rwlock_tryrdlock(
      &plist->lk)) != 0) {
        return NULL;
    }

    for (pt = plist->head; pt != NULL;
      pt = pt->pnext) {
        if (fun (extobj, pt->pvalue)) {
            (void) pthread_rwlock_unlock(
              &plist->lk);
            return (pt->pvalue);
        }
    }

    (void) pthread_rwlock_unlock(
      &plist->lk);
    return NULL;
}

/* * * * * * * * * * * * * * */
/* Выдать элемент по номеру     */
/* * * * * * * * * * * * * * */
void *g_list_get_bynum (g_listP plist,
   int n) {
    register g_linkP pt;

    if ((errno =
       pthread_rwlock_tryrdlock(
      &plist->lk)) != 0) {
        return NULL;
    }

    for (pt = plist->head; n--, pt != NULL;
       pt = pt->pnext) {
        if (n == 0) {
           (void) pthread_rwlock_unlock(
              &plist->lk);
            return (pt->pvalue);
        }
    }

    /* Пытаемся извлечь несуществующий */
    /* компонент */
    (void) pthread_rwlock_unlock(
      &plist->lk);
    errno = EINVAL;

    return NULL;
}
Листинг 2.29. Исходный текст функций для работы со списками в многопотоковой среде.
/* * * * * * * * * * * * * * * * * * * */
/* Тест функций многопотоковой работы  */
/* со списками                         */
/* * * * * * * * * * * * * * * * * * * */

#define _XOPEN_SOURCE 600

#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include "g_list.h"

/* Структура для засыпания на */
/* минимальное время */
static struct timespec nslp =
  {0, 1};

/* Структуры для возврата результатов */
/* потоками:    */
/* Сколько операций пытались сделать,      */
/* сколько из них оказались неудачными     */
/* из-за занятости списка         */
static struct pt_res {
    int nops;
    int nbusy;
} ptres [3] = {{0, 0}, {0, 0}, {0, 0}};

/* * * * * * * * * * * * * * * * * * * * */
/* Этот поток добавляет элементы к списку */
/* * * * * * * * * * * * * * * * * * * * */
static void *start_func_1 (void *plist) {
    int *p1;


 while (1) {
    if ((p1 = 
     (int *) malloc (
       sizeof (int))) !=
        NULL) {
        *p1 = rand ();
        errno = 0;
        g_list_ins_last (plist, p1);
        ptres [0].nops++;
        if (errno == EBUSY) {
        ptres [0].nbusy++;
        }
    }
    (void) nanosleep (&nslp, NULL);
    }

    return (NULL);
}

/* * * * * * * * * * * * * * * * * * * * * */
/* Процедура для подсчета суммы элементов  */
/* списка                                  */
/* * * * * * * * * * * * * * * * * * * * * */
static void proc_sum (void *sum, void *pval) {
    *((int *) sum) += *((int *) pval);
}
/* * * * * * * * * * * * * * * * * * * * * */
/* Этот поток подсчитывает сумму элементов */
/* списка */
/* * * * * * * * * * * * * * * * * * * * * */
static void *start_func_2 (void *plist) {
    int sum;

    while (1) {
        sum = 0;
        errno = 0;
        g_list_forall_x (plist,
           &proc_sum, &sum);
        ptres [1].nops++;
        if (errno == EBUSY) {
            ptres [1].nbusy++;
        }
        (void) nanosleep (&nslp, NULL);
    }

    return (NULL);
}

/* * * * * * * * * * * * * * * * * */
/* Этот поток удаляет из списка     */
/* элементы со случайными номерами     */
/* * * * * * * * * * * * * * * * * */
static void *start_func_3 (void *plist) {
    int *p1;

    while (1) {
        errno = 0;
        p1 = (int *) g_list_get_bynum(
          plist, rand ());
        ptres [2].nops++;
        if (errno == EBUSY) {
            ptres[2].nbusy++;
        }

        if (p1 != NULL) {
            errno = 0;
            g_list_del (plist, p1);
            ptres [2].nops++;
            if (errno == EBUSY) {
             ptres [2].nbusy++;
            }
            free (p1);
        }
        (void) nanosleep (&nslp, NULL);
    }

    return (NULL);
}

/* * * * * * * * * * * * * * * * * * * * * * */
/* Начальный поток всех запустит, поспит, */
/* потом всех терминирует и выдаст статистику */
/* * * * * * * * * * * * * * * * * * * * * * */
int main (void) {
    g_listP plist;
    pthread_t pt1, pt2, pt3;

    g_list_init (plist);

    pthread_create (&pt1, NULL,
      start_func_1, plist);
    pthread_create (&pt2, NULL,
      start_func_2, plist);
    pthread_create (&pt3, NULL,
      start_func_3, plist);

    sleep (10);

    pthread_cancel (pt1);
    pthread_cancel (pt2);
    pthread_cancel (pt3);

    pthread_join (pt1, NULL);
    pthread_join (pt2, NULL);
    pthread_join (pt3, NULL);

    g_list_destroy (plist);

    printf ("Попыток выполнить "
            "операцию со списком: %d\n", 
        ptres [0].nops + ptres [1].nops
          + ptres [2].nops);
    printf ("Число попыток, неудачных"
            " из-за занятости "
                "списка: %d\n", 
        ptres [0].nbusy + ptres [1].nbusy
          + ptres [2].nbusy);

    return (0);
}
Листинг 2.30. Пример программы, использующей функции для работы со списками в многопотоковой среде.
Попыток выполнить операцию со списком: 1503 
Число попыток, неудачных из-за
  занятости списка: 0
Листинг 2.31. Возможные результаты работы программы, использующей функции для работы со списками в многопотоковой среде.

Можно видеть, что в данном случае оптимистичное применение блокировок чтение-запись без приостановки выполнения в случае невозможности немедленной установки себя полностью оправдало.

Владимир Крюков
Владимир Крюков
Казахстан