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

LaGir

Client
Регистрация
01.10.2015
Сообщения
186
Благодарностей
628
Баллы
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. :-)
 

Для запуска проектов требуется программа ZennoPoster.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...

Для того чтобы запустить шаблон, откройте программу ZennoPoster. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.

Sanekk

Client
Регистрация
24.06.2016
Сообщения
654
Благодарностей
240
Баллы
43
еу))) первая техническая статья в этом сезоне. :bp:Было и интересно и актуально,возьму на вооружение для своих проектов.
LaGir подскажи с разработчиками зенки эти методы согласовывались, не будет неожиданных траблов при внедрении?
 
Последнее редактирование:
  • Спасибо
Реакции: AZANIR и LaGir

WebBot

Client
Регистрация
04.04.2015
Сообщения
1 075
Благодарностей
638
Баллы
113
Однозначно отдам свой голос за эту статью .... все эти "манимэйкерские" статьи в большиснтве случаев бесполезны (ну только если для мотивации) ... а тут реально нужная вещь разобрана!
 

LaGir

Client
Регистрация
01.10.2015
Сообщения
186
Благодарностей
628
Баллы
93
LaGir подскажи с разработчиками зенки эти методы согласовывались, не будет неожиданных траблов при внедрении?
Описанный способ решения с разработчиками никак не согласовывался, но траблов быть не должно. Принцип работы локов и область видимости общего кода не раз обсуждались на форуме, ну а все утверждения в статье, разумеется, я многократно тестировал, примеры использовал на практике в своих шаблонах. :-)
 

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 250
Благодарностей
2 734
Баллы
113
Спасибо, хорошая статья!
Сталкивался с этим недавно и точно также решал, но для работы с СУБД MySql (select + update в многопотоке).

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

doc

Client
Регистрация
30.03.2012
Сообщения
7 390
Благодарностей
3 555
Баллы
113
Стоит дополнить об области действия локов общего кода для проектов, которые несколько раз добавляются в ZP из одного шаблона
 

AgentRassilok

Известная личность
Регистрация
08.11.2016
Сообщения
1 273
Благодарностей
449
Баллы
83
отличная статья
 
  • Спасибо
Реакции: LaGir

LaGir

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

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

doc

Client
Регистрация
30.03.2012
Сообщения
7 390
Благодарностей
3 555
Баллы
113
Вынести в файл действительно не получится, даже если общий код привязывать к одному файлу, у разных шаблонов будет своя копия кода.
Выносить в библиотеку - отличный вариант, работать будут как стандартные, но создавать их в либе можно сколько угодно, под каждый файл/ресурс.


Честно говоря, пока не понял, что именно имеется в виду, можно поподробнее?
если добавлять несколько проектов в ЗП из одного шаблона - статик переменные общего кода для них будут общие
 
  • Спасибо
Реакции: nicanil и LaGir

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
Для популярных типов внешних ресурсов в 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());    
    }
}
 
  • Спасибо
Реакции: Alex733 и Zymlex

doc

Client
Регистрация
30.03.2012
Сообщения
7 390
Благодарностей
3 555
Баллы
113
Не пойму откуда вы взяли, что нужно вообще пользоваться этими объектами? Поискал в документации и не встретил упоминания, что в шаблонах нужно синхронизировать через них.
Сдается мне, что они созданы исключительно для внутренней логики 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 потоков будет брать по строке с удалением из твоего списка и класть в новый. А потом удали дубли из нового списка
 

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
а теперь набросай код, который в 10 потоков будет брать по строке с удалением из твоего списка и класть в новый. А потом удали дубли из нового списка
И что изменится?
 

amyboose

Client
Регистрация
21.04.2016
Сообщения
2 060
Благодарностей
865
Баллы
113
Кажется кто-то плохо читал литературу по C# и не знает как работает lock. А теперь вопрос кто это: doc или shtift? :bz:
 
  • Спасибо
Реакции: shtift и doc

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
кроме того, что часть строк будет потеряна, ничего
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);
}
 

doc

