ThreadStatic и List<string> = пересекаются данные в потоках (даже параллельных!!!)

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Нашел немного времени и решил допилить свою либу ProfileActions для работы с профилем ZP, а именно - добавить туда сохранение своих данных в профиль. Но каким-то образом споткнулся на странное и непонятное мне поведение, поэтому прошу помощи у знающих, чтобы сильно не переделывать логику работы.

Суть в том, что при работе с:
C#:
[ThreadStatic] private static List<string> variables = null;
почему-то данные внутри переменной variables пересекаются между потоками, даже последовательными, а не только параллельными (к слову, с Dictionary<string, string> всё на 100% окей).

Привожу отрывок кода, который воспроизводит эту ситуацию:
C#:
namespace ZPProfileActions
{
    public static class ProfileActions
    {
        [ThreadStatic] private static List<string> variables = null;

        private static void InitVariables() {
            if (ProfileActions.variables == null) {
                ProfileActions.variables = new List<string>();
            }
        }

        public static void SpyVariable(IZennoPosterProjectModel project, string varname) {
            ProfileActions.InitVariables();
         
            project.SendInfoToLog("Vars: " + String.Join(",", ProfileActions.variables.ToArray()), true);
         
            if (!ProfileActions.variables.Contains(varname)) {
                ProfileActions.variables.Add(varname);
            } else {
                throw new Exception(String.Format(
                    "[ProfileActions.SpyVariable]: already spying on variable '{0}'!",
                    varname
                ));
            }
         
        }
    }
}
Вызов этого, соответственно, из шаблона:
C#:
ProfileActions.SpyVariable(project, "test_var");
И вот что получается при последовательном выполнении (1 поток):



То есть проблема будет даже в том, что если в 1 шаблоне мы начали "следить" за переменной и добавили её в List, то совсем в другом шаблоне у нас вывалится ошибка при сохранении (этот код я не приводил, чтоб не путать), т.к. в другом шаблоне почему-то тоже окажется, что мы следим за этой переменной.

Почему так происходит?
Где я накосячил? :-)
 

up_lvl

Client
Регистрация
02.09.2014
Сообщения
130
Благодарностей
52
Баллы
28
Недавно столкнулся с массовой поломкой профилей и опять хотел узнать есть ли. Словами не передать. На версии 5.11.3.0. На разных машинах, в разных шаблонах.
Классная либа и она реально нужна, получается. Но нужен рафакторинг.

Элементы статических классов видны всем потокам так как они все находятся в одной области видимости, AppDomain, в процессе выполнения кода.
В зенопостере для клиента нет места где бы инициализировались классы из своих библиотек.
Инициализация своих классов происходит в стандартном кубике "Свой код С#" и добавляется в project.Context потому что после завершения работы кубика, все инициализированные объекты удаляются.
В дальнейшем, используй свои объекты через projеct.Context.
При редактировании любого из кубиков "Свой код С#" контекст теряется. Для комфортной работы с ProfileActions в прожектмейкере прийдется хранить дамп профиля в зенопостеровской переменной или списке. Лучше всего сериализовать настройки в джейсон и при работе с контекстом в случае потери обьекта десереализовывать его из переменной, ну и дампить по мере изменений.

Сперва вынеси variables и все что меняется в процессе работы в отдельный класс, допустим ProfileExt, а все остальной оставь как есть, но чтобы могло работать с этим классом, т. е. в параметры передавай ProfileExt из контекста.
 
Последнее редактирование:
  • Спасибо
Реакции: Yuriy Zymlex и Lord_Alfred

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Недавно столкнулся с массовой поломкой профилей и опять хотел узнать есть ли. Словами не передать. На версии 5.11.3.0. На разных машинах, в разных шаблонах.
Классная либа и она реально нужна, получается. Но нужен рафакторинг.

Элементы статических классов видны всем потокам так как они все находятся в одной области видимости, AppDomain, в процессе выполнения кода.
В зенопостере для клиента нет места где бы инициализировались классы из своих библиотек.
Инициализация своих классов происходит в стандартном кубике "Свой код С#" и добавляется в project.Context потому что после завершения работы кубика, все инициализированные объекты удаляются.
В дальнейшем, используй свои объекты через projеct.Context.
При редактировании любого из кубиков "Свой код С#" контекст теряется. Для комфортной работы с ProfileActions в прожектмейкере прийдется хранить дамп профиля в зенопостеровской переменной или списке. Лучше всего сериализовать настройки в джейсон и при работе с контекстом в случае потери обьекта десереализовывать его из переменной, ну и дампить по мере изменений.

