Đây là một vấn đề rất phức tạp liên quan đến Thư viện Tác vụ. Tóm lại, có quá nhiều tác vụ được tạo và lên lịch để một trong những tác vụ mà trình điều khiển của MongoDB đang chờ sẽ không thể hoàn thành. Tôi đã mất một thời gian rất dài để nhận ra đó không phải là bế tắc mặc dù có vẻ như vậy.
Đây là bước để tái tạo:
- Tải xuống mã nguồn của Trình điều khiển CSharp của MongoDB .
- Mở giải pháp đó và tạo một dự án bảng điều khiển bên trong và tham chiếu dự án trình điều khiển.
- Trong chức năng Chính, tạo System.Threading.Timer sẽ gọi TestTask đúng giờ. Đặt hẹn giờ để bắt đầu ngay lập tức một lần. Cuối cùng, hãy thêm Console.Read ().
- Trong TestTask, sử dụng vòng lặp for để tạo 300 nhiệm vụ bằng cách gọi Task.Factory.StartNew (DoOneThing). Thêm tất cả các nhiệm vụ đó vào danh sách và sử dụng Task.WaitAll để đợi tất cả chúng hoàn thành.
- Trong hàm DoOneThing, hãy tạo MongoClient và thực hiện một số truy vấn đơn giản.
- Bây giờ hãy chạy nó.
Điều này sẽ không thành công tại cùng một nơi bạn đã đề cập:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)
Nếu bạn đặt một số điểm ngắt, bạn sẽ biết rằng WaitForDescriptionChangedHelper đã tạo một tác vụ hết thời gian chờ. Sau đó, nó sẽ đợi bất kỳ một trong các tác vụ DescriptionUpdate hoặc tác vụ hết thời gian hoàn thành. Tuy nhiên, DescriptionUpdate không bao giờ xảy ra, nhưng tại sao?
Bây giờ, quay lại ví dụ của tôi, có một phần thú vị:Tôi đã bắt đầu hẹn giờ. Nếu bạn gọi TestTask trực tiếp, nó sẽ chạy mà không gặp bất kỳ sự cố nào. Bằng cách so sánh chúng với cửa sổ Nhiệm vụ của Visual Studio, bạn sẽ nhận thấy rằng phiên bản hẹn giờ sẽ tạo ra nhiều tác vụ hơn phiên bản không hẹn giờ. Hãy để tôi giải thích phần này một chút sau. Có một sự khác biệt quan trọng khác. Bạn cần thêm các dòng gỡ lỗi trong Cluster.cs
:
protected void UpdateClusterDescription(ClusterDescription newClusterDescription)
{
ClusterDescription oldClusterDescription = null;
TaskCompletionSource<bool> oldDescriptionChangedTaskCompletionSource = null;
Console.WriteLine($"Before UpdateClusterDescription {_descriptionChangedTaskCompletionSource?.Task.Id}, {_descriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
lock (_descriptionLock)
{
oldClusterDescription = _description;
_description = newClusterDescription;
oldDescriptionChangedTaskCompletionSource = _descriptionChangedTaskCompletionSource;
_descriptionChangedTaskCompletionSource = new TaskCompletionSource<bool>();
}
OnDescriptionChanged(oldClusterDescription, newClusterDescription);
Console.WriteLine($"Setting UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
oldDescriptionChangedTaskCompletionSource.TrySetResult(true);
Console.WriteLine($"Set UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
}
private void WaitForDescriptionChanged(IServerSelector selector, ClusterDescription description, Task descriptionChangedTask, TimeSpan timeout, CancellationToken cancellationToken)
{
using (var helper = new WaitForDescriptionChangedHelper(this, selector, description, descriptionChangedTask, timeout, cancellationToken))
{
Console.WriteLine($"Waiting {descriptionChangedTask?.Id}, {descriptionChangedTask?.GetHashCode().ToString("F8")}");
var index = Task.WaitAny(helper.Tasks);
helper.HandleCompletedTask(helper.Tasks[index]);
}
}
Bằng cách thêm những dòng này, bạn cũng sẽ phát hiện ra rằng phiên bản không hẹn giờ sẽ cập nhật hai lần nhưng phiên bản hẹn giờ sẽ chỉ cập nhật một lần. Và cái thứ hai đến từ "MonitorServerAsync" trong ServerMonitor.cs. Hóa ra, trong phiên bản hẹn giờ, MontiorServerAsync đã được thực thi nhưng sau khi nó được thực thi thông qua ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync và TcpStreamFactory.CreateStreamAsync, cuối cùng nó đã đến được với TcpStreamFactory. Điều tồi tệ xảy ra ở đây:Dns.GetHostAddressesAsync
. Cái này không bao giờ được thực thi. Nếu bạn sửa đổi một chút mã và biến nó thành:
var task = Dns.GetHostAddressesAsync(dnsInitial.Host).ConfigureAwait(false);
return (await task)
.Select(x => new IPEndPoint(x, dnsInitial.Port))
.OrderBy(x => x, new PreferredAddressFamilyComparer(preferred))
.ToArray();
Bạn sẽ có thể tìm thấy id nhiệm vụ. Bằng cách nhìn vào cửa sổ Nhiệm vụ của Visual Studio, rõ ràng là có khoảng 300 nhiệm vụ phía trước nó. Chỉ một số trong số họ đang thực thi nhưng bị chặn. Nếu bạn thêm Console.Writeline trong chức năng DoOneThing, bạn sẽ thấy rằng bộ lập lịch tác vụ bắt đầu một vài trong số chúng gần như cùng một lúc nhưng sau đó, nó chậm lại khoảng một lần mỗi giây. Vì vậy, điều này có nghĩa là, bạn cần đợi khoảng 300 giây trước khi tác vụ giải quyết các dns bắt đầu chạy. Đó là lý do tại sao nó vượt quá thời gian chờ 30 giây.
Bây giờ, đây là một giải pháp nhanh chóng nếu bạn không làm những điều điên rồ:
Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);
Điều này sẽ buộc ThreadPoolScheduler bắt đầu một chuỗi ngay lập tức thay vì đợi một giây trước khi tạo một luồng mới.
Tuy nhiên, điều này sẽ không hiệu quả nếu bạn đang làm điều thực sự điên rồ như tôi. Hãy thay đổi vòng lặp for từ 300 thành 30000, ngay cả giải pháp này cũng có thể thất bại. Lý do là nó tạo ra quá nhiều luồng. Đây là nguồn lực và thời gian. Và nó có thể bắt đầu khởi động quy trình GC. Tất cả cùng nhau, nó có thể không thể hoàn thành việc tạo tất cả các chuỗi đó trước khi hết thời gian.
Cách hoàn hảo là ngừng tạo nhiều tác vụ và sử dụng bộ lập lịch mặc định để lập lịch cho chúng. Bạn có thể thử tạo mục công việc và đặt nó vào ConcurrentQueue, sau đó tạo một số chuỗi làm công nhân để tiêu thụ các mục.
Tuy nhiên, nếu không muốn thay đổi cấu trúc ban đầu quá nhiều, bạn có thể thử cách sau:
Tạo một ThrottledTaskScheduler bắt nguồn từ TaskScheduler.
- ThrottledTaskScheduler này chấp nhận TaskScheduler làm cơ sở sẽ chạy tác vụ thực tế.
- Đưa các tác vụ vào bộ lập lịch bên dưới nhưng nếu nó vượt quá giới hạn, hãy đặt nó vào hàng đợi thay thế.
- Nếu bất kỳ nhiệm vụ nào đã hoàn thành, hãy kiểm tra hàng đợi và cố gắng chuyển chúng vào bộ lập lịch bên dưới trong giới hạn cho phép.
- Sử dụng mã sau để bắt đầu tất cả các nhiệm vụ mới thú vị đó:
·
var taskScheduler = new ThrottledTaskScheduler(
TaskScheduler.Default,
128,
TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler,
logger
);
var taskFactory = new TaskFactory(taskScheduler);
for (var i = 0; i < 30000; i++)
{
tasks.Add(taskFactory.StartNew(DoOneThing))
}
Task.WaitAll(tasks.ToArray());
Bạn có thể lấy System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler làm tham chiếu. Nó phức tạp hơn một chút so với thứ chúng ta cần. Nó cho một số mục đích khác. Vì vậy, đừng lo lắng về những phần đi qua lại với hàm bên trong lớp ConcurrentExclusiveSchedulerPair. Tuy nhiên, bạn không thể sử dụng nó trực tiếp vì nó không vượt qua TaskCreationOptions.LongRunning khi tạo tác vụ gói.
Nó làm việc cho tôi. Chúc bạn thành công!
Tái bút:Lý do có nhiều tác vụ trong phiên bản hẹn giờ có thể nằm bên trong TaskScheduler.TryExecuteTaskInline. Nếu nó nằm trong luồng chính nơi ThreadPool được tạo, nó sẽ có thể thực thi một số tác vụ mà không cần đưa chúng vào hàng đợi.