Client
Регистрация
30.03.2012
Сообщения
7 390
Благодарностей
3 555
Баллы
113
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);
}
это мой недосмотр. Исходя из скептического посыла поста и того, что для добавления лок в принципе не нужен, я был уверен, что в твоём изначальном коде нет лока. В итоге для меня пост был по содержанию что-то типа "вот лока нет и все строки целы", а там хоть есть, хоть нет - результат один будет. Отсюда и моё предложение проделать удаление строк, подразумевая без лока
 

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
"вот лока нет и все строки целы", а там хоть есть, хоть нет - результат один будет
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 потоков одновременно. Это происходит потому что несколько потоков могут просто перезаписать одну ячейку памяти и таким образом новые данные не добавятся.
 
Последнее редактирование:

doc

Client
Регистрация
30.03.2012
Сообщения
7 390
Благодарностей
3 555
Баллы
113
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 потоков одновременно. Это происходит потому что несколько потоков могут просто перезаписать одну ячейку памяти и таким образом новые данные не добавятся.
значит у меня особенный компьютер)


Стартовое значение


Промежуточные



Результаты

 

amyboose

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

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
значит у меня особенный компьютер)
Видимо Зенка синхронизирует у себя что-то дополнительно. Если позапускаете этот код, то увидите, что каждый раз возвращается разное количество элементов.

Код:
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

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
Записывать id потока - это в корне неверное решение, так как может быть пул потоков с одним и тем же id, но сами потоки разные и могут конфликтовать.
Действительно. Не знал, спасибо.
 

doc

Client
Регистрация
30.03.2012
Сообщения
7 390
Благодарностей
3 555
Баллы
113
Видимо Зенка синхронизирует у себя что-то дополнительно. Если позапускаете этот код, то увидите, что каждый раз возвращается разное количество элементов.

Код:
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

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
ты выводишь счётчик не дождавшись завершения потоков
Точно, забыл. Если дожидаться завершения, то добавляются все элементы. Видимо особенности реализации зеновских списков.

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

ТРОН

Client
Регистрация
31.07.2016
Сообщения
304
Благодарностей
314
Баллы
63
По статье, хорошо написана, доступным языком. Сохранил себе в копилку. Спасибо.
 
  • Спасибо
Реакции: LaGir

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
Точно, забыл. Если дожидаться завершения, то добавляются все элементы. Видимо особенности реализации зеновских списков.

Если var list1 = project.Lists["Список 1"]; заменить на var list1 = new List<string>(), то будет уже не все так гладко.
А кто-нибудь проверял, может зеновские списки уже полностью из коробки потокобезопасны? Ибо раз добавление элементов сделали thread-safe, то было бы логичным сделать потокобезопасными и другие методы.
 

doc

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

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 250
Благодарностей
2 734
Баллы
113
А кто-нибудь проверял, может зеновские списки уже полностью из коробки потокобезопасны? Ибо раз добавление элементов сделали thread-safe, то было бы логичным сделать потокобезопасными и другие методы.
Отличный челлендж! Думаю, все это обсуждение должно привести к истине, которую мы и усвоим.

@shtift, @doc, @amyboose, жгите, мужики!
 
  • Спасибо
Реакции: Sanekk

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 250
Благодарностей
2 734
Баллы
113
К слову, тут очень в тему будет обсудить [ThreadStatic]. Я вот с такой штукой сталкивался: http://zennolab.com/discussion/threads/41807/
Вроде бы в комментариях там было какое-то рабочее решение, но я пока так и не собрался его реализовать :(
 

shtift

Client
Регистрация
29.07.2015
Сообщения
149
Благодарностей
242
Баллы
43
Та все уже, отожгли)
Предполагаю, что методы зеновского листа потокобезопасны в том плане, что только один поток может одновременно вызывать метод, но в тоже время сама коллекция не является потокобезопасной. И как, сказал @doc, если нужно вызвать больше одного метода нужно их лочить, чтобы между выполениями не вклинился другой поток. А вообще можно не гадать и просто написать в саппорт. :D
 
  • Спасибо
Реакции: orka13

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