StackOverflow при использовании IProgress

Переписываю библиотеку скачивалки. Для форума код немного сократил, но суть такая:

        public int ContentToStream(Stream stream, int bufferSize,
            IProgress<long> progressReporter, CancellationToken cancellationToken)
        {
            long bytesTransfered = 0L;
            byte[] buf = new byte[bufferSize];
            do
            {
                int bytesRead = ContentData.Read(buf, 0, buf.Length);
                if (bytesRead <= 0)
                {
                    break;
                }
                stream.Write(buf, 0, bytesRead);
                bytesTransfered += bytesRead;
                progressReporter?.Report(bytesTransfered);
            }
            while (!cancellationToken.IsCancellationRequested);

            if (cancellationToken.IsCancellationRequested)
            {
                return FileDownloader.DOWNLOAD_ERROR_CANCELED_BY_USER;
            }
            else if (ContentLength >= 0L && bytesTransfered != ContentLength)
            {
                return FileDownloader.DOWNLOAD_ERROR_INCOMPLETE_DATA_READ;
            }

            return 200;
        }
    public sealed class FileDownloader
    {
        public string Url { get; set; }
 
        public const int DOWNLOAD_ERROR_URL_NOT_DEFINED = -1;
        public const int DOWNLOAD_ERROR_INVALID_URL = -2;
        public const int DOWNLOAD_ERROR_CANCELED_BY_USER = -3;
        public const int DOWNLOAD_ERROR_INCOMPLETE_DATA_READ = -4;
        public const int DOWNLOAD_ERROR_RANGE = -5;
        public const int DOWNLOAD_ERROR_ZERO_LENGTH_CONTENT = -6;
        public const int DOWNLOAD_ERROR_INSUFFICIENT_DISK_SPACE = -7;
        public const int DOWNLOAD_ERROR_DRIVE_NOT_READY = -8;
        public const int DOWNLOAD_ERROR_NULL_CONTENT = -9;

        public delegate void ConnectingDelegate(object sender, string url);
        public delegate void ConnectedDelegate(object sender, string url, HttpRequestResult requestResult, ref int errorCode);
        public delegate void WorkStartedDelegate(object sender, long contentLength);
        public delegate void WorkProgressDelegate(object sender, long bytesTransfered, long contentLength);
        public delegate void WorkFinishedDelegate(object sender, long bytesTransfered, long contentLength, int errorCode);
        public ConnectingDelegate Connecting;
        public ConnectedDelegate Connected;
        public WorkStartedDelegate WorkStarted;
        public WorkProgressDelegate WorkProgress;
        public WorkFinishedDelegate WorkFinished;
 
        public int Download(Stream stream)
        {
            Connecting?.Invoke(this, Url);

            HttpRequestResult requestResult = HttpRequestSender.Send("GET", Url, null, Headers);

            long size = requestResult.WebContent.ContentLength;

            WorkStarted?.Invoke(this, size);

            Progress<long> progress = new Progress<long>();
            progress.ProgressChanged += (s, n) =>
            {
                WorkProgress?.Invoke(this, n, size);
            };
            IProgress<long> reporter = progress;
			
            try
            {
                LastErrorCode = requestResult.WebContent.ContentToStream(
                    stream, 99999, reporter, default);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
                LastErrorMessage = ex.Message;
                LastErrorCode = ex.HResult;
                return LastErrorCode;
            }
            requestResult.Dispose();

            WorkFinished?.Invoke(this, _bytesTransfered, size, LastErrorCode);

            return LastErrorCode;
        }
            downloader.WorkProgress += (s, bytesTransfered, contentLength) =>
            {
                if (contentLength > 0L)
                {
                    double percent = 100.0 / contentLength * bytesTransfered;
                    progressBar1.Value = (int)percent;
                    string percentFormatted = string.Format("{0:F3}", percent);
                    lblDownloadingProgress.Text = $"Скачано {bytesTransfered} из {contentLength} ({percentFormatted}%)";
                }
                else
                {
                    lblDownloadingProgress.Text = $"Скачано {bytesTransfered} из <Неизвестно>";
                }

                Application.DoEvents();
            };

Если использую IProgress, то на строчке lblDownloadingProgress.Text = $"Скачано {bytesTransfered} из {contentLength} ({percentFormatted}%)"; через какое-то время возникает StackOverflow. При этом, данные на форме вообще не обновляются, но файл скачивается правильно. Если поставить точку останова на строчку Application.DoEvents(), то дебаггер падает на неё только через какое-то время, а не сразу. И после этого через несколько итераций StackOverflow. Всё это выполняется в одном потоке с формой.
Наверное, нифига не понятно. Задавайте вопросы :man_shrugging:

А зачем тут DoEvents?
Оно ж и так выполнится и дальше основной поток свободен.

Так а в стеке-то что?

Как он станет свободен, если там идёт цикл скачивания? :thinking:

MultiThreadedDownloaderLib.GuiTest.exe!MultiThreadedDownloaderLib.GuiTest.Form1.btnDownloadSingleThreaded_Click.AnonymousMethod__3(object s, long bytesTransfered, long contentLength) Строка 192	C#
MultiThreadedDownloader.dll!MultiThreadedDownloaderLib.FileDownloader.Download.AnonymousMethod__0(object s, long n) Строка 93	C#

:confusedparrot: а почему он там? И зачем тогда Invoke?

:thinking: Я думал, это просто его вызов. Сейчас пытаюсь загуглить, что делает Invoke у делегата, но чёт не гуглится :man_shrugging:
То есть, если я хочу использовать IProgress, то цикл обязательно должен быть в другом потоке?

А, если у делегата, то да.

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

Но тут может и не в нем дело, стектрейс странный, надо включить показ “внешнего кода” и смотреть что там между ними.

Ну и вообще надо делать нормально с потоками без DoEvents.

Да, я уже понял. Мог бы и сам догадаться :man_shrugging: Всё логично же.