8

在当前的 CPython 实现中,有一个称为“GIL”或“全局解释器锁”的对象。它本质上是一个互斥体,可以防止两个 Python 线程同时执行 Python 代码。这可以防止两个线程破坏 Python 解释器的状态,但也可以防止多个线程真正一起执行。本质上,如果我这样做:

# Thread A
some_list.append(3)
# Thread B
some_list.append(4)

我不能破坏列表,因为在任何给定时间,只有一个线程正在执行,因为它们必须持有 GIL 才能这样做。现在,列表中的项目可能会以某种不确定的顺序添加,但关键是列表没有损坏,并且总是会添加两件事。

所以,现在到 C#。C# 本质上面临与 Python 相同的问题,那么,C# 是如何防止这种情况的呢?如果有人知道的话,我也有兴趣听听 Java 的故事。


澄清:我对没有显式锁定语句会发生什么很感兴趣,尤其是对 VM 而言。我知道 Java 和 C# 都存在锁定原语——它们也存在于 Python 中:GIL 不用于多线程代码,除了保持解释器正常。我对上面的直接等价物感兴趣,所以,在 C# 中,如果我能记住的话...... :-)

List<String> s;
// Reference to s is shared by two threads, which both execute this:
s.Add("hello");
// State of s?
// State of the VM? (And if sane, how so?)

这是另一个例子:

class A
{
    public String s;
}
// Thread A & B
some_A.s = some_other_value;

// some_A's state must change: how does it change?
// Is the VM still in good shape afterwards?

我不想写糟糕的 C# 代码,我理解这些lock陈述。即使在 Python 中,GIL 也不会为您提供神奇的多线程代码:您仍然必须锁定共享资源。但是 GIL 可以防止 Python 的“VM”被破坏——我感兴趣的是这种行为。

4

6 回答 6

11

大多数其他支持线程的语言都没有 Python GIL 的等价物。它们要求您隐式或显式地使用互斥锁。

于 2010-10-26T17:33:54.197 回答
4

使用锁,你会这样做:

lock(some_list)
{
    some_list.Add(3);
}

在线程 2 中:

lock(some_list)
{
    some_list.Add(4);
}

lock语句确保语句中的对象locksome_list在这种情况下,一次只能由一个线程访问。有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.80).aspx

于 2010-10-26T17:32:26.733 回答
3

C# 没有与 Python 等效的 GIL。

尽管他们面临同样的问题,但他们的设计目标却让他们与众不同。

使用 GIL,CPython确保从两个线程追加列表等操作很简单。这也意味着它在任何时候都只允许一个线程运行。这 使得列表和字典线程安全。尽管这使工作变得更简单和直观,但它使得在多核上利用多线程优势变得更加困难。

如果没有 GIL,C#会做相反的事情。它确保完整性的负担由程序的开发人员承担,但允许您利用同时运行多个线程的优势。

根据其中一项讨论-

CPython 中的 GIL 纯粹是一种设计选择,即拥有一个大锁而不是每个对象一个锁和同步,以确保对象保持一致状态。这包括权衡 - 放弃多线程的全部功能。

大多数问题都不会受到这个缺点的影响,并且有一些库可以在需要时帮助您专门解决这个问题。这意味着对于特定类别的问题,使用多核的负担将转嫁给开发人员,以便其他人可以享受更简单、更直观的方法。

注意:IronPython 等其他实现没有 GIL。

于 2010-10-26T17:54:02.097 回答
1

查看您正在讨论的类的 Java 等效文档可能会很有启发性:

请注意,此实现不同步。如果多个线程ArrayList同时访问一个实例,并且至少有一个线程在结构上修改了列表,则必须在外部进行同步。(结构修改是添加或删除一个或多个元素,或显式调整后备数组大小的任何操作;仅设置元素的值不是结构修改。)这通常通过同步一些自然封装的对象来完成列表。如果不存在这样的对象,则应使用该Collections.synchronizedList方法“包装”列表。这最好在创建时完成,以防止对列表的意外不同步访问:

List list = Collections.synchronizedList(new ArrayList(...));

此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:如果在创建迭代器后的任何时间对列表进行结构修改,除了通过迭代器自己的 remove 或 add 方法之外的任何方式,迭代器将抛出一个ConcurrentModificationException. 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。

请注意,不能保证迭代器的快速失败行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。快速失败的迭代器ConcurrentModificationException在尽力而为的基础上抛出。因此,编写一个依赖于这个异常的正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误

于 2010-10-26T17:53:56.043 回答
0

我将大胆猜测这个问题的真正含义......

在 Python 中,解释器中的数据结构会损坏,因为 Python 使用了一种引用计数形式。

C# 和 Java 都使用垃圾回收,事实上它们在进行完整堆回收时确实使用了全局锁。

可以在没有锁的情况下在“代”之间标记和移动数据。但要真正清理它,一切都必须停止。希望是一个非常短暂的停止,但一个完整的停止。

这是截至 2007 年有关 CLR 垃圾收集的有趣链接:http: //vineetgupta.spaces.live.com/blog/cns!8DE4BDC896BEE1AD!
1104.entry

于 2010-10-26T18:56:27.693 回答
0

大多数复杂的数据结构(例如列表)在不锁定多个线程的情况下使用时可能会损坏。

由于引用的更改是原子的,因此引用始终保持有效引用。

但是在与安全关键代码交互时会出现问题。因此,关键代码使用的任何数据结构大多是以下之一:

  • 无法从不受信任的代码访问,并且被受信任的代码正确锁定/使用
  • 不可变(字符串类)
  • 使用前复制(值类型参数)
  • 用可信代码编写并使用内部锁定来保证安全状态

例如,关键代码不能信任可从不受信任代码访问的列表。如果它在 List 中传递,它必须创建一个私有副本,对副本进行前置条件检查,然后对副本进行操作。

于 2010-10-26T18:01:22.857 回答