c#事件的执行是线程安全的
本文关键字:线程 安全 执行 事件 | 更新日期: 2023-09-27 18:11:04
我读了很多关于事件和线程的讨论,但所有这些都集中在"如果我取消订阅事件并稍后尝试调用它会发生什么"上。我的问题不一样……如果线程a中有一个进程在毫秒1内触发事件"I finish",并且线程B中也有一个进程在毫秒2内触发事件"I finish",会发生什么?
两个进程都使用相同的方法来监听和处理事件。因此,c#必须执行处理事件的方法2次:1次用于线程A中触发的事件,1次用于线程b中触发的事件。
会发生什么??当"来自线程A的第一个事件"开始执行处理该事件的方法时,c#是否锁定该方法,并在完成执行时解锁该方法,从而允许其他等待的"事件"执行方法内容?
或者从线程A触发的事件将开始执行处理该事件的方法,并且1毫秒后从线程B触发的事件也将开始执行同一方法,而不会注意到当前该方法正在被其他"进程"执行?
我问这个,因为我想在捕获事件的方法中做一些文件写入,但如果该方法可以同时执行(取决于事件何时被触发),我想我不能在这里这样做,因为文件上的信息将是两个进程同时写入同一文件之间的混合(文件上的无效信息)。
我的代码看起来像这样(有点长,抱歉)。请注意,这将不会编译,只是一个示例,以显示我在做什么:
public partial class MainForm : Form
{
FTPClientManager client = null;
public MainForm()
{
InitializeComponent();
}
private void btnConnect_Click(object sender, EventArgs e)
{
Connect(this.tbFTPServer.Text.Trim());
this.lstLog.Items.Add("Connected"); //This lstLog is a list box that will have a list of downloaded files from all threads.
}
void Connect(string urlStr)
{
try {
client = new FTPClientManager();
//subscribe to the event
client.FileDownloadCompleted += new EventHandler<FileDownloadCompletedEventArgs>(client_FileDownloadCompleted);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
void client_FileDownloadCompleted(object sender, FileDownloadCompletedEventArgs e)
{
this.Invoke(new EventHandler<FileDownloadCompletedEventArgs>(
client_FileDownloadCompletedHandler), sender, e);
}
void client_FileDownloadCompletedHandler(object sender, FileDownloadCompletedEventArgs e)
{
string log = string.Format("{0} Instance {5} Download from {1} to {2} is completed. Length: {3} Time: {4}. ",
DateTime.Now, e.ServerPath, e.LocalFile.FullName, e.LocalFile.Length, e.DownloadTime, e.ftpInstance);
this.lstLog.Items.Add(log);
}
private void btnDownload_Click(object sender, EventArgs e)
{
client.DownloadFiles();
}
}
public class FTPClientManager {
FTPDownloadClient[] arrayDownloadClient = new FTPDownloadClient[2];
public event EventHandler<FileDownloadCompletedEventArgs> FileDownloadCompleted;
public void DownloadFiles()
{
for (int i = 0; i < 2; i++)
{
arrayDownloadClient[i] = new FTPDownloadClient();
//subscribe to the event. each instance of FTPDownloadClient will suscribe to the same event.
arrayDownloadClient[i].FileDownloadCompleted += new EventHandler<FileDownloadCompletedEventArgs>(downloadClient_FileDownloadCompleted);
}
//download one set of files in thread A
arrayDownloadClient[0].DownloadFiles(list_of_files_to_download);
//download another set of files in thread B
arrayDownloadClient[1].DownloadFiles(another_list_of_files_to_download);
}
//In theory, the method downloadClient_FileDownloadCompleted will be executed by any instance of FTPDownloadClient
//running in either thread A or thread B, whichever finish first downloading a file.
//My question comes in the execution of this method.
//Lets say the process in thread A finish downloading and fires the event.
//Lets say the process in thread B finish downloading 1 millisecond after thread A finish, so it also fires the event.
//how C# manage the execution of the downloadClient_FileDownloadCompleted??
//does the event coming from thread A will lock the method downloadClient_FileDownloadCompleted, execute it, and when finish execution unlock the method
//and allows the event coming from thread B start locking, processing, unlock ??
//Or the method will be executed "at the same time" (1 millisecond difference) by each event fired from thread A and thread B??
void downloadClient_FileDownloadCompleted(object sender, FileDownloadCompletedEventArgs e)
{
this.OnFileDownloadCompleted(e);
}
protected virtual void OnFileDownloadCompleted(FileDownloadCompletedEventArgs e)
{
if (FileDownloadCompleted != null)
{
//this will fire the event, so the main form will catch it
//again, this fire can be triggered from process in thread A or from process in thread B
FileDownloadCompleted(this, e);
}
}
}
public class FTPDownloadClient {
public event EventHandler<FileDownloadCompletedEventArgs>
FileDownloadCompleted;
public void DownloadFiles(string [] files_to_download)
{
ParameterizedThreadStart threadStart =
new ParameterizedThreadStart(StartDownloadFiles);
Thread downloadThread = new Thread(threadStart);
downloadThread.IsBackground = true;
downloadThread.Start(new object[] { files_to_donwload });
}
//This metod will download all the files in the list passed as parameter.
//Every file downloaded will raise the event FileDownloadComplete, so a message can be added to the lstlog on the main form
void StartDownloadFiles(object state)
{
var paras = state as object[];
string [] files = paras[0] as string [];
foreach (var file in files)
{
DownloadOneFile(file);
}
}
void DownloadFile(string onefile)
{
//Donwload file done here
var fileDownloadCompletedEventArgs = new FileDownloadCompletedEventArgs
{
LocalFile = new FileInfo(destPath),
ServerPath = onefile,
DownloadTime = fileDownloadTime.ElapsedMilliseconds.ToString()
};
this.OnFileDownloadCompleted(fileDownloadCompletedEventArgs);
}
protected virtual void OnFileDownloadCompleted(FileDownloadCompletedEventArgs e)
{
if (FileDownloadCompleted != null)
{
//the event is fired when the file being downloaded by this thread is finish.
//so, thread A will fire this event from its current thread
//and also thread B will fire the same event from its own thread.
FileDownloadCompleted(this, e);
}
}
}
c#不会为你做任何锁。如果事件可以由多个线程同时引发,你必须编写代码来处理它(如果必要的话)。
您可以使用lock
语句来防止多个线程执行它:
private void MyEventHandler(object sender, EventArgs e)
{
lock (lockingObject)
{
// Handle event here.
// Only one thread at a time can reach this code.
}
}
其中lockingObject
是类中的一个字段,声明如下:
private readonly object lockingObject = new object();
在引发事件的方法中,您还必须小心线程化。
假设你的类中有一个名为MyEvent
的事件。你应该而不是这样做:
private void RaiseMyEvent()
{
if (MyEvent != null) // {1}
MyEvent(this, new EventArgs()); // {2}
}
如果另一个线程可以从MyEvent
分离,那么它可能在行{1}和行{2}之间分离。如果发生这种情况,行{2}将抛出空引用异常,因为MyEvent
将突然变为空!
正确的方法是:
private void RaiseMyEvent()
{
var handler = MyEvent;
if (handler != null)
handler (this, new EventArgs());
}
现在null引用异常不会发生了
然而,请注意,当使用多个线程时,事件处理程序有可能在线程分离它之后被调用 !看起来你想使用锁。它可以防止不同的线程同时执行相同的代码。但是,不建议使用锁,大多数情况下应该避免使用锁,但它在您的情况下看起来不错。
Lock语句我喜欢微软给你的取款的例子
int Withdraw(int amount)
{
// This condition never is true unless the lock statement
// is commented out.
if (balance < 0)
{
throw new Exception("Negative Balance");
}
// Comment out the next line to see the effect of leaving out
// the lock keyword.
lock (thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : " + balance);
Console.WriteLine("Amount to Withdraw : -" + amount);
balance = balance - amount;
Console.WriteLine("Balance after Withdrawal : " + balance);
return amount;
}
else
{
return 0; // transaction rejected
}
}
}
似乎你已经使你的代码线程安全,因为你在你的方法client_FileDownloadCompleted
中使用Form.Invoke
。
事件FTPClientManager.FileDownloadCompleted
可能同时在不同的线程上触发,但Form.Invoke
将序列化每个回调到主UI线程。因此,在您的代码中,您不需要任何锁定,因为Form.Invoke
将采取它,client_FileDownloadCompletedHandler
将始终在您的UI线程上被调用。