Многопоточная ошибка, не попавшая в ловушку

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

static void Main(string[] args)
{
    try
    {
        string filePath = ConfigurationManager.AppSettings["filePath"],
            folder = ConfigurationManager.AppSettings["folder"];
        Directory.CreateDirectory(folder);
        List<string> urls = File.ReadAllLines(filePath).Take(10000).ToList();

        int urlIX = -1;
        Task.WaitAll(Enumerable.Range(0, 50).Select(x => Task.Factory.StartNew(() =>
          {
              while (true)
              {
                  int curUrlIX = Interlocked.Increment(ref urlIX);
                  if (curUrlIX >= urls.Count)
                      break;
                  string url = urls[curUrlIX];
                  try
                  {
                      var req = (HttpWebRequest)WebRequest.Create(url);
                      using (var res = (HttpWebResponse)req.GetResponse())
                      using (var resStream = res.GetResponseStream())
                      using (var fileStream = File.Create(Path.Combine(folder, Guid.NewGuid() + url.Substring(url.LastIndexOf('.')))))
                          resStream.CopyTo(fileStream);
                  }
                  catch (Exception ex)
                  {
                      Console.WriteLine("Error downloading img: " + url + "\n" + ex);
                      continue;
                  }
              }
          })).ToArray());
    }
    catch
    {
        Console.WriteLine("Something bad happened.");
    }
}

На моем локальном компьютере все работает отлично. На сервере после загрузки нескольких сотен изображений отображается ошибка либо Attempted to read or write protected memory, либо Unable to read data from the transport connection: A blocking operation was interrupted by a call to WSACancelBlockingCall..

Кажется, это естественная ошибка, потому что ни внутренняя, ни внешняя фиксация не поймают ее. Я никогда не вижу Something bad happened..

Я запустил его в WinDbg, и он показал следующее:

