我什么时候应该使用关键部分?

时间:2011-03-19 09:01:13

标签: multithreading delphi synchronization critical-section

这是交易。我的应用程序有很多线程可以做同样的事情 - 从大文件中读取特定数据(> 2gb),解析数据并最终写入该文件。

问题是,有时可能会发生一个线程从文件A读取X而第二个线程写入同一文件A的X.会出现问题?

I / O代码对每个文件使用TFileStream。我将I / O代码拆分为本地(静态类),因为我担心会出现问题。由于它是分裂的,应该有关键部分。

以下每个案例都是未实例化的本地(静态)代码。

案例1:

procedure Foo(obj:TObject);
begin ... end;

案例2:

procedure Bar(obj:TObject);
var i: integer;
begin
  for i:=0 to X do ...{something}
end;

案例3:

function Foo(obj:TObject; j:Integer):TSomeObject
var i:integer;
begin
  for i:=0 to X do
    for j:=0 to Y do
      Result:={something}
end;

问题1:在哪种情况下我需要关键部分,以便在> 1个线程同时调用它时没有问题?

问题2:如果线程1从文件A读取X(条目)而线程2写入X(条目)到文件A,是否会出现问题?

我应该何时使用关键部分?我试着把它想象成我的脑袋,但这很难 - 只有一个线程:))

修改

这是否适合它?

{每2GB文件的一个类}

TSpecificFile = class
  cs: TCriticalSection;
  ...
end;

TFileParser = class
  file :TSpecificFile;
  void Parsethis; void ParseThat....
end;

function Read(file: TSpecificFile): TSomeObject;
begin
  file.cs.Enter;
  try
    ...//read
  finally
    file.cs.Leave;
  end;
end;

function Write(file: TSpecificFile): TSomeObject;
begin
  file.cs.Enter;
  try
    //write
  finally
    file.cs.Leave
  end;
end;

如果两个线程调用Read with:

,那么现在会出现问题

案例1:相同的TSpecificFile

案例2:不同的TSpecificFile?

我还需要另一个关键部分吗?

3 个答案:

答案 0 :(得分:7)

通常,只要多个线程可以同时访问共享资源,并且至少有一个线程将写入/修改共享资源,您需要一个锁定机制(关键部分是锁定机制)。登记/> 无论资源是内存中的对象还是磁盘上的文件,都是如此 并且锁定是必要的原因是,如果读操作与写操作同时发生,则读操作可能获得不一致的数据,从而导致不可预测的行为。
Stephen Cheung在文件处理方面提到了平台特定的考虑因素,我在此不再赘述。

  

作为旁注,我想强调可能适用于您案例的另一个并发问题。

     
      
  • 假设一个线程读取一些数据并开始处理。
  •   
  • 然后另一个线程也这样做。
  •   
  • 两个线程都确定他们必须将结果写入文件A的位置X。
  •   
  • 最好写入的值是相同的,其中一个线程实际上没有做任何事情,只是浪费时间。
  •   
  • 最糟糕的是,其中一个线程的计算被覆盖,结果就会丢失。
  •   
     

您需要确定这是否会对您的应用程序造成问题。我必须指出,如果是这样,只是锁定读写操作将无法解决它。此外,试图延长锁的持续时间会导致其他问题。

选项

关键部分

是的,您可以使用关键部分。

  • 您需要选择关键部分的最佳粒度:每个文件一个,或者可能使用它们来指定文件中的特定块。
  • 这个决定需要更好地了解你的申请是做什么的,所以我不打算为你回答。
  • 请注意死锁的可能性:
    • 线程1获取锁定A
    • 线程2获取锁B
    • 线程1需要锁定B,但必须等待
    • 线程2需要锁定A - 导致死锁,因为两个线程都无法释放其获取的锁定。

我还将建议您在解决方案中考虑其他两种工具。

单线程

真是令人震惊的事情!但是,严肃地说,如果你去多线程的理由是“让应用程序更快”,那么你就会出现多线程的 错误的 原因。这样做的大多数人实际上最终制作他们的应用程序,更难写,更不可靠, 更慢

多线程加速应用程序是一个非常普遍的误解。如果任务需要X个时钟周期来执行 - 它将需要X个时钟周期!多线程不会加速任务,它允许并行完成多个任务。但是这个可能是一件坏事! ...

您已经将应用程序描述为高度依赖于从磁盘读取,解析读取和写入磁盘的内容。根据解析步骤的CPU密集程度,您可能会发现所有线程都花费大部分时间等待磁盘IO操作。在这种情况下,多个线程通常仅用于将磁盘头分流到您的(ummm )磁盘盘的远角。磁盘IO仍然是瓶颈,线程使其表现得好像文件最大化碎片一样。

排队操作

让我们假设您进入多线程的原因是有效的,并且您仍然可以在共享资源上运行线程。您可以将共享资源操作排队到特定线程上,而不是使用锁来避免并发问题。

