十一、GLib——GThread
1 GThread基本概念
线程几乎像进程一样运行,但与进程不同的是,一个进程的所有线程共享相同的内存。这是好的,因为它通过共享内存提供了线程之间的轻松通信,但也有不好的地方,因为如果程序没有谨慎设计,可能会发生奇怪的事情(所谓的“Heisenbugs”)。线程的并发性质意味着不能假设不同线程中运行的代码的执行顺序,除非程序员通过同步原语primitive明确强制执行顺序。
GLib中与线程相关的函数的目标是提供一种可移植的方式来编写多线程软件。有用于保护内存部分访问的互斥锁的原语(#GMutex、#GRecMutex和#GRWLock);有使用单个位进行锁定的设施(g_bit_lock());有用于线程同步的条件变量的原语(#GCond);有线程专用数据的原语 - 每个线程都有一个私有实例(#GPrivate);有一次性初始化的功能(#GOnce、g_once_init_enter());最后,有创建和管理线程的原语(#GThread)。
GLib的线程系统曾经需要使用g_thread_init()进行初始化,但自2.32版本以来,GLib的线程系统会在程序启动时自动初始化,所有与线程创建函数和同步原语相关的函数也会立即可用。
请注意,不能安全地假设您的程序没有线程,即使您不自己调用g_thread_new()。GLib和GIO在某些情况下会为其自身的目的创建线程,例如在使用g_unix_signal_source_new()或使用GDBus时。
最初,UNIX没有线程,因此一些传统的UNIX API在多线程程序中存在问题。一些显著的例子包括:
-
返回数据的C库函数使用静态分配的缓冲区,例如strtok()或strerror()。对于这些函数,通常有带有_r后缀的线程安全变体,或者您可以查看对应的GLib API(如g_strsplit()或g_strerror())。
-
setenv()和unsetenv()函数以不线程安全的方式操纵进程环境,并可能干扰其他线程中的getenv()调用。请注意,getenv()调用可能隐藏在其他API之后。例如,GNU gettext()在幕后调用getenv()。通常最好将环境视为只读。如果确实必须修改环境,请在没有其他线程存在的情况下在main()中早早地进行修改。
-
setlocale()函数更改整个进程的区域设置,影响所有线程。通常会临时更改区域设置以更改字符串扫描或格式化函数的行为,如scanf()或printf()。GLib提供了许多字符串API(如g_ascii_formatd()或g_ascii_strtod()),通常可用作替代方法。或者,您可以使用uselocale()函数仅为当前线程更改区域设置。
-
fork()函数仅将调用线程引入子进程的进程映像的一部分。如果其他线程正在关键部分执行,它们可能已经将互斥锁锁定,这可能很容易导致新子进程中的死锁。 因此,应在子进程中尽快调用exit()或exec(),并仅在此之前进行信号安全的库调用。
-
daemon()函数以与上述描述相反的方式使用fork()。不应在GLib程序中使用它。
GLib本身在内部完全线程安全(所有全局数据都会自动锁定),但出于性能原因,不会自动锁定各个数据结构实例。 例如,您必须协调从多个线程访问同一个#GHashTable的访问。从这个规则中的两个显著例外是#GMainLoop和#GAsyncQueue, 它们是线程安全的,不需要进一步的应用级锁定即可从多个线程访问。大多数引用计数函数,如g_object_ref(),也是线程安全的。
#GThread的常见用法之一是从主线程中把长时间阻塞的线程移除出去,移动到一个工作线程。对于GLib函数(例如单个GIO操作),这是不必要的,并且会使代码复杂化。 相反,应该从主线程中使用函数的…_async()版本,从而消除了在多个线程之间的锁定和同步的需要。如果确实需要将操作移动到工作线程, 请考虑使用g_task_run_in_thread()或#GThreadPool。与#GThread相比,#GThreadPool通常是更好的选择,因为它处理线程重用和任务排队;#GTask在内部使用这个功能。
但是,如果需要顺序执行多个阻塞操作,并且不能使用#GTask进行操作,将它们移到工作线程中可以使代码更清晰。
函数 g_thread_create
已经不赞成使用了,使用 g_thread_new
替换。
2 互斥锁
以下是GMutex对象的定义,使用了联合体。非静态变量都需要使用g_mutex_init
函数进行初始化,将 #GMutex 的 p
设定为0。静态变量不需要设定,是因为未被初始化的静态变量会被程序自动初始化为0。
注意GMutex是联合体,其余递归锁,读写锁,条件都是结构体。
如果不进行初始化,会在进行上锁的时候,判断有没有创建内存赋值给 `mutex->p`,所以静态变量初始化也没必要。示例程序参考:/assets/GLibStudy/02_GThread/mutex.c
/* filename: gthread.h */
union _GMutex
{
/*< private >*/
gpointer p;
guint i[2];
};
/* ghread-posix.c */
void
g_mutex_init (GMutex *mutex) {
mutex->p = g_mutex_impl_new (); /* 本质 mutex->p = malloc (sizeof (pthread_mutex_t)); */
}
2.1 G_LOCK宏使用
NOTE:
不是全局变量(包括静态全局变量和非静态全局变量)和静态局部变量定义的GMutex对象,需要使用g_mutex_init
进行初始化。
/* filename: gthread.h */
#define G_LOCK_NAME(name) g__ ## name ## _lock /* G_LOCK_NAME(current_number) 实际是 g__current_number_lock*/
#define G_LOCK_DEFINE_STATIC(name) static G_LOCK_DEFINE (name)
#define G_LOCK_DEFINE(name) GMutex G_LOCK_NAME (name)
/* 只不过是对g_mutex_lock的封装 */
#define G_LOCK(name) g_mutex_lock (&G_LOCK_NAME (name))
#define G_UNLOCK(name) g_mutex_unlock (&G_LOCK_NAME (name))
2.2 GMutex使用
g_mutex_new
自从版本2.32开始,已经不赞成使用了。
/* 初始化 #GMutex */
void g_mutex_init (GMutex *mutex) {
mutex->p = g_mutex_impl_new (); /* 本质 mutex->p = malloc (sizeof (pthread_mutex_t)); */
}
void g_mutex_clear (GMutex *mutex) {
g_mutex_impl_free (mutex->p);
}
void g_mutex_lock (GMutex *mutex);
/* g_mutex_trylock() 尝试对互斥锁进行加锁。如果互斥锁已经被另一个线程锁定,它会立即返回 FALSE。否则,它会锁定互斥锁并返回 TRUE。
*
* 需要注意的是,GMutex 既不保证是可递归的,也不保证是非递归的。因此,如果在同一
* 线程已经锁定了某个 GMutex,再次调用 g_mutex_lock() 会导致未定义的行为(可
* 能包括但不限于死锁或返回任意值)。
*
* 因此可以使用 g_mutex_trylock ()
*/
gboolean g_mutex_trylock (GMutex *mutex);
void g_mutex_unlock (GMutex *mutex);
/* gthread-posix.c */
void
g_mutex_clear (GMutex *mutex)
{
if G_UNLIKELY (mutex->i[0] != G_MUTEX_STATE_EMPTY) /* 只有在被锁的时候或者内存区域没有赋值为0的时候才执行 */
{
fprintf (stderr, "g_mutex_clear() called on uninitialised or locked mutex\n");
g_abort ();
}
}
/* gthread.h */
union _GMutex
{
/*< private >*/
gpointer p;
guint i[2];
};
2.3 GRecMutex(递归互斥锁)
GRecMutex结构体是一个不透明的数据结构,用于表示递归互斥锁。它类似于#GMutex,但不同之处在于在同一线程中可以多次锁定GRecMutex而不会发生死锁。在这样做时,必须小心确保解锁递归互斥锁的次数与锁定的次数相同。
如果在静态存储中分配了GRecMutex,那么它可以在不进行初始化的情况下使用。否则,您应该在使用后调用g_rec_mutex_init()进行初始化,并在完成后调用g_rec_mutex_clear()进行清理。
NOTE:
不是全局变量(包括静态全局变量和非静态全局变量)和静态局部变量定义的GMutex对象,需要使用g_rec_mutex_init
进行初始化,最后一定要使用 g_rec_mutex_clear
释放初始化过程申请内存。
GRecMutex与GMutex的不同:
GMutex使用的是成员guint i[2]
,而GRecMutex使用的是指针变量gpointer p
。
关于局部非静态变量释放问题: 因为局部函数被频繁的调用,如果没有使用初始化函数,上锁的时候也会给pthread_mutex_t申请内存空间,然后赋值给rec_mutex->p。如果每次不释放,就会造成内存泄漏。对于静态变量和全局变量,程序运行一次就只会分配一次,所以不需要释放也没问题。但是对于程序规范来说,最后也应该释放。
/* 初始化函数是申请一个pthread_mutex_t所需的内存,然后赋值给rec_mutex->p */
void g_rec_mutex_init (GRecMutex *rec_mutex) {
rec_mutex->p = g_rec_mutex_impl_new (); /* 其实里申请了 pthread_mutex_t 类型内存空间 */
}
/* 释放初始化过程中申请的内存空间 */
void g_rec_mutex_clear (GRecMutex *rec_mutex);
/* 如果没有使用初始化函数,上锁的时候也会检查是否rec_mutex->p为NULL,如果是NULL,会调用 g_rec_mutex_impl_new() 申请内存空间 */
void g_rec_mutex_lock (GRecMutex *rec_mutex);
gboolean g_rec_mutex_trylock (GRecMutex *rec_mutex);
void g_rec_mutex_unlock (GRecMutex *rec_mutex);
/* gthread.h */
struct _GRecMutex
{
/*< private >*/
gpointer p;
guint i[2];
};
2.4 GRWLock(读写锁)
GRWLock结构体是一个不透明的数据结构,用于表示读写锁。它类似于#GMutex,允许多个线程协调对共享资源的访问。
与互斥锁不同的是,读写锁区分只读(’reader’)和完全(’writer’)访问。虽然一次只允许一个线程进行写访问(通过持有’writer’锁,使用g_rw_lock_writer_lock()),但多个线程可以同时获得只读访问(通过持有’reader’锁,使用g_rw_lock_reader_lock())。当一个读者已经持有锁并且一个写者排队等待获取锁时,不确定读者或写者在获取锁时的优先级。
同一线程或者不同线程能够同时上成功读锁,意味着可以同时读取GRWLock跟GRecMutex初始化类似。
/* 初始化函数是申请一个pthread_rwlock_t所需的内存,然后赋值给rw_lock->p */
void g_rw_lock_init (GRWLock *rw_lock);
/* 释放初始化过程中申请的内存空间 */
void g_rw_lock_clear (GRWLock *rw_lock);
void g_rw_lock_writer_lock (GRWLock *rw_lock);
gboolean g_rw_lock_writer_trylock (GRWLock *rw_lock);
void g_rw_lock_writer_unlock (GRWLock *rw_lock);
void g_rw_lock_reader_lock (GRWLock *rw_lock);
gboolean g_rw_lock_reader_trylock (GRWLock *rw_lock);
void g_rw_lock_reader_unlock (GRWLock *rw_lock);
/* gthread.h */
struct _GRWLock
{
/*< private >*/
gpointer p;
guint i[2];
};
3 GCond
GCond结构体是一个不透明的数据结构,用于表示条件变量。如果线程发现某个条件为假,它们可以在GCond上阻塞。如果其他线程更改此条件的状态,它们会发出GCond信号,导致等待的线程被唤醒。
考虑以下共享变量的示例。一个或多个线程可以等待数据发布到变量上,当另一个线程发布数据时,它可以向其中一个等待的线程发出信号,以唤醒它们以收集数据。
/* 该初始化是 cond->i[0] = 0; */
void g_cond_init (GCond *cond);
/* 这是个空函数,什么都没有做 */
void g_cond_clear (GCond *cond);
/**
* 进入阻塞等待线程条件
* 该函数以下操作:
* 1.先解锁@mutex
* 2.调用syscall函数等待线程条件,当条件被发出时候,退出等待
* 3.上锁@mutex
*/
void g_cond_wait (GCond *cond, GMutex *mutex);
/* 唤醒所有等待线程中的某一个线程
* 建议:这个函数使用前后要上锁
*/
void g_cond_signal (GCond *cond);
/* 唤醒所有等待线程
* 建议:这个函数使用前后要上锁
*/
void g_cond_broadcast (GCond *cond);
/* @cond唤醒或者是超时@end_time唤醒 */
gboolean g_cond_wait_until (GCond *cond, GMutex *mutex, gint64 end_time);
struct _GCond
{
/*< private >*/
gpointer p;
guint i[2];
};
4 GThread
4.1 GThread 结构体
/* filename: gthread.h */
typedef enum
{
G_THREAD_PRIORITY_LOW,
G_THREAD_PRIORITY_NORMAL,
G_THREAD_PRIORITY_HIGH,
G_THREAD_PRIORITY_URGENT
} GThreadPriority GLIB_DEPRECATED_TYPE_IN_2_32;
struct _GThread
{
/*< private >*/
GThreadFunc func;
gpointer data;
gboolean joinable;
GThreadPriority priority;
};
/* filename: gthreadprivate.h */
typedef struct _GRealThread GRealThread;
struct _GRealThread
{
GThread thread;
gint ref_count;
gboolean ours;
gchar *name;
gpointer retval;
};
引用计数:
/* 引用计数+2,此时 ref_count == 2 */
GThread *
g_thread_try_new (const gchar *name,
GThreadFunc func,
gpointer data,
GError **error)
// 或者
GThread *
g_thread_new (const gchar *name,
GThreadFunc func,
gpointer data)
/* 如果在创建thread之后,就使用 g_thread_unref,线程函数执行完毕就会自动释放所有内存 */
/**
* 线程函数执行完毕的时候引用计数会减一 ref_count = 2 - 1 = 1
*/
/* 最后释放内存的时候,可以使用 g_thread_join 或者 g_thread_unref */
void
g_thread_unref (GThread *thread);
// 或者
gpointer
g_thread_join (GThread *thread); /* 内部会调用 g_thread_unref */
4.2 GThread相关函数
GThread * g_thread_ref (GThread *thread);
void g_thread_unref (GThread *thread);
GThread * g_thread_new (const gchar *name,
GThreadFunc func,
gpointer data);
GThread * g_thread_try_new (const gchar *name,
GThreadFunc func,
gpointer data,
GError **error);
/* 得到当前线程的 #GThread对象 */
GThread * g_thread_self (void);
/* 退出当前线程,@retval是线程返回值 */
void g_thread_exit (gpointer retval);
/* 阻塞等待@thread线程结束,@return是线程的返回值 */
gpointer g_thread_join (GThread *thread);
/* 使调用线程自愿放弃CPU,以便其他线程可以运行 */
void g_thread_yield (void);