Лучше конечно Qt-контейнеры, поскольку они Implicitly Shared. Ссылки можно кидать прямо между потоками, при этом разделение данных происходит только при попытке изменения. Операции копирования для всех Qt-контейнеров атомарны, экономится память, ресурсы CPU, код полностью свободен от механизмов синхронизации, вроде мутексов. Экземпляры Qt-контейнеров можно ложить в сигналы или пускать по событиям, не боясь в какой поток сигнал или событие прийдёт.
Более того, если набить собственную структуру кучей Qt-контейнеров, то время выполнения Copy-On-Write для такой структуры будет равно времени Copy-On-Write только для того поля, которое вы меняете. Если эту же структуру набить STL-контейнерами - время отделения будет равно сумме времени копирования всех полей, даже если вы захотели поменять в структуре всего один байт.
У каждого экземпляра счётчик ссылок, который автомарной операцией может меняться. Когда выполняется мутабельная операция - этот счётчик проверяется, и если не равен единице - делается глубокая копия. Процесс глубокой копии безопасен, так как экземпляры в других потоках не смогут изменить содержимое, поскольку для них счётчик ссылок до окончания процесса копирования всегда будет больше 1. После разделения у новой копии всегда количество ссылок 1, что значит, никто из других потоков физически не может ссылаться на обьект, а значит его можно менять. При этом в процессе копирования остальные обьекты могут свободно читать данные.
Суть такова - если у вас есть экземпляр QString - это ваша логическая копия и неважно сколько и из каких потоков на эту же строку ссылаются - вы можете делать с ней всё что угодно. Такой подход на порядок безопастней, поскольку убирает необходимость работы с памятью и мутексами.
Также Qt-контейнеры позволяют забыть про возвращение константных ссылок из классов, и при этом следить, чтобы пока ты работает с этой ссылкой обьект-хозяин не дай бог не измнил возвращённый экземпляр. В Qt можно попросту возвратить не ссылку, а сам экземпляр.
Контейнеры STL технически не могут выполнять задачи контейнеров Qt. И дело даже не в отсутствии реализации такой же многопоточности. Даже если кто-то завтра создаст реализацию контейнеров с интерфейсом STL и поведением Qt - это будет сизифов труд, поскольку проблема в предопределённом поведении STL. Вы попросту не сможете использовать эту библиотеку STL Qt Edition, поскольку она будет конфликтовать с кодом, написаным для обычного STL. Да и как вы обьясните другим, что с одной стороны программа совместима с STL на уровне исходного кода, а с другой стороны, чтобы её собрать нужна левая реализация этого STL, бинарно несовместимая с оригиналом? С Qt же вы можете как угодно мешать оба типа контейнеров в одной программе без опасений.
Many Qt classes are reentrant, but they are not made thread-safe, because making them thread-safe would incur the extra overhead of repeatedly locking and unlocking a QMutex. For example, QString is reentrant but not thread-safe. You can safely access different instances of QString from multiple threads simultaneously, but you can't safely access the same instance of QString from multiple threads simultaneously (unless you protect the accesses yourself with a QMutex).
All functions in this class are reentrant, except for ascii(), latin1(), utf8(), and local8Bit(), which are nonreentrant.
QString не является потокобезопасным, в нём не используются блокировки. Безопасно работать в каждом потоке с отдельным экземпляром, если не использовать самостоятельно мьютексы. Аналогичная ситуация с std::string.
> А как тогда идёт синхронизация ресурсов(самих контейнеров) между потоками? Как оно работает?
Доступ к одному и тому же объекту из разных потоков должен синхронизироваться с помощью мьютексов, спинлоков и пр., если в документации явно не сказано, что класс thread-safe. Qt'шное неявноее разделение данных не имеет никакого отношения к обеспечению потокобезопасности.
Пизди^WВрешь, не было у нас этого в 1-м семестре. Может на втором курсе и рассказывали, но я на лекциях не был и судя по уровню того, что было на тех лекциях на которых я был, не могли там таких вещей рассказывать.
QString не является потокобезопасным, в нём не используются блокировки. Безопасно работать в каждом потоке с отдельным экземпляром, если не использовать самостоятельно мьютексы. Аналогичная ситуация с std::string.
Обьясняю. Потокобезопастными являются не классы, а функции/методы. Если все методы в классе являются потокобезопастными, то и весь класс для удобства называют потокобезопастными. Теперь как это работает в Qt-контейнерах на примере того же QString:
Потокобезопастными являются все без исключения:
копирующие конструкторы
операторы присвоения
деструкторы
операторы явного отделения detach()
Это значит, что отделив одну логическую копию от другой между потоками, внутри самого потока с экземпляром можно делать всё что угодно, поскольку все функции - reentrant. И совершенно неважно, что несколько экземпляров в разных потоках ссылаются на одни и те же данные.
Теперь по поводу:
All functions in this class are reentrant, except for ascii(), latin1(), utf8(), and local8Bit(), which are nonreentrant.
Присмотритесь. Эти функции не являются частью Qt4 API, они выделены красным. Это режим совместимости с Qt3, функции находятся в Qt3Support. Эти несколько функций и не могут быть reentrant(), поскольку не позволяются безопастно отделить экземпляр, так как содержимое мультибайтной кодировки больше не хранится в Qt4 QString в отличии от Qt3 QString. Забудьте про них, как про страшный сон.
Доступ к одному и тому же объекту из разных потоков должен синхронизироваться с помощью мьютексов, спинлоков и пр., если в документации явно не сказано, что класс thread-safe. Qt'шное неявноее разделение данных не имеет никакого отношения к обеспечению потокобезопасности.
Вам нужно подучить матчасть, прежде чем дезинформировать других. А лучше почитать исходники, тоже хорошая практика.
Qt uses an optimization called implicit sharing for many of its value class, notably QImage and QString. Beginning with Qt 4, implicit shared classes can safely be copied across threads, like any other value classes. They are fully reentrant. The implicit sharing is really implicit.
P.S. Вообще я уже не удивляюсь, почему столько людей постоянно спрашивают чем Qt-контейнеры отличаются от STL, а некоторые с разгону кричат «велосипед». Ответ как всегда: RTFM!
> Вам нужно подучить матчасть, прежде чем дезинформировать других. А лучше почитать исходники, тоже хорошая практика.
Не торопись задирать носик, детка. В вопросе, на который отвечал я, речь шла о синхронизации доступа к одному и тому же объекту, а не к его различным копиям. Ты, как какой-то неадекват, в ответ начал рассказывать о том, как замечательно и атамарно работает банальный счетчик ссылок. Теперь для примера расскажи мне, как copy-on-write поможет тебе безопасно добавлять элементы в общий список из разных потоков?
потокобезопастными
Я бы на твоем месте постеснялся использовать слово «подучить» в принципе.
То что вы думали написать и то что написали - разные вещи. Говоря о доступе уточнили бы, что имеются в виду мутабельные операции, а то ведь совершенно непонятно.
Так вот, поскольку в контейнерах (не только Qt, а и STL) предусмотрено разделение данных (другими словами - копирующий конструктор) значит, что данные обьекта неуникальны и мутабельные операции на обьектах не могут гарантировать изменение целевого обекта в принципе, поскольку в принципе невозможно знать данные какого обьекта меняются. Эта схема идёт вразрез с идеей общих данных, неважно, поддерживают ли они copy-on-write или нет, и решается, как вы верное заметили, самостоятельными механизмами многопоточной синхронизации.
Класс QString не является потокобезопасным. С его экземпляром нельзя работать из разных потоков одновременно. Мьютексы в нём не используются из соображений производительности.
Но можно работать из одного потока с одим экземпляром, потому что функции QString не изменяют глобальные данные для своей работы, что являеся истиной также и для std::string, vector, map и т.п.
Операторы присваивания, копирующие конструкторы, деструкторы и т.п. не являются потокобезопасными у QString.
> Говоря о доступе уточнили бы, что имеются в виду мутабельные операции
Не вижу смысла рассуждать о том, что операции, не изменяющие никаких разделяемых данных, должны быть потокобезопасными. Если для кого-то это является важной фичей, то ради бога.
Хотя операторы присваивания, копирующие конструкторы, деструкторы у QString действительно потокобезопасны. Пишут в книге «Qt4: программррвание GUI на C++», что операции со счётчиками ссылок выполняют атомарно и поэтому при изменении данных какой-то функцией она сделает копию объекта, так как копирующий конструктор для начала увеличит счётчик на данные.
Но это всё значит геморрой вместо простоты STL, где запись в вектор (например vector<int>) выполняется в встроенном виде (inline) за 1 команду процессора без вызова функций и без каких бы то ни было дополнительных действий.
Операторы присваивания, копирующие конструкторы, деструкторы и т.п. не являются потокобезопасными у QString.
Вы ошибаетесь.
Все методы контейнеров в Qt - reentrant. Потокобезопастность достигается за счёт потокобезопастных (прошу прощение за тавтологию) конктрукторов, деструкторов и операций присваивания и дальнейшую работу с экземпляром как с копией. Изменение экземпляра в другом потоке без средств синхронизации достигается присваиванием:
Но это всё значит геморрой вместо простоты STL, где запись в вектор (например vector<int>) выполняется в встроенном виде (inline) за 1 команду процессора без вызова функций и без каких бы то ни было дополнительных действий.
В Qt-контейнерах присваивание тоже делается ровно одной атомарной операцией. Проверка при мутабельной операции тоже выполняется одной атомарной операцией, хотя они (мутабельные операции вроде QString::operator[]) настолько редки, что оверхед на их использование по сравнению с другой логикой просто смешной.
В крайнем случае, если затраты на атомарные операции сравнимы со сложностью самой логики, можно делать вместо:
QVector<int> ints;
ints.resize( 100 );
for ( int i = 0; i < 100; ++i )
ints[i] = i;
так:
QVector<int> ints;
ints.resize( 100 );
int * pints = ints.data();
for ( int i = 0; i < 100; ++i )
pints[i] = i;
То-есть проверка на мутабельность всего одна - в ints.data();
В результате получается, что действия с контейнером типа operator[] даже в однопоточной программе у QString и QVector требуют обязательной проверки счётчиков ссылок при каждом вызове, хотя я этого может и не просил.
std::vector, string, list тогда предпочтительней и проще для однопоточной работы. Для многопоточной работы с ОДНИМ объектом использование мьютекосв обязательно и в Qt и в STL.
Нахрена оно тогда нужно гемор с QString и QVector. Для особых специфических случаев, где без разделяемых данных них не обойтись, я и сам могу легко написать на основе того же vector.
Во-первых, проверка на счётчик ссылок будет и в STL, если конечно реализация не предусматривает глубокое копирование при присваивании, что есть полный ахтунг в наше время.
Во-вторых, мутабельные операции не являются узким местом. Пример с тем же QString::operator[] не состоятелен, поскольку подобные операции практически не используются, и как правило затраты на перерасспределение внутренней структуры контейнера, выделение памяти, создание обьектов, которыми заполняется контейнер - на порядки больше, чем жалкая атомарная операция. Более того, лично я написал десятки тысяч строк кода с использованием QString и ни разу не пользовался QString::operator[], поскольку в абсолютном большинстве случаев контейнеры используются для чтения, а не для записи. Прекратите экономить по одиному такту процессора на секунду в то время как бизнес-логика программы сжирает ресурсы CPU как Карлсон варенье. Если очень необходимо, то как экономить на атомарных операциях я привёл пример выше.
При всём желании не найдёте ни одной программы на Qt, тормозящей именно из-за атомарных проверок copy-on-write.
> operator [] не редки. У меня этого навалом в программах.
Хотелось бы посмотреть на код. Как правило новички делают типичную ошибку - используют QString::operator[] для чтения, в то время как нужно использовать константную QString::at(), не проверяющую счётчик ссылок вообще.
at как раз проверяет, а [] не проверяет у вектора из stl.
The at() function returns a reference to the element in the vector at index loc. The at() function is safer than the [] operator, because it won't let you reference items outside the bounds of the vector.
Мне Qt-шные классы больше нравятся, с ними удобнее работать. Естественно, тянуть Qt только из за них не стоит, но если проект использует Qt, то почему бы и нет.
Твои программы это твои программы. Если тебе важно иметь разделяемые данные, то в моих программах разделяемые данные не нужны вообще. Абсолютно. Никакой выгоды не дадут. А записей в контейнеры у меня не меньше, чем чтения из них.
Термин thread-safe относится не к классам, а к методам классов. Когда метод гарантирует, что его можно вызывать одновременно для одного и того же экземпляра класса из разных потоков. reentrant - можно вызывать только из одного потока в один момент времени для экземпляра. А потоконебезопастные методы встретить вообще сложно, как правило это те, что пытаются использовать статические переменные, при этом никаких механизмов защиты этой переменной для одновременного использования в методе нет.
Вообще-то речь шла про QString::operator[] и QString::at(). Как работать с QString инлайновыми функциями для записи без атомарной проверки я уже написал: QString::data().
* reentrant - Describes a function which can be called simultaneously by multiple threads when each invocation of the function references unique data. Calling a reentrant function simultaneously with the same data is not safe, and such invocations should be serialized.
* thread-safe - Describes a function which can be called simultaneously by multiple threads when each invocation references shared data. Calling a thread-safe function simultaneously with the same data is safe, since all access to the shared data are serialized.
Термин thread-safe применяется не к классам, а к методам класса. Если все методы в классе thread-safe - весь класс называется thread-safe для удобства, чтобы не повторяться в каждом методе отдельно. Если не все методы thread-safe - как каждого метода в отдельности делается приписка thread-safe он или нет.
К примеру контейнеры Qt - не thread-safe, а потокобезопастность работы с ними достигается за счёт создания копий и использования reentrant методов у этих копий. Выше я постарался подробно описать как.
>потокобезопастность работы с ними достигается за счёт создания копий и использования reentrant методов у этих копий. Выше я постарался подробно описать как.
Ну насмешил. «ПотокобезопасТноть» не достигается за счёт «создания копий и использования reentrant методов у этих копий». Потому что reentrant означает потокобезопасноть для работы с разными данными, а не для одних и тех же. С одним экземпляром класса без мьютексов никакой потокобезопасности не будет.
Ну насмешил. «ПотокобезопасТноть» не достигается за счёт «создания копий и использования reentrant методов у этих копий». Потому что reentrant означает потокобезопасноть для работы с разными данными, а не для одних и тех же. С одним экземпляром класса без мьютексов никакой потокобезопасности не будет.
Я рад, что развеселил вас, жалко радоваться вам недолго. Обьясняю, вот reentrant функция:
void i_am_a_reentrant_function( void * p );
Она потому и reentrant, что безопастна для вызова из любого количества потоков при условии, что значения p будут разные. Надеюсь с этим вы спорите не будете? Идём дальше, вот та же reentrant функция для, к примеру, QString::replace():
Теперь обратите внимание, что указатель &myCopyOfString не совпадает с указателем &stringFromOtherThread. Первый лежит неизвестно где, второй - у нас в стеке. Значит для этих строк можно вызывать reentrant методы в разных потоках.
Насмешил ещё и очень. Потокобезопасность у тебя для РАЗНЫХ экземпляров. STL вся такая «потокобезопасная». И любой класс, не изменяющий глобальные данные, тогда «потокобезопасен».