Самая важная часть вашей программы - код управления данными. Если данные повреждены, то вся ваша программа абсолютно бесполезна. Достаточно просто убедится в том, что однопоточное приложение случайно не повредит данные. Совсем другая история с многопоточными приложениями.
Проблема перекрытия
Проблема многопоточных приложений заключается в том, что один поток может изменять данные в то самое время, когда другой эти данные использует. Если чтение и запись данных перекрываются, то могут появится осложнения.
Напишем небольшое приложение, использующее структуру такого вида:
typedef struct
{
int a;
int b;
int result;
int result2;
int use_count;
int use_count2;
int max_use;
int max_use2;
} app_data;
Будем передавать структуру app_data в поток, который читает данные и использует переменные a и b 100 раз, когда a равно пяти. Будем использовать локальную переменную "use", чтобы отсчитать 100 использований. Также будем увеличивать "use_count" ( допустим, если вам надо считать количество обращений к данным ). Добавим небольшую паузу ( usleep ), чтобы дать планировщику запустить и другие потоки:
void *user_thread(void *data)
{
int uses=0;
app_data *td=(app_data*)data;
while(usesmax_use)
{
if (td->a==5)
{
td->result+=(td->a+td->b);
td->use_count++;
uses++;
}
usleep(1);
} return 0;
}
Теперь напишем код, изменяющий данные. Будем продолжать изменять a и b до тех пор, пока они не будут использованы в других потоках (в данном случае user_thread) 100 раз. Изменения несложные: будем "переключать" значение переменной a между 5 и 50 и создавать ложные трудоемкие вычисления переменной b - поставим usleep (1000). (usleep всегда возвращает 0, если компьютер исправен.). Это значит, что изменится a, а через одну миллисекунду изменится b. Получается промежуток времени в одну миллисекунду, во время корогого a и b не должны использоваться нигде в приложении, поскольку они еще не приняли тех значений, которые нам нужны:
void *changer_thread(void *data)
{
app_data *td=(app_data*)data;
while ((td->use_count+td->use_count2)<(td->max_use+td->max_use2))
{
if (td->a==5)
{
td->a=50;
td->b=td->a+usleep(1000);
}
else
{
td->a=5;
td->b=td->a+usleep(1000);
}
usleep(1);
} return 0;
}
Теперь напишем функцию main(),которая будет создавать экземпляр app_data, запускать два потока, ожидать их завершения и печатать результаты:
main()
{
pthread_t ct,ut,st;
app_data td={5,5,0,0,0,0,100,0};
void *retval; pthread_create(&ut,NULL,user_thread,&td);
pthread_create(&ct,NULL,changer_thread,&td); pthread_join(ct,&retval);
pthread_join(ut,&retval);
printf("result should be %d, is %d\n",td.max_use*(5+5),td.result);
}
Скомпилируем нашу программу и посмотрим на результаты:
$ gcc assets.c -o assets
$ ./assets
result should be 1000, but it is 3965.
У вас получится другое значение, в зависимости от скорости процессора.
Понятно, что это совсем не то, что нам нужно. Почему мы получаем такой странный результат ? Проблема в том, что user_thread ожидает, пока a не станет равным 5, и добавляет значение a и b ( которые обе должны равняться 5 ) к результату. Но так как переменная b вычисляется слишком долго, иногда используется ее старое значение ( 50 ), вместо нового ( 5 ). Вам нужно защищать данные каким-то образом, чтобы user_thread не могла их использовать, когда вы этого не хотите.
Мутексы
Добро пожаловать в замечательный мир мутексов ( mutex = mutual exclusion, что переводится как "взаимное исключение" ). Из названия видно, что мутексы предназначены для того, чтобы сделать определнный код или данные доступными только одному потоку одновременно.
Использовать мутексы очень просто. После того, как мутекс создан, вы просто вставляете команду pthread_mutex_lock() перед кодом, использующим защищенные данные и pthread_mutex_unlock() сразу после этого кода. В этом случае нам надо защищать a и b как во время чтения, так и во время модификации.
Для начала напишем код создания и удаления мутекса. Поместим мутекс вот в такую структуру:
typedef struct
{
int a;
int b;
int result;
int result2;
int use_count;
int use_count2;
int max_use;
int max_use2;
pthread_mutex_t mutex;
} app_data;
Создавать и уничтожать мутекс будем в функции main(). Будем передавать NULL в качестве параметра attr, что означает использование параметров мутекса по умолчанию:
main()
{
pthread_t ct,ut,st;
app_data td={5,5,0,0,0,0,100,0};
void *retval; pthread_mutex_init(&td.mutex,NULL);
pthread_create(&ut,NULL,user_thread,&td);
pthread_create(&ct,NULL,changer_thread,&td); pthread_join(ct,&retval);
pthread_join(ut,&retval);
pthread_mutex_destroy(&td.mutex);
printf("result should be %d, is %d\n",td.max_use*(5+5),td.result);
}
Теперь добавим вызовы устанавливающие и снимающие блокировки перед кодом, использующим защищенные переменные( чтение и запись ). Поменяем цикл while() в user_thread:
while(usesmax_use)
{
pthread_mutex_lock(&td->mutex);
if (td->a==5)
{
td->result+=(td->a+td->b);
td->use_count++;
uses++;
}
pthread_mutex_unlock(&td->mutex);
usleep(1);
}
И цикл, изменяющий эти переменные:
while ((td->use_count+td->use_count2)<(td->max_use+td->max_use2))
{
pthread_mutex_lock(&td->mutex);
if (td->a==5)
{
td->a=50;
td->b=td->a+usleep(1000);
}
else
{
td->a=5;
td->b=td->a+usleep(1000);
}
pthread_mutex_unlock(&td->mutex);
usleep(1);
}
На этот раз после компиляции и запуска вы получите:
result should be 1000, but is 1000
Это то, что нам нужно.
А что если у вас только один поток изменяет данные, но больше чем один их читают ?
Добавим еще один поток, использующий a и b:
void *subtracter_thread(void *data)
{
int use=0;
app_data *td=(app_data*)data;
while(usemax_use2)
{
pthread_mutex_lock(&td->mutex);
if (td->a==50)
{
td->result2-=(td->a+td->b);
use++;
td->use_count2++;
}
pthread_mutex_unlock(&td->mutex);
usleep(1);
} return 0;
}
Добавим запуск этого потока в функцию main() и установим значение переменной max_use2 в структуре app_data:
main()
{
pthread_t ct,ut,st;
app_data td={5,5,0,0,0,0,100,100};
void *retval; pthread_mutex_init(&td.mutex,NULL);
pthread_create(&ut,NULL,user_thread,&td);
pthread_create(&ct,NULL,changer_thread,&td);
pthread_create(&st,NULL,subtracter_thread,&td); pthread_join(st,&retval);
pthread_join(ct,&retval);
pthread_join(ut,&retval);
pthread_mutex_destroy(&td.mutex);
printf("result should be %d, is %d\n",td.max_use*(5+5),td.result);
printf("result2 should be %d, is %d\n",-(td.max_use2*(50+50)),td.result2);
}
Скомпилируем и запустим:
result should be 1000, is 1000
result2 should be -10000, is -10000
Стоп. А не должны ли user_thread и subtracter_thread читать значение a и b одновременно ? ( Помните, что код, заключенный между вызовами блокировки и разблокировки мутекса не может быть запущен, пока этот мутекс блокирован в другом потоке. ) Ни одна из них не именяет a и b, так что мы вполне можем читать их одновременно, пока они не изменяются в changer_thread.
Rwlocks
С помощью другуго блокирующего механизма, rwlocks, сделать это несложно. В отличие от мутексов, rwlock может иметь два состояния блокировки: чтение и запись. Пока rwlock не заблокирован на запись, любой поток может устанавливать и снимать блокировку на чтение. Если же установлена блокировка на запись, то все блокировки на чтение блокируются, пока блокировка на запись не снята.
Для начала поменяем мутекс на rwlock в нашей структуре данных:
typedef struct
{
int a;
int b;
int result;
int result2;
int use_count;
int use_count2;
int max_use;
int max_use2;
pthread_rwlock_t rwl;
} app_data;
Дальше, поменяем инициализацию мутекса на rwlock в функции main (). Опять же используем NULL в качестве атрибутов rwlock:
main()
{
pthread_t ct,ut,st;
app_data td={5,5,0,0,0,0,100,100};
void *retval;
pthread_rwlock_init(&td.rwl);
pthread_create(&ut,NULL,user_thread,&td);
pthread_create(&ct,NULL,changer_thread,&td);
pthread_create(&st,NULL,subtracter_thread,&td);
pthread_join(st,&retval);
pthread_join(ct,&retval);
pthread_join(ut,&retval);
pthread_rwlock_destroy(&td.rwl);
printf("result should be %d, is %d\n",td.max_use*(5+5),td.result);
printf("result2 should be %d, is %d\n",-(td.max_use2*(50+50)),td.result2);
}
Теперь поменяем pthread_mutex_lock на pthread_rwlock_rdlock и pthread_mutex_unlock на pthread_rwlock_unlock в user_thread и changer_thread. Это обезопасит доступ на чтение.
Также заменим pthread_mutex_lock на pthread_rwlock_wrlock и pthread_mutex_unlock на pthread_rwlock_unlock в функции changer_thread function, что обезопасит доступ на запись:
void *user_thread(void *data)
{
int uses=0;
app_data *td=(app_data*)data;
while(usesmax_use)
{
pthread_rwlock_rdlock(&td->rwl);
if (td->a==5)
{
td->result+=(td->a+td->b);
td->use_count++;
uses++;
}
pthread_rwlock_unlock(&td->rwl);
usleep(1);
} return 0;
} void *changer_thread(void *data)
{
app_data *td=(app_data*)data;
while ((td->use_count+td->use_count2)<(td->max_use+td->max_use2))
{
pthread_rwlock_wrlock(&td->rwl);
if (td->a==5)
{
td->a=50;
td->b=td->a+usleep(1000);
}
else
{
td->a=5;
td->b=td->a+usleep(1000);
}
pthread_rwlock_unlock(&td->rwl);
usleep(1);
} return 0;
} void *subtracter_thread(void *data)
{
int use=0;
app_data *td=(app_data*)data;
while(usemax_use2)
{
pthread_rwlock_rdlock(&td->rwl);
if (td->a==50)
{
td->result2-=(td->a+td->b);
use++;
td->use_count2++;
}
pthread_rwlock_unlock(&td->rwl);
usleep(1);
} return 0;
}
Вот оно! Теперь subtracter_thread и user_thread могут читать данные одновременно, а changer_thread не имеет к ним доступ, пока они используются в subtracter_thread или в user_thread.
И последнее, что стоит сказать о rwlocks: они не могут накладываться друг на друга. То есть вы не можете дважды заблокировать rwlock, предварительно его не разблокировав.
Также rwlock нельзя заблокировать на чтение и на запись одновременно. Все это было сделано для того, чтобы максимально облегчить системную реализацию rwlocks.
Original document: http://support.qnx.com/articles/oct3101/index.html |