Сперва вынеси variables и все что меняется в процессе работы в отдельный класс, допустим ProfileExt, а все остальной оставь как есть, но чтобы могло работать с этим классом, т. е. в параметры передавай ProfileExt из контекста.
2 раза перечитал, половину не понял... Что называется "почувствуй себя тупым" :-)
 
  • Спасибо
Реакции: orka13

shtift

Client
Регистрация
29.07.2015
Сообщения
148
Благодарностей
290
Баллы
63
Не совсем понял суть проблемы. Вставил ваш код в PM. Запустил несколько раз. Такой вывод:



Вроде все ок, нет?
 

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Вроде все ок, нет?
Не, как раз таки "не ок", т.к. между потоками/запусками получилось пересечение переменных :-) Получается что-то вроде того, что после остановки потока - в памяти остается это значение, а при повтором запуске - оно мешает и вываливается эта ошибка

Изначально я как раз об этом и писал, что с [ThreadStatic] при работе с Dictionary всё будет работать в многопотоке без пересечений, а вот с List - появятся пересечения между потоками.

И, да, либу надо мне рефакторить) Уже немного разобрался как сделать через project.Context передачу объектов, но не проверял что там за проблема может возникнуть с зенновским профилем (про которую писал @up_lvl)
 

shtift

Client
Регистрация
29.07.2015
Сообщения
148
Благодарностей
290
Баллы
63
между потоками/запусками получилось пересечение переменных
Я не понимаю, что вы имеете ввиду под пересечением переменных. Можете объяснить, что по вашему должно выводиться в лог при повторных запусках, только Exception?
 

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Я не понимаю, что вы имеете ввиду под пересечением переменных. Можете объяснить, что по вашему должно выводиться в лог при повторных запусках, только Exception?
При повторных запусках как раз не должно быть Exception, ветка if должна идти на условие "ProfileActions.variables.Add(varname);" и всё, суть то в том, что в каждом выполнении/потоке - должны быть свои переменные, а не общие. Но тут это всё скорее всего из-за первоначально неправильно построенной логики с static и [ThreadStatic] (думал он спасет, но нет) - просто как раз в ProfileActions либе, где это используется - выполнение методов должно происходить между несколькими кубиками C# (с сохранением состояния между ними) и первоначально я не знал о возможности передачи объектов через project.Context, поэтому сделал static методы и переменные в классе, поэтому получилось, что они иногда стали "общими" для потоков, [ThreadStatic] в какой-то мере помог это избежать, но в целом - всё равно каша получилась по итогу, которую нужно рефакторить и тогда либа будет достойна внимания каждого зенноюзера, чтоб использовать её заместо стандартных профилей.

ЗЫ: если кто-то хочет помочь с рефакторингом - могу все текущие изменения залить в отдельную ветку на гитхабе, а то я так ещё с месяц-два не соберусь допилить это всё. А либа то полезная и нужная )
 

shtift

Client
Регистрация
29.07.2015
Сообщения
148
Благодарностей
290
Баллы
63
При повторных запусках как раз не должно быть Exception,
Так ведь если запускать через ZP, то ошибок и нет. А в PM появляются ошибки, потому что, насколько я понимаю, висит один поток и при повторных запусках все в одном и том же потоке и выполняется. Или я опять что-то не так понял?
 

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113

shtift

Client
Регистрация
29.07.2015
Сообщения
148
Благодарностей
290
Баллы
63

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Хм..ну да, и у меня раз через раз появляются ошибки.
О том и речь) Поэтому я и сделал ссылку на эту тему в обсуждении локов, мало ли кто-то столкнется потом - чтоб знали если что)
 
  • Спасибо
Реакции: shtift

shtift

