Конференция "Начинающим" » TThread копирование файла в 2-х потоках [D7]
 
  • PacMan © (01.05.12 20:31) [0]
    Помогите кто-нибудь.
    Нужно в 2-х потоках копировать файл.
    1-й поток читает файл и пишет данные в буфер1, затем содержимое буфера 1 передает в буфер 2.
    2-й поток пишет из буфера 2 в файл.
    бьюсь уже давно, ни как не могу понять как работают потоки.. как 1 заставить ждать, пока не выполнится что-либо во-2м...

    листинг:
    unit Unit1;

    interface

    uses
     Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
     Dialogs, unit2, StdCtrls, SyncObjs, Gauges;

    type
     TForm1 = class(TForm)
       Label1: TLabel;
       Label2: TLabel;
       Button1: TButton;
       Edit1: TEdit;
       Edit2: TEdit;
       OpenDialog1: TOpenDialog;
       SaveDialog1: TSaveDialog;
       Label3: TLabel;
       Gauge1: TGauge;
       procedure Button1Click(Sender: TObject);
       procedure Edit1Click(Sender: TObject);
       procedure Edit2Click(Sender: TObject);
     private
       { Private declarations }
     public
       { Public declarations }
     end;

    var
     Form1: TForm1;

    implementation

    {$R *.dfm}

    procedure TForm1.Button1Click(Sender: TObject);
    begin
     CriticalSection:=TCriticalSection.Create;
     stream1.Create(false);
     stream2.Create(false);
    end;

    procedure TForm1.Edit1Click(Sender: TObject);
    begin
     if OpenDialog1.Execute then
       Edit1.Text := OpenDialog1.FileName;
    end;

    procedure TForm1.Edit2Click(Sender: TObject);
    begin
     if SaveDialog1.Execute then
       Edit2.Text := SaveDialog1.FileName;
    end;

    end.

    unit Unit2;

    interface

    uses
     Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
     Dialogs, StdCtrls, SyncObjs;

    type
     stream1 = class(TThread)

     protected

     public
       Procedure Podgot;
       Procedure ZabivBuf;
       Procedure Zakon;
       procedure Execute; override;
     end;

     stream2 = class(TThread)

     protected

     public
       procedure Execute; override;
       procedure FWrite;
     end;

     var
     f1, f2: File; // первый и второй файл
     buf1: array [1..8192] of Char; //буфер 1
     buf2: array [1..8192] of Char; //буфер 2
     sizefile, sizeread: Int64; //размер файла и размер прочитанного
     colRead, colWrite : Integer; //прочитано и записано
     fOtkuda, fKuda : String; //адреса и имена файлов
     CriticalSection: TCriticalSection;
     flag: Boolean;

     implementation
     uses unit1;

    procedure stream1.Execute;
    begin
     Podgot;
     while colRead = colWrite do
     begin
       if flag=false then   //если буфер 2 уже пустой
       begin
         CriticalSection.Enter;
         ZabivBuf;

       end;
     end;
     Zakon;
    end;

    procedure stream2.Execute;
    begin
     while colRead = colWrite do
     begin
       if flag=True then   //если буфер 2 уже пустой
       begin
         CriticalSection.Enter;
         Fwrite;
       end;
     end;
    end;

    Procedure stream1.Podgot;       // подготовка
    var i:integer;
    begin
     {i-} //даем компилятору директиву, чтобы не отслеживал ошибки ввода-вывода:
     if (Form1.Edit1.Text='') or (Form1.Edit2.Text='') then   //проверяем, указаны ли файлы. если нет - выходим
     begin
       ShowMessage('Вы не указали данные для копирования');
       Exit;
     end;
     //Try
       AssignFile(f1, Form1.Edit1.Text);  //связываем файловые переменные:
       AssignFile(f2, Form1.Edit2.Text);
       Reset(F1, 1);  //связываем файловые переменные:
       sizefile := FileSize(f1); //определяем его размер в переменную:
       Form1.Label3.Caption := 'Размер файла: '+IntToStr(Round(sizefile / 8192)) + ' Кб.';     //отображаем размер файла в килобайтах:
       Rewrite(f2, 1);  //создаем или перезаписываем второй файл:
       colRead := 0;    //делаем, пока не достигнут конец исходного файла
       colWrite := 0;
       sizeread := 0;
       Screen.Cursor := crHourGlass; //песочные часы
       {i+}
    end;

    Procedure Stream1.ZabivBuf;
    var
       i:integer;    //while colRead = colWrite do
    begin
     {i-}
     BlockRead(f1, Buf1, SizeOf(Buf1), colRead);
     //if colRead = 0 then break;

       for I := 1 to 8192 do
         Buf2[i]:=Buf1[i];  //Буфер1 в Буфер2

     Form1.Gauge1.Progress := Round(100*sizeread/sizefile);
     Flag:=True;
     CriticalSection.Leave;
     {i+}
    end;

    Procedure Stream1.Zakon;
    begin
     {i-}
       Screen.Cursor := crDefault; //обычный вид курсора
     //Finally
       CloseFile(f1);
       CloseFile(f2);
     //end; //try
     fOtkuda := Form1.Edit1.Text;  //исправляем дату
     fKuda := Form1.Edit2.Text;
     if IOResult <> 0 then
       Application.MessageBox('Ошибка при копировании файла!', 'Внимание!!!',
         MB_OK+MB_ICONERROR)
     else ShowMessage('Копирование  завершено успешно');
     //включаем обработчик компилятором ошибок
     {i+}
    End;

    procedure stream2.FWrite;
    var
       i: Integer;
    begin
       BlockWrite(f2, Buf2, colRead, colWrite);
       sizeread := sizeread + colRead;
       Flag:=False;
       CriticalSection.Leave;

    end;

    end.
  • sniknik © (01.05.12 20:44) [1]
    > как 1 заставить ждать, пока не выполнится что-либо во-2м...
    а то что по сути это станет одним потоком, просто с разнесенным кодом/логикой на два тебя не смущает?

    получается типа, "хочу нести воду в 2х ведрах, но так чтобы пол пути в одном, пол в другом. как на полпути воду в ведрах местами поменять? емкостей то больше нет".
  • PacMan © (01.05.12 20:52) [2]
    меня это не смущает, т.к. по заданию мне нужно копировать файл в 2-х потоках, и препод ничего не может мне подсказать по этому поводу.
    Если у кого-то есть "знание" как сделать хотя бы это, буду благодарен.
    Если кто-то может показать как сделать оптимальнее, также буду благодарен.
  • sniknik © (01.05.12 21:27) [3]
    > как 1 заставить ждать, пока не выполнится что-либо во-2м...
    уже используется - CriticalSection.Enter; во втором заставит первый ждать, не скажу насчет правильности, логики его использования у тебя, но это вот оно.

    и кстати у тебя куча всего чего нельзя в потоках... типа прямого обращения к vcl.
  • PacMan © (01.05.12 21:36) [4]
    ну синхронайз пока не трогаю...  программа запускается и так.

    при запуске я выбираю исходный файл, выбираю куда и под каким именем его скопировать...
    по кнопке "что-то происходит" и в конце выводится "копирование успешно завершено".
    открываю директорию назначения и вижу файлик с нужным именем... !НО! его размер 0 байт.
    смотрел пошаговое выполнение, у меня запускается 1-й поток, проходит круг... ииииии..... "копирование завершено".

    чтобы было понятнее что и как, могу на почту скинуть прогу.
  • QAZ (01.05.12 22:16) [5]

    > sniknik ©   (01.05.12 20:44) [1]

    там на самом деле гинеальная идея заложена :)
    типа пока один поток пишет первый кусок,другой читает второй и тд
    но она имеет смысл только при наличии 2х жестких дисков , если источник на одном а приемник на другом
  • Сергей М. © (01.05.12 22:18) [6]

    > 1-й поток читает файл и пишет данные в буфер1, затем содержимое
    > буфера 1 передает в буфер 2.
    > 2-й поток пишет из буфера 2 в файл.


    Это ты сам придумал или препод навязал тебе именно такую логику ?
  • sniknik © (01.05.12 22:50) [7]
    оптимальностью не пахнет, сама логика кривая, но... более... не знаю даже как сказать, но работоспособно это точно. -
    unit Unit1;

    interface

    uses
     Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
     Dialogs, Gauges, StdCtrls;

    const
     WM_WRITE = WM_USER + 101;

    type
     TForm1 = class(TForm)
       Edit1: TEdit;
       Edit2: TEdit;
       Button1: TButton;
       Gauge1: TGauge;
       procedure Button1Click(Sender: TObject);
     private
       procedure WMWrite(var Msg: TMessage); message WM_WRITE;
     public
     end;

    var
     Form1: TForm1;

    implementation

    {$R *.dfm}

    const
     BufSize  = 4096;
     WM_BUF   = WM_USER + 102;

    type
     TFileRead = class(TThread)
     protected
       F: File;
       FName: string;
       ThreadID2: integer;
       procedure Execute; override;
     public
       constructor Create(ID2: integer; const Name: string);
     end;

     TFileWrite = class(TThread)
     protected
       F: File;
       FName: string;
       MainHandle: THandle;
       procedure Execute; override;
     public
       constructor Create(Handle: THandle; const Name: string);
     end;

    constructor TFileRead.Create(ID2: integer; const Name: string);
    begin
     ThreadID2:= ID2;
     FName    := Name;
     inherited Create(false);
    end;

    procedure TFileRead.Execute;
    var
     Buf: PChar;
     NumRead: integer;
    begin
     AssignFile(F, FName);
     Reset(F, 1);

     repeat
       GetMem(Buf, BufSize);
       BlockRead(F, Buf^, BufSize, NumRead);
       PostThreadMessage(ThreadID2, WM_BUF, integer(Buf), NumRead);
     until (NumRead = 0);

     CloseFile(F);
    end;

    constructor TFileWrite.Create(Handle: THandle; const Name: string);
    begin
     MainHandle:= Handle;
     FName     := Name;
     inherited Create(false);
    end;

    procedure TFileWrite.Execute;
    var
     Buf: PChar;
     NumWritten, Count: integer;
     Msg: TMsg;
    begin
     AssignFile(F, FName);
     Rewrite(F, 1);

     Count:= 0;
     while GetMessage(Msg, 0, 0, 0) do begin
       DispatchMessage(Msg);

       Buf  := Pointer(Msg.wParam);
       Count:= Count + Msg.lParam;
       BlockWrite(F, Buf^, Msg.lParam, NumWritten);
       FreeMem(Buf);

       PostMessage(MainHandle, WM_WRITE, Count, 0);
     end;

     CloseFile(F);
    end;

    procedure TForm1.WMWrite(var Msg: TMessage);
    begin
     Gauge1.Progress:= Msg.WParam;
     Button1.Enabled:= Gauge1.Progress = Gauge1.MaxValue;
    end;

    procedure TForm1.Button1Click(Sender: TObject);
    var
     FileWrite: TFileWrite;
    begin
     with TFileStream.Create(Edit1.Text, fmOpenRead) do
     try
       Gauge1.MaxValue:= Size;
     finally
       Free;
     end;
     Button1.Enabled:= false;

     FileWrite:= TFileWrite.Create(Handle, Edit2.Text);
     TFileRead.Create(FileWrite.ThreadID, Edit1.Text);
    end;

    end.

  • sniknik © (01.05.12 23:05) [8]
    кстати в цикле второго потока (или вообще) ошибка... он не завершается. исправь.
  • Sha © (02.05.12 02:08) [9]
  • PacMan © (02.05.12 10:24) [10]

    > Это ты сам придумал или препод навязал тебе именно такую
    > логику ?


    на самом деле задание звучит так:
    "Разработать компонент, реализующий копирование файлов, при помощи тройной буферизации. Программа должна использовать потоки. Первый поток считывает блок данных из указанного файла. Второй поток записывает этот блок данных в файл. Буферизацию реализовать из 2-х массивов, состоящих из 3-х элементов. Каждый элемент - это буфер. Первый массив отвечает за чтение из файла, а второй за запись. Первый поток получает имя файла, берет пустой элемент из первого массива, записывает в него блок данных и передает второму массиву этот блок данных. При этом буфер, в котором считали блок данных, обнуляется. Второй поток, видя, что во втором массиве есть полный буфер, записывает этот буфер в файл и обнуляет его. При этом, в первом массиве выделяется новый буфер. И так пока весь файл не будет скопирован"

    !НО! я даже малейшего представления не имею как это реализовать. Вот и решил упростить "слегка".

    если у кого-нибудь есть идеи как это сделать, или еще лучше исходники... буду просто счастлив))
    надеюсь не сильно запутал.
  • PacMan © (02.05.12 10:30) [11]
    Есть коммерческое предложение, т.к. сам это все равно не напишу.
    Если кто желает заморочиться по заданию, договоримся о цене.
    Кто надумает, пишите в мыло yrungxay@mail.ru
  • Sha © (02.05.12 11:36) [12]
    > PacMan ©   (02.05.12 10:24) [10]

    Формулировка ужасная. Это точно оригинальный текст "от препода"?
  • PacMan © (02.05.12 11:52) [13]
    ага)
    это сфотографировано лично)
  • Cobalt © (02.05.12 11:54) [14]
    Имитация обработки - один поток откуда-то получает данные, складирует их в буфер.
    Второй поток - мониторит поступлени данных, и записывает их в файл, например, в базу данных.
    Вполне жизненная ситуация.
  • Sha © (02.05.12 12:57) [15]
    > PacMan ©   (02.05.12 10:24) [10]
    > при помощи тройной буферизации.

    обычно говорят буферизации чего, какой операции

    > Программа должна использовать потоки

    ну раз надо, значит, надо

    > Буферизацию реализовать из 2-х массивов, состоящих из 3-х элементов.
    всего 3 или 3 в каждом массиве? элементы - это что? обычно указатели

    > Первый массив отвечает за чтение из файла, а второй за запись.

    переменная ни за что никому не отвечает

    > Первый поток получает имя файла,
    > берет пустой элемент из первого массива,
    > записывает в него блок данных..

    значит, элемент - не указатель?

    > и передает второму массиву этот блок данных.

    передать переменной? она же памятник
    копирование данных или указателей?
    кто об этом и как узнает?
    а если некуда передавать? табличка "занято" 3 раза?

    > При этом буфер, в котором считали блок данных, обнуляется.

    нафига? если в нем были данные - то это бред
    может, указатель, хотя это тоже не оптимал

    > Второй поток, видя, что во втором массиве есть полный буфер,

    просто песня! КАК он это видит?

    > записывает этот буфер в файл и обнуляет его.

    опять же нафига?
    опять же, если видит, но не может? типа еще е все доделал

    > При этом, в первом массиве выделяется новый буфер.

    вроде мы не удаляли ничего,
    в таком случае лимит в 3 буфера будет превышен!

    > Cobalt

    Конечно, можно мониторить поступление телефонного звонка,
    но, вероятно, намного проще брать трубку по звонку.
  • Anatoly Podgoretsky © (02.05.12 13:43) [16]
    > Sha  (02.05.2012 12:57:15)  [15]

    Это тест на испытание копипастеров, не выживет так не выживет.
  • PacMan © (02.05.12 19:41) [17]

    > Sha ©   (02.05.12 12:57) [15]


    Это текст задания слово в слово.
    Причем сама препод не может ни чего пояснить по нему.
    Спрашиваю из элементов какого типа состоят массивы(массив байт или строки или еще что)? Отвечает, что как мне удобнее пусть так и будет, а про копирование вообще ни чего сказать не может.
    как-то так.
  • Сергей М. © (02.05.12 20:44) [18]

    > сама


    Это многое что объясняет)
  • Андреевич (02.05.12 21:13) [19]

    > там на самом деле гинеальная идея заложена :)
    > типа пока один поток пишет первый кусок,другой читает второй
    > и тд
    > но она имеет смысл только при наличии 2х жестких дисков
    > , если источник на одном а приемник на другом

    или если источники типа \\server\share\file.txt
  • Anatoly Podgoretsky © (02.05.12 21:36) [20]
    > PacMan  (02.05.2012 19:41:17)  [17]

    А что ты ей плохого сделал?
  • Сергей М. © (02.05.12 22:43) [21]

    > Anatoly Podgoretsky ©   (02.05.12 21:36) [20]


    А может просто не сделал, притом что должен был сделать ?
    imho, сей вариант наиболее вероятен)
  • Sha © (03.05.12 01:20) [22]
    > PacMan ©

    Почитай про кольцевой буфер клавиатуры,
    прерывания 9 и 16, можно поверхностно.
    Изобрази ей нечто похожее.
  • Германн © (03.05.12 01:54) [23]

    > Sha ©   (03.05.12 01:20) [22]

    Ностальгия?
  • han_malign (03.05.12 09:07) [24]

    > Почитай про кольцевой буфер клавиатуры

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

    По сути(технический перевод):
    1. есть пул блоков чтения и пул блоков записи фиксированной максимальной глубины...
    2. пул чтения изначально заполнен указателями на блоки буфера(скажем размера 64K)
    3. поток чтения выбирает блок из пула чтения, заполняет его и помещает в пул записи.
    4. поток записи выбирает блок из пула записи, обрабатывает - и помещает в пул чтения.
    5. соответственно, если в пуле нет блоков, а "процесс" не завершен - поток приостанавливается.

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

    А по делу - это делается двумя счетчиками прочитанных/записанных данных - раздельный доступ к частям буфера обеспечивается счетчиками:
    cbR += read(pb[cbR mod sizeof(pb)], min(MAX_BLOCK,sizeof(pb) - (cbR-cbW)));
    cbW += write(pb[cbW mod sizeof(pb)], min(MAX_BLOCK,cbR-cbW));



    З.Ы. Кстати - в этом варианте уже посчитан последний блок неполного размера...

    З.З.Ы. И никакой тройной буферизации нет, буфер в рамках одного уровня одного тракта - один, хотя и может состоять из произвольного числа блоков(т.н. "FIFO-половинки")...
  • PacMan © (03.05.12 09:37) [25]

    > Anatoly Podgoretsky ©   (02.05.12 21:36) [20]

    хотел другое задание взять... сказала: "Нет, оно не интересное, я тебе вот это дам..."
    и вот что заимел(
  • Медвежонок Пятачок © (03.05.12 09:44) [26]
    она на тебя просто запала. запишись на дополнительное индивидуальное занятие. вечернее.
  • sniknik © (03.05.12 09:45) [27]
    > и вот что заимел(
    заслуженная двойка по предмету... даже видя перед собой решение, не понимаешь, что это такое.
  • PacMan © (03.05.12 11:13) [28]
    ))) Решение вижу, понимаю алгоритм... не понимаю как реализовать.
    В  моем примере 1-й поток начинает работать, но проходит только 1 круг, из-за того что 2-й поток не хочет работать.
  • sniknik © (03.05.12 12:01) [29]
    > ))) Решение вижу, понимаю алгоритм... не понимаю как реализовать.
    угадал все буквы, не смог назвать слово... это нельзя назвать "понимаю".

    > В  моем примере 1-й поток начинает работать, но проходит только 1 круг, из-за того что 2-й поток не хочет работать.
    твой пример, это набор функций почти каждая из которых "не в том месте и не в то время".

    вот что ты сделал по поводу
    > и кстати у тебя куча всего чего нельзя в потоках... типа прямого обращения к vcl.
    ?
    а ведь это может приводить к блокировкам, "локированию" потока. ... не думаю что ты "нарвался" именно на это (более вероятно кривая логика), но рассматривать код в котором такие очевидные глюки вряд ли кто будет (я точно нет).
  • Медвежонок Пятачок © (03.05.12 12:21) [30]
    стартуем два потока.
    первый распределяет буфер и читает файл.
    заполнив буфер, первый поток посылает средмессадж второму и сообщает адрес и длину буфера.
    второй поток вынимает данные буфера в файл, затем грохает буфер и усыпает.
    первый поток, закончив с одним буфером, распределяет второй и снова оповещает второй поток о готовой порции данных.
    второй поток просыпается и повторяет операцию.
    после того, как файл прочитан целиком, первый поток оповещает второй, что можно закругляться и идти пить пиво.
  • Sha © (03.05.12 12:23) [31]
    > han_malign   (03.05.12 09:07) [24]

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

    Если уж делать по теории,
    то поток чтения занимается только чтением и ничего не знает о потоке записи,
    а поток записи занимается только записью и ничего не знает о потоке чтения.
  • sniknik © (03.05.12 12:48) [32]
    > Если уж делать по теории,
    > то поток чтения занимается только чтением и ничего не знает о потоке записи,
    > а поток записи занимается только записью и ничего не знает о потоке чтения.
    посмотри код в [7], так и сделано.
    условие завершения второго потока только забыл, 1 строка, после решил оставить это автору топика.
  • Sha © (03.05.12 14:01) [33]
    > sniknik ©   (03.05.12 12:48) [32]

    сразу бросилось в глаза,
    что код [7] легко исчерпает память при быстром чтении и медленной записи
  • han_malign (03.05.12 14:10) [34]

    > что код [7] легко исчерпает память при быстром чтении и медленной записи

    - и будет утечка, если PostThreadMessage произойдет раньше оппозитного GetMessage...
  • Медвежонок Пятачок © (03.05.12 14:19) [35]
    что код [7] легко исчерпает память при быстром чтении и медленной записи

    какая разница, если даже это реально произойдет?
    это же абстрактное дурацкое учебное задание ради самого задания.
  • Anatoly Podgoretsky © (03.05.12 14:23) [36]
    А может от него надо избавиться, тогда задание предстает совсем в другом свете.
  • sniknik © (03.05.12 14:26) [37]
    > что код [7] легко исчерпает память при быстром чтении и медленной записи
    естественно. но цель примера не в "копипасте", как понял задача учебная.
    а сделать счетчик/ограничитель на например 5 буферов, не проблема...

    > - и будет утечка, если PostThreadMessage произойдет раньше оппозитного GetMessage...
    с чего это оно произойдет раньше? первый поток только создается когда у второго уже ThreadID есть, т.е. уже "крутится".
  • sniknik © (03.05.12 14:28) [38]
    > это же абстрактное дурацкое учебное задание ради самого задания.
    +1
    на понимание. которого автор якобы достиг ([28]) но почему то все одно "не получается".
  • sniknik © (03.05.12 14:35) [39]
    >> - и будет утечка, если PostThreadMessage произойдет раньше оппозитного GetMessage...
    > с чего это оно произойдет раньше? первый поток только создается когда у второго уже ThreadID есть, т.е. уже "крутится".
    или имеется ввиду промежуток между стартом, и началом ожидания, там где пара команд открытия файла (если задержка), ну и что там дельфя своего добавляет?
    в этом случае событие будет ждать в очереди, пока его явно из нее не выкинут. т.е. однозначно дождется цикла.
  • Вариант (03.05.12 15:03) [40]

    > sniknik ©   (03.05.12 14:35) [39]


    >  с чего это оно произойдет раньше? первый поток только создается
    > когда у второго уже ThreadID есть, т.е. уже "крутится"

    Может начать крутиться и быть остановленным системой, не дойдя до первого вызова GetMessage. В этом случае еще не будет очереди сообщений потока, тогда PostThreadNessage вернет fails. Вероятность этого не нулевая, хотя и повторяемость события не очень большая (если не использовать например отладчик для эмуляции), сам натыкался на такое, хотя первой командой в Execute потока был PeekMessage, просто поток был остановлен планировщиком. Поэтому было бы неплохо после PeekMessage  сигналить в запускающий поток, что мой поток готов принимать сообщения и только потом уже разрешать слать ему сообщения.....  ну  или вариант по MSDN сценарию...
  • Вариант (03.05.12 15:11) [41]
    А автору поста посоветовал бы Рихтера с Кларком почитать, "Программирование серверных приложений для Windows 2000". Там готовый код на си есть копирования файлов с использованием потоков. Правда придется разбираться и читать, и пробовать и на дельфи перетаскивать. Но зато интересно.
  • PacMan © (04.05.12 11:12) [42]
    Ладно, всем спасибо, буду тыкаться. Посмотрим что выйдет.
  • Медвежонок Пятачок © (04.05.12 11:13) [43]
    смотря чем тыкаться.
    если головой, то она первой и выйдет с другой стороны.
 
Конференция "Начинающим" » TThread копирование файла в 2-х потоках [D7]
Есть новые Нет новых   [134464   +62][b:0][p:0.002]