线程
线程
线程几乎像进程一样运行,但与进程不同,同一个进程的所有线程共享相同的内存。这很好,因为它提供了一种通过共享内存简单地在涉及到的线程之间进行通信的方式,但这也很糟糕,因为如果程序没有仔细设计,可能会发生奇怪的事情(所谓的“海森堡虫”)。特别是,由于线程的并发性,不能假设不同线程上运行的代码的执行顺序,除非程序员通过同步原语明确地强制执行顺序。
GLib中线程相关函数的目标是提供一种可移植的方式来编写多线程软件。有用于保护内存部分访问的互斥量原语(GMutex
、GRecMutex
和GRWLock
)。有使用单个位来实现锁的机制(g_bit_lock()
)。有用于条件变量的原语以允许线程同步(GCond
)。有用于线程私有数据的原语 - 每个线程都有一个私有实例的数据(GPrivate
)。有一旦初始化的机制(GOnce
、g_once_init_enter_pointer()
、g_once_init_enter()
)。最后,有用于创建和管理线程的原语(GThread
)。
GLib线程系统过去使用g_thread_init()
初始化。现在这不再必要了。自2.32版本以来,GLib线程系统在程序开始时会自动初始化,并且所有线程创建函数和同步原语都立即可用。
请注意,即使没有调用g_thread_new()
,也不能保证你的程序没有线程。在某些情况下,例如使用g_unix_signal_source_new()
或使用GDBus时,GLib GIO 可能会并会为其自己的目的创建线程。
最初,UNIX 没有线程,因此一些传统的UNIX API在多线程程序中存在问题。一些值得注意的例子包括:
- 某些在静态分配的缓冲区中返回数据的C库函数,如
strtok()
或strerror()
。对于这些中的许多,都有包含_r
后缀的线程安全版本,或者你可以查看相应的GLib API(如g_strsplit()
或g_strerror()
)。 setenv()
和unsetenv()
函数以非线程安全的方式操作进程环境,可能会干扰其他线程中的getenv()
调用。请注意,getenv()
调用可能隐藏在其他API之后。例如,GNUgettext()
在幕后调用getenv()
。在一般情况下,最好将环境视为只读。如果你绝对需要修改环境,请在没有其他线程的早期main()
中执行。setlocale()
函数改变了整个进程的区域设置,会影响所有线程。通常更改区域设置是为了改变字符串扫描或格式化函数,如scanf()
或printf()
的行为。GLib提供了一些字符串API(如g_ascii_formatd()
或g_ascii_strtod()
),通常可以用作替代。或者你可以使用uselocale()
函数仅更改当前线程的区域设置。- 函数
fork()
只将调用线程复制到进程映像的子进程。如果在关键部分中其他线程正在执行,它们可能会锁定互斥锁,这很容易导致新子进程的死锁。因此,您应该在子进程中尽快调用exit()
或exec()
,并且只有在这些调用之前才能进行信号安全的库调用。 - 函数
daemon()
的用法与上述描述相反,不应与GLib程序一起使用。
GLib本身在内部是完全线程安全的(所有全局数据都自动锁定),但出于性能原因,个别数据结构实例不会自动锁定。例如,您必须协调多个线程对同一GHashTable
的访问。这一规则的两个例外是GMainLoop
和GAsyncQueue
,它们是线程安全的,并且不需要应用程序级别的锁定就可以从多个线程访问。大多数计数函数(如g_object_ref()
)也是线程安全的。
GThreads的常见用途是将耗时较长的阻塞操作从主线程移动到工作线程。对于GLib函数,如单个操作,这不是必需的,并且会使代码变得复杂。相反,应从主线程使用函数的
…_async()
版本,这样可以消除多个线程之间的锁定和同步的需要。如果需要将操作移动到工作线程,考虑使用g_task_run_in_thread()
或GThreadPool
。与GThread
相比,GThreadPool
通常是一个更好的选择,因为它处理线程重用和任务排队;GTask
在内部使用它。
但是,如果有多个阻塞操作需要按顺序执行,并且无法为它们使用GTask
,将它们移到工作线程可以使代码更加清晰。