Особенности использования стандартных lock'ов для многопотока

LaGir

Client
Регистрация
01.10.2015
Сообщения
256
Реакции
1 058
Баллы
93
Приветствую всех! :)

Сразу предупреждаю, что данная статья сугубо техническая (возможно, даже слишком для этого конкурса манимейкерских статей :ap:). Так что если что, неподготовленным умам рекомендую сразу покидать топик, не стоит мучать себя. :)


Кратко о стандартных lock'ах

Если вам приходилось использовать сниппеты работы со списками, таблицами и буфером обмена, наверняка много раз видели, что такие блоки кода помещаются в специальные конструкции, которые гарантируют правильную работу с указаными ресурсами в многопоточном режиме. В кубики подобные конструкции вставлены по умолчанию, просто мы их не видим.

C#:
Развернуть Свернуть Копировать
//Лочим код изменения списка для многопотока
lock (SyncObjects.ListSyncer){
    //Добавляем в список "Список 1" элемент со значением "строка"
    project.Lists["Список 1"].Add("строка");
}

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

Для популярных типов внешних ресурсов в ZennoPoster предусмотрено три объекта синхронизации, которые в C#-коде указываются в круглых скобках после lock:
SyncObjects.ListSyncer - для списков
SyncObjects.TableSyncer - для таблиц
SyncObjects.InputSyncer - для буфера обмена

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

Допустим, у нас шаблон работает с 1 списком и 1 таблицей, привязанных к файлам. Если использовать для них обоих 1 объект синхронизации, то может возникнуть такая ситуация: пока один поток работает со списком, другой поток подошел к работе с таблицей, но не может её начать - так как соответствующий код заблокирован тем же объектом, что и код работы со списком.

Чтобы такого не было, для каждого типа ресурса в ZennoPoster используется свой объект синхронизации. В результате получаем, что ситуация, когда один поток работает со списком, а другой с таблицей - нормальная и позволительная, а ситуация, когда 2 потока работают со списком - запрещена и невозможна, так как может привести к сбоям.


Проблема стандартных локов

С вышеописанной базой, думаю, всё понятно. Но давайте рассмотрим другую ситуацию.

Допустим, в проекте 10 списков, каждый из них привязан к своему собственному файлу.

Если лочить работу с каждым из них конструкцией lock (SyncObjects.ListSyncer) { ... }, то в многопотоке велика вероятность случится подобной ситуации:
Один поток начал работать, скажем, со списком №5, в этот момент другой поток дошёл до работы со списком №8. Что начинает делать другой поток? Ждать, когда первый поток закончит работу с пятым списком, так как оба лока блокируются одним и тем же объектом синхронизации.

Согласитесь, ситуация не совсем нормальная, так как списки (и, соответственно, файлы) совершенно разные, и второй поток ждать первого по-хорошему не должен. В перспективе это значит, что когда какой-то поток работает с одним списком, другие потоки могут "подтормаживать" не только на подходе к этому, но и ко всем другим спискам.
Если обратится к аналогии - пока через один из турникетов в метро кто-то проходит, соседние турникеты никого не пускают. :)

Если вы создаёте шаблоны только на кубиках - эта проблема касается и вас тоже, так как в них явно используются те же стандартные объекты синхронизации (на 100% всё же сказать не могу, т.к. не проверял, но вряд ли там используются другие объекты).


Проблема пересечения с другими шаблонами

Но это ещё не всё. Объекты SyncObjects.ListSyncer, SyncObjects.TableSyncer, SyncObjects.InputSyncer берутся из библиотечки Global.dll, что в нашем случае значит, что одни и те же объекты синхронизации применяются не только к разным потокам одного шаблона, но и вообще ко всем запущенным шаблонам.

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

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

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


Решение

Что же можно сделать?
С кубиками, к сожалению, ничего не сделать, а вот с кодом - можно. Необходимо создать свои собственные объекты синхронизации в Общем коде. По умолчанию там даже один уже есть:

