本文总结多线程的特性和同步方法。
多线程
多线程的复杂性来源:
- CPU任务可抢占
- 多核并发
- 线程让渡
1 线程数据
1.1 线程私有数据
线程有如下私有数据:
- 线程ID,线程ID在进程内部唯一。
- 一组寄存器
- 栈
- 调度优先级和调度策略
- 信号屏蔽字
- errno变量
- 线程私有数据
1.2 线程共享数据
一个进程中的所有信息对该进程中的所有线程都是共享的。包含如下信息:
-
可执行程序的代码
-
程序的全局变量
-
堆内存
-
栈
-
文件描述符
1 线程同步方法
原子操作相关的接口由GCC自身提供,使用时无需包含其他头文件。pthread相关接口由glibc提供,使用时需要包含pthread.h头文件。
1.1 原子操作
原子操作主要用于对整数进行简单的取值、加减、交换操作。由GCC提供的原子操作:
- n++类
1
2
3
4
5
6
7
8
|
type __sync_fetch_and_add(type *ptr, type value, ...); // m+n
type __sync_fetch_and_sub(type *ptr, type value, ...); // m-n
type __sync_fetch_and_or(type *ptr, type value, ...); // m|n
type __sync_fetch_and_and(type *ptr, type value, ...); // m&n
type __sync_fetch_and_xor(type *ptr, type value, ...); // m^n
type __sync_fetch_and_nand(type *ptr, type value, ...); // (~m)&n
/* 对应的伪代码 */
{ tmp = *ptr; *ptr op= value; return tmp; }{ tmp = *ptr; *ptr = (~tmp) & value; return tmp; } // nand
|
- ++n类
1
2
3
4
5
6
7
8
|
type __sync_add_and_fetch(type *ptr, type value, ...); // m+n
type __sync_sub_and_fetch(type *ptr, type value, ...); // m-n
type __sync_or_and_fetch(type *ptr, type value, ...); // m|n
type __sync_and_and_fetch(type *ptr, type value, ...); // m&n
type __sync_xor_and_fetch(type *ptr, type value, ...); // m^n
type __sync_nand_and_fetch(type *ptr, type value, ...); // (~m)&n
/* 对应的伪代码 */
{ *ptr op= value; return *ptr; }{ *ptr = (~*ptr) & value; return *ptr; } // nand
|
3.CAS类
1
2
3
4
|
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...);
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...);
/* 对应的伪代码 */
{ if (*ptr == oldval) { *ptr = newval; return true; } else { return false; } }{ if (*ptr == oldval) { *ptr = newval; } return oldval; }
|
1.2 自旋锁(spinklock)
自旋锁使用场景:执行时间短的任务。
等待自旋锁的过程中,不会让渡CPU资源,盲等。
1.2.1 pthread自旋锁接口
1
2
3
4
5
6
|
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t* lock, int pshared); // init spink lock
int pthread_spin_destroy(pthread_spinlock_t* lock); // destroy spin lock
int pthread_spin_lock(pthread_spinlock_t* lock);
int pthread_spin_unlock(pthread_spinlock_t* lock);
int pthread_spin_trylock(pthread_spinlock_t* lock);
|
1.2.2 pthread_spin_lock实现
以x86系统为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int pthread_spin_lock ( pthread_spinlock_t *lock) {
asm ("\n"
"1:\t" LOCK_PREFIX "decl %0\n\t"
"jne 2f\n\t"
".subsection 1\n\t"
".align 16\n"
"2:\trep; nop\n\t"
"cmpl $0, %0\n\t"
"jg 1b\n\t"
"jmp 2b\n\t"
".previous"
: "=m" (*lock)
: "m" (*lock));
return 0;
}
|
1.2.2 pthread_spin_unlock实现
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.globl pthread_spin_unlock
.type pthread_spin_unlock,@function
.align 16
pthread_spin_unlock:
movl 4(%esp), %eax
movl $1, (%eax)
xorl %eax, %eax
ret
.size pthread_spin_unlock,.-pthread_spin_unlock
/* The implementation of pthread_spin_init is identical. */
.globl pthread_spin_init
pthread_spin_init = pthread_spin_unlock
|
1.3 互斥量(mutex)
对互斥量进行加锁以后,任何其它试图再次对互斥量加锁的线程都会被阻塞,直到当前你线程释放该互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么所有阻塞在该互斥量上的线程都会变为可运行状态。但是只有第一个可运行状态的线程可以获取互斥锁而得以继续运行,其他线程仍然需要继续等待。
1.3.1 pthread mutex接口
1
2
3
4
5
6
7
8
9
10
|
// pthread_mutex_t结构定义:
// nptl\sysdeps\unix\sysv\linux\x86_64\bits\pthreadtypes.h
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int phtread_mutex_unlock(pthread_mutex_t* mutex);
|
1.3.2 pthread_mutex结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// x86_64结构
typedef union {
struct __pthread_mutex_s {
int __lock;
unsigned int __count;
int __owner;
unsigned int __nusers;
int __kind;
int __spins;
__pthread_list_t __list;
# define __PTHREAD_MUTEX_HAVE_PREV 1
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
|
1.4 读写锁(rw_lock)
1.4.1 读写锁特性
读写锁可以有3种状态:读模式下加锁状态,写模式下加锁状态和不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,在这个锁被解锁前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以的到访问权。但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。该模式为读锁优先模式,存在申请写锁的线程饥饿问题。
优化方案:当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,写模式锁一直等待的问题。
1.4.2 读写锁接口
1
2
3
4
5
6
7
8
|
#include <pthread.h>
int pthread_rwlock_init(phread_rwlock_t* rwlock, pthread_rwlockattr_t* attr); //init
int pthread_rwlock_destroy(pthread_rwlok_t* rwlock); // destroy
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock); // set read lock
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock); // set write lock
int pthread_rwlock_unlock(pthread_rwlock_t* unlock); // unlock read/write lock
|
1.5 条件变量(condition variable)
1.5.1 条件变量相关接口
1
2
3
4
5
6
7
8
9
10
|
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* tsptr);
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
|
1.5.2 条件变量实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <pthread.h>
4 #include <errno.h>
5
6 int done = 0;
7 pthread_mutex_t m_lock = PTHREAD_MUTEX_INITIALIZER;
8 pthread_cond_t c_lock = PTHREAD_COND_INITIALIZER;
9
10 void thread_join(void)
11 {
12 pthread_mutex_lock(&m_lock);
13 while (done == 0) {
14 pthread_cond_wait(&c_lock, &m_lock);
15 }
16 pthread_mutex_unlock(&m_lock);
17 }
18
19 void thread_exit(void)
20 {
21 pthread_mutex_lock(&m_lock);
22 done = 1;
23 printf("thread end.\n");
24 pthread_cond_signal(&c_lock);
25 pthread_mutex_unlock(&m_lock);
26 }
28 void* t_func(void* arg)
29 {
30 printf("thread is start.\n");
31 thread_exit();
32 return NULL;
33 }
34
35 int main(void)
36 {
37 printf("parent start.\n");
38 pthread_t tid;
39 int ret = pthread_create(&tid, NULL, t_func, NULL);
40 if (ret != 0) {
41 printf("create pthread failed, errno(%d).\n", errno);
42 }
43 thread_join();
44
45 printf("parent end.\n");
46 return 0;
47 }
|
1.6 二进制信号量
二进制信号量也可以用来实现互斥。二进制信号量的作用类似于互斥锁。详细使用方法见进程通信文章中的信号量部分。
2 死锁
2.1 死锁的产生
2.2 死锁检测
2.2 常见死锁
3 线程安全函数
如果一个函数在相同的时间点可以被多个线程安全的调用,则称该函数是线程安全的。
如果一个函数对多个线程来说是可重入的,就说这个函数是线程安全的。
最典型的场景是:使用了静态变量的函数就是线程不安全的函数。因为多个线程调用时,都可以修改该静态变量。
解决方法是: 将静态变量替换为调用者传入,各个线程自己管理自己的变量。