(3200.1790): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
LavasoftTcpService64+0x765f:
00000001`8000765f 807a1900        cmp     byte ptr [rdx+19h],0 ds:baadf00d`0000001a=??
0:006> g
(3200.326c): CLR exception - code e0434352 (first chance)
(3200.326c): CLR exception - code e0434352 (first chance)
(3200.2b9c): Access violation - code c0000005 (!!! second chance !!!)
LavasoftTcpService64!WSPStartup+0x9749:
00000001`8002c8b9 f3a4            rep movs byte ptr [rdi],byte ptr [rsi]

Я просто отключил Lavasoft, и теперь WinDbg показывает это:

Critical error detected c0000374
(3c4.3494): Break instruction exception - code 80000003 (first chance)
ntdll!RtlReportCriticalFailure+0x4b:
00007fff`4acf1b2f cc              int     3
0:006> g
(3c4.3494): Unknown exception - code c0000374 (first chance)
(3c4.3494): Unknown exception - code c0000374 (!!! second chance !!!)
ntdll!RtlReportCriticalFailure+0x8c:
00007fff`4acf1b70 eb00            jmp     ntdll!RtlReportCriticalFailure+0x8e (00007fff`4acf1b72)
0:006> g
WARNING: Continuing a non-continuable exception
(3c4.3494): C++ EH exception - code e06d7363 (first chance)
HEAP[VIPJobsTest.exe]: HEAP: Free Heap block 0000007AB96CC5D0 modified at 0000007AB96CC748 after it was freed
(3c4.3494): Break instruction exception - code 80000003 (first chance)
ntdll!RtlpBreakPointHeap+0x1d:
00007fff`4acf3991 cc              int     3

Ответ 1

В конце концов, проблема была в Lavasoft Web Companion. Несмотря на то, что я отключил его, все еще было что-то от него, работающее на заднем плане. Удалив его, исправлена ​​проблема.

Ответ 2

Ваше исключение не бросается, потому что вы, ну, не пытаетесь его получить. WaitAll метод - это в основном Barrier, который ждет (haha) для завершения всех задач. Это void, поэтому вам нужно сохранить ссылку для своих задач для последующих действий, например:

var tasks = Enumerable.Range(0, 50).Select(x => Task.Factory.StartNew(() =>
{
    while (true)
    {
        // ..
        try
        {
            // ..
        }
        catch (Exception ex)
        {
            // ..
        }
    }
})).ToArray();

Task.WaitAl((tasks);

// investigate exceptions here
var faulted = tasks.Where(t => t.IsFaulted);

Согласно MSDN, исключения распространяются при использовании одного из статических или экземпляров Task.Wait или Task<TResult>.Wait, или .Result. Однако это не вариант для вас, поскольку вы используете try/catch здесь. Поэтому вам нужно подписаться на TaskScheduler.UnobservedTaskException событие:

TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
    Console.WriteLine("Error." + e);
    e.SetObserved();
}

Почему он запускается без металирования?

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

Чтобы разработчики могли писать асинхронный код на основе задач, .NET Framework 4.5 изменяет поведение исключения по умолчанию для незаметных исключений. Хотя незаметные исключения по-прежнему вызывают исключение UnobservedTaskException, , процесс по умолчанию не завершается. Вместо этого исключение обрабатывается средой выполнения после возникновения события, независимо от того, наблюдает ли обработчик события исключение. Такое поведение можно настроить. Начиная с .NET Framework 4.5, вы можете использовать элемент конфигурации, чтобы вернуться к поведению .NET Framework 4 и завершить процесс:

<configuration> 
 <runtime> 
  <ThrowUnobservedTaskExceptions enabled="true"/> 
 </runtime> 
</configuration>

Теперь вернемся к вашему коду. Рассмотрите возможность использования статического экземпляра HttpClient вместо HttpWebRequest, поскольку вам просто нужна строка результата. Этот класс был разработан для использования в многопоточном коде, поэтому методы являются потокобезопасными.

Кроме того, вы должны предоставить TaskCreationOptions.LongRunning (который опасен, кстати, но он вам еще нужен):

Указывает, что задача будет длительной, крупнозернистой операцией, включающей менее крупные компоненты, чем мелкозернистые системы. Он дает подсказку TaskScheduler, что может потребоваться превышение подписки.

Oversubscription позволяет создавать больше потоков, чем доступное количество аппаратных потоков. Он также дает подсказку планировщику задач, что для задачи может потребоваться дополнительный поток, чтобы он не блокировал продвижение вперед других потоков или рабочих элементов в локальной очереди пулов потоков.

Ответ 3

Вы можете добавить продолжение в свою задачу.

 Task.Factory.StartNew(() => ...)
    .ContinueWith (task => 
    {
       If (task.isFaulted)
       {
           //task.Exception
           //handle the exception from this context
       }
    });

Ответ 4

В вашем примере есть код, который фактически не защищен try/catch. Ошибка в том, что в потоке было бы исключено неперехваченное исключение.

Вот рефактор (комментарии по ранее незащищенному коду). Внешняя попытка не поймает их, как в исходной нити. Поэтому он будет необработанным на подписях

static void Main(string[] args)
{
    try
    {
        string filePath = ConfigurationManager.AppSettings["filePath"],
            folder = ConfigurationManager.AppSettings["folder"];
        Directory.CreateDirectory(folder);
        List<string> urls = File.ReadAllLines(filePath).Take(10000).ToList();

        int urlIX = -1;
        Task.WaitAll(Enumerable.Range(0, 50).Select(x => Task.Factory.StartNew(() =>
          {
              try
              {
                  while (true) // ** was unprotected
                  {
                      int curUrlIX = Interlocked.Increment(ref urlIX);  // ** was unprotected
                      if (curUrlIX >= urls.Count)   // ** was unprotected
                          break;                    // ** was unprotected
                      string url = urls[curUrlIX];  // ** was unprotected
                      try
                      {
                          var req = (HttpWebRequest)WebRequest.Create(url);
                          using (var res = (HttpWebResponse)req.GetResponse())
                          using (var resStream = res.GetResponseStream())
                          using (var fileStream = File.Create(Path.Combine                    (folder, Guid.NewGuid() + url.Substring(url.LastIndexOf('.')))))
                              resStream.CopyTo(fileStream);
                      }
                      catch (Exception ex)
                      {
                          Console.WriteLine("Error downloading img: " + url + "\n" + ex);
                          continue;
                      }
                  } // while
              } // try
          })).ToArray());
    }
    catch
    {
        Console.WriteLine("Something bad happened.");
    }
}