04.png


Допустим, у нас в шаблоне 3 таблицы и 3 списка, привязанные к своим файлам. В этом случае рекомендуется создать по объекту на каждый файл.

2017-12-19_21-47-06.png


Далее можно использовать их в коде C#-сниппетов:

C#:
Развернуть Свернуть Копировать
lock (CommonCode.ProxyLocker){
    project.Lists["Прокси"].Add(proxy);
}
//...тут некий код
lock (CommonCode.KeywordsLocker){
    project.Lists["Ключевики"].Clear();
}
//...тут некий код
lock (CommonCode.ResultsLocker){
    project.Lists["Results"].AddRange(results);
}

Как результат - в многопоточном режиме у нас точно ни один поток зазря подтормаживать не будет. Также, как другие шаблоны не смогут влиять на текущий, так и текущий на другие - у всех свои собственные локи под каждый файл.
Вот мы и научились небольшой оптимизации проектов для многопотока и одновременной работе нескольких шаблонов. :)


PS

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

Реальные улучшения, как правило, встречаются в следующих ситуациях:
1) когда выполняются операции с ооооочень большими файлами;
2) когда используются "кривые" сниппеты - то есть, лочится не отдельная операция (н-р, добавление строки в таблицу), а целый набор операций (н-р, цикл добавления 100500 строк в таблицу).

Тем не менее, надеюсь, что вам статья придётся по вкусу, хотя бы чисто для расширения кругозора в сфере разработки на ZennoPoster. :)
 
Номер конкурса статей
  1. Восьмой конкурс статей
Тема статьи
  1. Другое
еу))) первая техническая статья в этом сезоне. :bp:Было и интересно и актуально,возьму на вооружение для своих проектов.
LaGir подскажи с разработчиками зенки эти методы согласовывались, не будет неожиданных траблов при внедрении?
 
Последнее редактирование:
  • Спасибо
Реакции: AZANIR и LaGir
Однозначно отдам свой голос за эту статью .... все эти "манимэйкерские" статьи в большиснтве случаев бесполезны (ну только если для мотивации) ... а тут реально нужная вещь разобрана!
 
LaGir подскажи с разработчиками зенки эти методы согласовывались, не будет неожиданных траблов при внедрении?
Описанный способ решения с разработчиками никак не согласовывался, но траблов быть не должно. Принцип работы локов и область видимости общего кода не раз обсуждались на форуме, ну а все утверждения в статье, разумеется, я многократно тестировал, примеры использовал на практике в своих шаблонах. :-)
 
Спасибо, хорошая статья!
Сталкивался с этим недавно и точно также решал, но для работы с СУБД MySql (select + update в многопотоке).

Но вот как-то возникал вопрос: а что, если мы работаем из нескольких шаблонов с одним файлом/таблицей и нужно сделать одну блокировку на несколько шаблонов (естественно, не используя стандартные локеры)? Как тогда лучше разрулить это - выносить в библиотеку или сделать общий код действительно "общим" через вынесение его в отдельный файл (но скорее всего это не поможет, подозреваю)?
 
  • Спасибо
Реакции: LaGir и WalkODoff
Стоит дополнить об области действия локов общего кода для проектов, которые несколько раз добавляются в ZP из одного шаблона
 
отличная статья
 
  • Спасибо
Реакции: LaGir
Но вот как-то возникал вопрос: а что, если мы работаем из нескольких шаблонов с одним файлом/таблицей и нужно сделать одну блокировку на несколько шаблонов (естественно, не используя стандартные локеры)? Как тогда лучше разрулить это - выносить в библиотеку или сделать общий код действительно "общим" через вынесение его в отдельный файл (но скорее всего это не поможет, подозреваю)?
Вынести в файл действительно не получится, даже если общий код привязывать к одному файлу, у разных шаблонов будет своя копия кода.
Выносить в библиотеку - отличный вариант, работать будут как стандартные, но создавать их в либе можно сколько угодно, под каждый файл/ресурс.