Client
Регистрация
29.07.2015
Сообщения
148
Благодарностей
290
Баллы
63
О том и речь) Поэтому я и сделал ссылку на эту тему в обсуждении локов, мало ли кто-то столкнется потом - чтоб знали если что)
Интересная проблема, я даже в саппорт написал. Посмотрим, что ответят.

В качестве альтернативы можно использовать словарь такого плана Dictionary<Guid, List<string>>, где ключ это идентификатор потока, а значение уже ваш список переменных. После того, как поток закончит работу удаляем запись из словаря.
 
Последнее редактирование:

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
В качестве альтернативы можно использовать словарь такого плана Dictionary<Guid, List<string>>, где ключ это идентификатор потока, а значение уже ваш список переменных. После того, как поток закончит работу удаляем запись из словаря.
Ещё в скайп-чате @Adigen посоветовал затестить вот такой код:
C#:
static readonly ThreadLocal<List<string>> variables = new ThreadLocal<List<string>>
        {
            Value = new List<string>()
        };
// и обращаемся через variables.Value.method
Возможно, стоит это сделать в либе и потестить, а уже потом над рефакторингом думать (оказывается, что не всё так просто с project.Context - он может теряться в PM при изменении C# кубиков, т.е. при дебаге можно отхватить косяков)
 

shtift

Client
Регистрация
29.07.2015
Сообщения
148
Благодарностей
290
Баллы
63
посоветовал затестить вот такой код:
Не, это не поможет. Я опробовал ThreadStatic, ThreadLocal и через LocalDataStoreSlot. Результат один и тот же.

Просто похоже на то, что после завершения выполнения шаблона поток не завершается, а берет на выполнение следующую задачу, поэтому значение полей, помеченых ThreadStatic сохраняются.
 
  • Спасибо
Реакции: Yuriy Zymlex и Lord_Alfred

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Просто похоже на то, что после завершения выполнения шаблона поток не завершается, а берет на выполнение следующую задачу, поэтому значение полей, помеченых ThreadStatic сохраняются.
Вот субъективно я что-то такое и ощутил, только объяснить нормально - не смог)

Значит всё таки надо рефакторить либу, где бы только время найти..
 
  • Спасибо
Реакции: one

one

Client
Регистрация
22.09.2015
Сообщения
6 793
Благодарностей
1 264
Баллы
113
2 раза перечитал, половину не понял... Что называется "почувствуй себя тупым"
Тоже два раза перечитал и совсем нифига не понял. Почему не понял, я знаю, но уверен, хорошее дело делаете мужики! Получилось бы все как задумано...
 
  • Спасибо
Реакции: shtift и Lord_Alfred

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Выложил эту ветку на гитхаб: https://github.com/lord-alfred/ProfileActions/tree/3.0
Там всё то, что я тогда не довел до логического коммита + правки по свежим версиям ZP (чтобы без ошибки сохраняло профиль). В данный момент не для продакшена эта версия, т.к. нужно пофиксить багу, но в целом - уже фундамент заложен :-)

@shtift и @up_lvl, если есть желание - присоединяйтесь к допиливанию! Буду рад пулл-реквестам :-)
 
  • Спасибо
Реакции: shtift

up_lvl

Client
Регистрация
02.09.2014
Сообщения
130
Благодарностей
52
Баллы
28
Сорян, нет времени. Вообще хранить бы инфу профилей не в файле, а где-то как-то без привязки к hdd, например в БД и вытаскивать всё что нужно по айди сущности(например аккаунта) его эмулируемые параметры+куки и апдейтить туда в случае необходимости.
 

Lord_Alfred

Client
Регистрация
09.10.2015
Сообщения
3 916
Благодарностей
3 856
Баллы
113
Сорян, нет времени. Вообще хранить бы инфу профилей не в файле, а где-то как-то без привязки к hdd, например в БД и вытаскивать всё что нужно по айди сущности(например аккаунта) его эмулируемые параметры+куки и апдейтить туда в случае необходимости.
Не проблема в будущем сменить бекенд на mysql/pgsql базу и выбирать при необходимости уже куда сейвить: в файл профиля / в базу.
Но пока что даже текущую багу с дублированием данных никто не помог решить (а код я выложил на гитхаб), так что я вряд ли соберусь ещё и бекенд сохранения накручивать сверху, пока это не будет исправлено.
 

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