背景
公司做Telegram開發,.net Framework專案,呼叫TLSharp作為框架進行開發。
開發需求是讀取群裡新到達的資訊並進行過濾。
由此不可避免得要用到
TLSharp.Core.TelegramClient.GetHistoryAsync(TLAbsInputPeer peer, int offsetId = 0, int offsetDate = 0, int addOffset = 0, int limit = 100, int maxId = 0, int minId = 0, CancellationToken token = default);
這一方法。
由於每次都只能取得一個群的聊天曆史記錄,顯然地在讀取群列表之後第一想到地就是用linq
(await Listener.Client.SendRequestAsync<TLChats>(new TLRequestGetAllChats()
{ ExceptIds = new TeleSharp.TL.TLVector<int>() })
.ConfigureAwait(false))
.Chats
.Where(item => item.GetType() == typeof(TLChannel))
.Cast<TLChannel>()
.ToList()
.ForEach(async item =>
{
((TLChannelMessages)await Listener.Client.GetHistoryAsync(
peer: new TLInputPeerChannel()
{
ChannelId = item.Id,
AccessHash = item.AccessHash.Value
}))
.Messages
.Where(subitem =>
subitem.GetType() == typeof(TLMessage))
.Cast<TLMessage>()
.Where(subitem =>
(subitem.Entities == null
|| (subitem.Entities != null && subitem.Entities.Count() < 5))
&& !string.IsNullOrWhiteSpace(subitem.Message))
.ToList()
.ForEach(subitem =>
{
//實際處理訊息
});
});
但是很意外的,跑掛了!
報出的原因是session.dat檔案被佔用。
探索
session.dat檔案是TG的訊息會話檔案,受TLSharp管控,因此不能自主去管理檔案的開啟關閉和釋放。
於是抱著試一試的心理,把非同步去掉了,再跑起來,還是一樣的錯誤。
難道是Linq的問題?還是因為沒有加ConfigAwait(false)?
這個框架下試了幾次,均報session.dat被佔用。
於是用foreach改寫了這一段:
List<TLChannel> AllGroups = (await Listener.Client.SendRequestAsync<TLChats>(new TLRequestGetAllChats()
{ ExceptIds = new TeleSharp.TL.TLVector<int>() })
.ConfigureAwait(false))
.Chats
.Where(item => item.GetType() == typeof(TLChannel))
.Cast<TLChannel>()
.ToList();
foreach (TLChannel item in AllGroups)
{
((TLChannelMessages)await Listener.Client.GetHistoryAsync(
peer: new TLInputPeerChannel()
{
ChannelId = item.Id,
AccessHash = item.AccessHash.Value
}))
.Messages
.Where(subitem =>
subitem.GetType() == typeof(TLMessage))
.Cast<TLMessage>()
.Where(subitem =>
(subitem.Entities == null
|| (subitem.Entities != null && subitem.Entities.Count() < 5))
&& !string.IsNullOrWhiteSpace(subitem.Message))
.ToList()
.ForEach(subitem =>
{
//實際處理訊息
});
};
繼續跑,繼續掛!!
然後其實又把foreach改成了for(;?,問題依舊!!!
解決
拆到for迴圈之後,因為方便斷點了,發現每次出問題都不是在第一個資料,很大概率也不是發生在第二個資料,一般都是第三個才開始報佔用錯誤,
這就帶來了思考的空間。
很顯然是迴圈體內的方法對session.dat的訪問有要求,而迴圈上一條還沒有結束,下一條就已經要訪問。
為了驗證這一點,手工用斷點停幾秒再執行,發現不報錯了!
這就更能說明問題了:
TLSharp中的方法使用了多執行緒對session.dat進行訪問,
而這些執行緒的行為不受控,在我方程式碼執行完之後,庫內程式碼並未執行完,
而我方程式碼在下一迴圈中又在庫內程式碼開啟了新的執行緒,要對該檔案進行使用,
在這個過程中,由於在迴圈體內使用了同一個變數來接收新值,自然就造成了我方程式碼執行完後該變數被作為垃圾回收,庫內執行緒存取出現檔案衝突的問題(無法寫入檔案),
從而報了這個錯。
為了驗證這個想法並使程式碼能跑起來,我把程式碼段複製了六個,接收的變數也改用陣列,發現能跑了。
於是最終把階段結果改用陣列儲存,成功解決問題:
List<TLChannel> AllGroups = (await Listener.Client.SendRequestAsync<TLChats>(new TLRequestGetAllChats() { ExceptIds = new TeleSharp.TL.TLVector<int>() }).ConfigureAwait(false)).Chats.Where(item => item.GetType() == typeof(TLChannel)).Cast<TLChannel>().ToList();
TLChannelMessages[] MessagesArray = new TLChannelMessages[AllGroups.Count];
for (int i = 0; i < AllGroups.Count; i++)
{
MessagesArray[i] = (TLChannelMessages)await Listener.Client.GetHistoryAsync(peer: new TLInputPeerChannel() { ChannelId = AllGroups[i].Id, AccessHash = AllGroups[i].AccessHash.Value });
MessagesArray[i].Messages.Where(item => item.GetType() == typeof(TLMessage)).Cast<TLMessage>().Where(item => (item.Entities == null || (item.Entities != null && item.Entities.Count() < 5)) && !string.IsNullOrWhiteSpace(item.Message)).ToList().ForEach(item =>
{
//實際處理訊息
});
}