Стоит дополнить об области действия локов общего кода для проектов, которые несколько раз добавляются в ZP из одного шаблона
Честно говоря, пока не понял, что именно имеется в виду, можно поподробнее?
 
Вынести в файл действительно не получится, даже если общий код привязывать к одному файлу, у разных шаблонов будет своя копия кода.
Выносить в библиотеку - отличный вариант, работать будут как стандартные, но создавать их в либе можно сколько угодно, под каждый файл/ресурс.


Честно говоря, пока не понял, что именно имеется в виду, можно поподробнее?
если добавлять несколько проектов в ЗП из одного шаблона - статик переменные общего кода для них будут общие
 
  • Спасибо
Реакции: nicanil и LaGir
Для популярных типов внешних ресурсов в ZennoPoster предусмотрено три объекта синхронизации, которые в C#-коде указываются в круглых скобках после lock:
SyncObjects.ListSyncer - для списков
SyncObjects.TableSyncer - для таблиц
SyncObjects.InputSyncer - для буфера обмена

Не пойму откуда вы взяли, что нужно вообще пользоваться этими объектами? Поискал в документации и не встретил упоминания, что в шаблонах нужно синхронизировать через них.
Сдается мне, что они созданы исключительно для внутренней логики ZP. Т.е. пока вся статья выглядит так, что вы изначально пользуетесь тем, чем не стоит, а затем поясняете какие из этого проблемы вытекают.

100500 объектов синхронизации тоже так себе идея, ибо при таком количестве в больших шаблонах есть опасность залочить ресурс не тем объектом и "привет, баг!", который будет тяжело отловить.

Проще лочить ресурс в Зенке им же. Подчеркиваю, именно в Зенке.

Если запустите этот код, например, в 10 потоков, то в список запишется 10000 строк и ни одна не пропадет.
Код:
Развернуть Свернуть Копировать
var list1 = project.Lists["Список 1"];

for(int i = 0; i < 1000; i++)
{  
    lock(list1)
    {
        list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());    
    }
}
 
Не пойму откуда вы взяли, что нужно вообще пользоваться этими объектами? Поискал в документации и не встретил упоминания, что в шаблонах нужно синхронизировать через них.
Сдается мне, что они созданы исключительно для внутренней логики ZP. Т.е. пока вся статья выглядит так, что вы изначально пользуетесь тем, чем не стоит, а затем поясняете какие из этого проблемы вытекают.

100500 объектов синхронизации тоже так себе идея, ибо при таком количестве в больших шаблонах есть опасность залочить ресурс не тем объектом и "привет, баг!", который будет тяжело отловить.

Проще лочить ресурс в Зенке им же. Подчеркиваю, именно в Зенке.

Если запустите этот код, например, в 10 потоков, то в список запишется 10000 строк и ни одна не пропадет.
Код:
Развернуть Свернуть Копировать
var list1 = project.Lists["Список 1"];

for(int i = 0; i < 1000; i++)
{ 
    lock(list1)
    {
        list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());   
    }
}
а теперь набросай код, который в 10 потоков будет брать по строке с удалением из твоего списка и класть в новый. А потом удали дубли из нового списка
 
а теперь набросай код, который в 10 потоков будет брать по строке с удалением из твоего списка и класть в новый. А потом удали дубли из нового списка

И что изменится?
 
Кажется кто-то плохо читал литературу по C# и не знает как работает lock. А теперь вопрос кто это: doc или shtift? :bz:
 
  • Спасибо
Реакции: shtift и doc
кроме того, что часть строк будет потеряна, ничего
Challenge Accepted!

Вообще вы описали немного странную логику. Зачем нам удалять дубликаты в конце, почему бы не проверять элементы на наличие перед добавлением? Ну тем не менее.


Код:
Развернуть Свернуть Копировать
Action<IList<string>, string> SyncAdd = (list, value) =>
{
    lock(list)
    {
        list.Add(value);
    }
};