所以代替线程1:

  • 从档案A中读取位置X
  • 解析数据
  • 写入文件A中的位置Y

创建另一个线程; FileA线程:

  • FileA有一个指令队列
  • 当它到达读取位置X的指令时,它就会这样做。
  • 它将数据发送到线程1
  • 线程1解析其数据---而FileA线程继续处理指令
  • 线程1放置一条指令,将其结果写入FileA线程队列后面的位置Y,而FileA线程继续处理其他指令。
  • 最终FileA线程将根据Trhead 1的要求写入数据。

答案 1 :(得分:5)

仅当共享数据可能导致问题(或错误)时才需要同步,如果多个代理正在对其执行某些操作。

显然,如果您不希望其他编写器进程在写入完成之前对新数据进行践踏,那么文件写入操作应该只包含在该文件的关键部分中 - 文件如果你有一半的新数据被另一个没有看到新数据的另一半的进程修改(原来的编写者进程尚未写出),那么可能不会长久保持一致。因此,您将拥有一个CS集合,每个文件一个。当你完成写作时,应尽快释放CS。

在某些情况下,例如内存映射文件或稀疏文件,O / S 可能允许您同时写入文件的不同部分。因此,在这种情况下,您的CS必须位于文件的特定上。因此,每个文件都有一个CS集合(每个段一个)。

如果您写入文件并同时阅读,则阅读器可能会收到不一致的数据。在某些操作系统中,允许读取与写入同时发生(可能读取来自缓存的缓冲区)。但是,如果您正在写入文件并同时阅读它,则您阅读的内容可能不正确。如果您需要有关读数的一致数据,那么读者也应该接受关键部分。

在某些情况下,如果您正在写一个片段并从另一个片段中读取,则O / S可能会允许它。但是,这是否会返回正确的数据通常无法保证,因为您无法始终确定文件的两个段是否可能驻留在一个磁盘扇区或其他低级O / S中。

因此,一般来说,建议是在CS中为每个文件包装任何文件操作。

理论上,您应该能够同时从同一个文件中读取,但将其锁定在CS中只能允许一个读者。在这种情况下,您需要将实现分为“读锁”和“写锁”(类似于数据库系统)。这是非常重要的,因为你必须处理促进不同级别的锁定。

注意事项:您尝试数据的方式(在段中同时读取和写入GB大小的数据集)通常在数据库中完成。您应该考虑将数据文件分解为数据库记录。否则,您要么因锁定而遭受非优化的读/写性能,要么最终重新发明关系数据库。

答案 2 :(得分:3)

结论第一

您不需要TCriticalSection。您应该实现一个基于队列的算法,该算法可确保没有两个线程正在处理同一条数据,而不会阻塞。

我是如何得出这个结论的

首先,Windows(Win 7?)将允许您根据需要同时写入文件。我不知道它对写操作有什么作用,我显然不是说这是个好主意,但我刚刚完成了以下测试,以证明Windows允许同时多次写入同一个文件:

我创建了一个打开文件进行写入的线程(使用“share deny none”)并继续将随机内容写入随机偏移量30秒。这是一个pastebin with the code

为什么TCriticalSection会不好

关键部分只允许一个线程在任何给定时间访问保护资源。您有两种选择:只在读/写操作期间保持锁定,或者在处理给定资源所需的整个时间内保持锁定。两者都有严重的问题。

如果线程仅在读/写操作期间持有锁,可能会发生以下情况:

  • 线程1获取锁,读取数据,释放锁
  • 线程2获取锁,读取相同的数据,释放锁
  • 线程1完成处理,获取锁定,写入数据,释放锁定
  • 线程2获取锁定,写入数据,这里是 oops :线程2一直在处理旧数据,因为线程1在后台进行了更改!

如果一个线程持有整个圆形修整读数的锁定,可能会发生这种情况。写操作:

  • 线程1获取锁定,开始读取数据
  • 线程2尝试获取相同的锁,被阻止......
  • 线程1完成读取数据,处理数据,将数据写回文件,释放锁定
  • 线程2获取锁定并再次开始处理相同的数据

队列解决方案

由于您是多线程的,并且您可以让多个线程同时处理来自同一文件的数据,我假设数据以某种方式“无上下文”:您可以在处理第一个文件之前处理文件的第三部分。这必须是真的,因为如果不是这样,你就不能多线程(或者每个文件限制为1个线程)。

在开始处理之前,您可以准备一些“工作”,如下所示:

  • 文件'file1.raw',偏移0,024 Kb
  • 文件'file1.raw',偏移1024 1024 kb。
  • ...
  • 文件'fileN.raw',偏移99999999,1024 kb

将所有这些“工作”放入队列中。让您的线程将一个Job从队列中排队并处理它。由于没有两个作业重叠,因此线程不需要彼此同步,因此您不需要临界区。您只需要关键部分来保护对Queue本身的访问。 Windows确保线程可以正常读取和写入文件,只要它们坚持分配的“作业”。