var list1 = project.Lists["Список 1"];
var list2 = project.Lists["Список 2"];


lock(list1)
{
    for(int i = 0; i < 1000; i++)
    {
        list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());          
    }
}

while(list1.Count > 0)
{
    lock(list1)
    {
        var firstElement = list1.First();
        SyncAdd(list2, firstElement);
        list1.RemoveAt(0);
    }
}

lock(list2)
{
    var hashset = new HashSet<string>(list2).ToList();
    list2.Clear();
    list2.AddRange(hashset);
}
 
Challenge Accepted!

Вообще вы описали немного странную логику. Зачем нам удалять дубликаты в конце, почему бы не проверять элементы на наличие перед добавление? Ну тем не менее.


Код:
Развернуть Свернуть Копировать
Action<IList<string>, string> SyncAdd = (list, value) =>
{
    lock(list)
    {
        list.Add(value);
    }
};

var list1 = project.Lists["Список 1"];
var list2 = project.Lists["Список 2"];


lock(list1)
{
    for(int i = 0; i < 1000; i++)
    {
        list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());          
    }
}

while(list1.Count > 0)
{
    lock(list1)
    {
        var firstElement = list1.First();
        SyncAdd(list2, firstElement);
        list1.RemoveAt(0);
    }
}

lock(list2)
{
    var hashset = new HashSet<string>(list2).ToList();
    list2.Clear();
    list2.AddRange(hashset);
}
это мой недосмотр. Исходя из скептического посыла поста и того, что для добавления лок в принципе не нужен, я был уверен, что в твоём изначальном коде нет лока. В итоге для меня пост был по содержанию что-то типа "вот лока нет и все строки целы", а там хоть есть, хоть нет - результат один будет. Отсюда и моё предложение проделать удаление строк, подразумевая без лока
 
"вот лока нет и все строки целы", а там хоть есть, хоть нет - результат один будет
Challenge Accepted V. 2

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

Код:
Развернуть Свернуть Копировать
var list1 = project.Lists["Список 1"];
for(int i = 0; i < 1000; i++)
{
    list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); 
}

Этот код не добавит 10000 строк при запуске в 10 потоков одновременно. Это происходит потому что несколько потоков могут просто перезаписать одну ячейку памяти и таким образом новые данные не добавятся.
 
Последнее редактирование:
Challenge Accepted V. 2

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

Код:
Развернуть Свернуть Копировать
var list1 = project.Lists["Список 1"];
for(int i = 0; i < 1000; i++)
{
  list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
}

Этот код не добавит 10000 строк при запуске в 10 потоков одновременно. Это происходит потому что несколько потоков могут просто перезаписать одну ячейку памяти и таким образом новые данные не добавятся.
значит у меня особенный компьютер)
c1d830b957a9aeaddadf16930664764f.png


Стартовое значение
a051e21668df060a85c9b9073c25e7d6.png


Промежуточные
6968561afeb7d3e7375c4c1cd1912a9c.png

dda2cc735037d4817b577dcc3af2ee36.png


Результаты
3f38104939f5701f992059d98d7013df.png

80f08a73a68df6bdb8ca9013081d7a08.png
 
Записывать id потока - это в корне неверное решение, так как может быть пул потоков с одним и тем же id, но сами потоки разные и могут конфликтовать.
Вообще тема уже давно исчерпала себя. Лочить можно и даже нужно тем же ссылочным объектом, которым и работаешь. Для того, чтобы использовать lock достаточно туда впихнуть ссылочный объект (есть исключение, например, string из-за того, что в момент компиляции проекта строка может быть интернирована).
 
  • Спасибо
Реакции: shtift
значит у меня особенный компьютер)
Видимо Зенка синхронизирует у себя что-то дополнительно. Если позапускаете этот код, то увидите, что каждый раз возвращается разное количество элементов.

Код:
Развернуть Свернуть Копировать
var list1 = project.Lists["Список 1"];
list1.Clear();

ThreadStart Add = () => {   
for(int i = 0; i < 100000; i++)
{ 
    list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());   
}
};

for(int i = 0; i < 10; i++)
{
    var t = new Thread(Add);
    t.Start();
}

project.SendInfoToLog(list1.Count.ToString(), true);
 
Записывать id потока - это в корне неверное решение, так как может быть пул потоков с одним и тем же id, но сами потоки разные и могут конфликтовать.
Действительно. Не знал, спасибо.
 
Видимо Зенка синхронизирует у себя что-то дополнительно. Если позапускаете этот код, то увидите, что каждый раз возвращается разное количество элементов.

Код:
Развернуть Свернуть Копировать
var list1 = project.Lists["Список 1"];
list1.Clear();

ThreadStart Add = () => {  
for(int i = 0; i < 100000; i++)
{
    list1.Add("Поток " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());  
}
};

for(int i = 0; i < 10; i++)
{
    var t = new Thread(Add);
    t.Start();
}

project.SendInfoToLog(list1.Count.ToString(), true);
ты выводишь счётчик не дождавшись завершения потоков
 
  • Спасибо
Реакции: shtift
ты выводишь счётчик не дождавшись завершения потоков
Точно, забыл. Если дожидаться завершения, то добавляются все элементы. Видимо особенности реализации зеновских списков.

Если var list1 = project.Lists["Список 1"]; заменить на var list1 = new List<string>(), то будет уже не все так гладко.
 
По статье, хорошо написана, доступным языком. Сохранил себе в копилку. Спасибо.
 
  • Спасибо
Реакции: LaGir
Точно, забыл. Если дожидаться завершения, то добавляются все элементы. Видимо особенности реализации зеновских списков.

Если var list1 = project.Lists["Список 1"]; заменить на var list1 = new List<string>(), то будет уже не все так гладко.

А кто-нибудь проверял, может зеновские списки уже полностью из коробки потокобезопасны? Ибо раз добавление элементов сделали thread-safe, то было бы логичным сделать потокобезопасными и другие методы.
 
А кто-нибудь проверял, может зеновские списки уже полностью из коробки потокобезопасны? Ибо раз добавление элементов сделали thread-safe, то было бы логичным сделать потокобезопасными и другие методы.
они не могут быть потокобезопасными, потому что их логика такого не позволяет. Например, чтобы взять строку с удалением, нужно отдельно взять, отдельно удалить. 2 действия. Если бы был отдельный метод, включающий в себя эти 2 действия, тогда он мог бы быть потокобезопасным
 
  • Спасибо
Реакции: orka13, LaGir и shtift
А кто-нибудь проверял, может зеновские списки уже полностью из коробки потокобезопасны? Ибо раз добавление элементов сделали thread-safe, то было бы логичным сделать потокобезопасными и другие методы.
Отличный челлендж! Думаю, все это обсуждение должно привести к истине, которую мы и усвоим.

@shtift, @doc, @amyboose, жгите, мужики!
 
  • Спасибо
Реакции: Sanekk
К слову, тут очень в тему будет обсудить [ThreadStatic]. Я вот с такой штукой сталкивался: http://zennolab.com/discussion/threads/41807/
Вроде бы в комментариях там было какое-то рабочее решение, но я пока так и не собрался его реализовать :(
 
Та все уже, отожгли)
Предполагаю, что методы зеновского листа потокобезопасны в том плане, что только один поток может одновременно вызывать метод, но в тоже время сама коллекция не является потокобезопасной. И как, сказал @doc, если нужно вызвать больше одного метода нужно их лочить, чтобы между выполениями не вклинился другой поток. А вообще можно не гадать и просто написать в саппорт. :D
 
  • Спасибо
Реакции: orka13

Кто просматривает тему: (Всего: 0, Пользователи: 0, Гости: 0)