本文总结多线程的特性和同步方法。

多线程

多线程的复杂性来源:

  1. CPU任务可抢占
  2. 多核并发
  3. 线程让渡

1 线程数据

1.1 线程私有数据

线程有如下私有数据:

  • 线程ID,线程ID在进程内部唯一。
  • 一组寄存器
  • 调度优先级和调度策略
  • 信号屏蔽字
  • errno变量
  • 线程私有数据

1.2 线程共享数据

一个进程中的所有信息对该进程中的所有线程都是共享的。包含如下信息:

  • 可执行程序的代码

  • 程序的全局变量

  • 堆内存

  • 文件描述符

1 线程同步方法

原子操作相关的接口由GCC自身提供,使用时无需包含其他头文件。pthread相关接口由glibc提供,使用时需要包含pthread.h头文件。

1.1 原子操作

原子操作主要用于对整数进行简单的取值、加减、交换操作。由GCC提供的原子操作:

  1. 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
  1. ++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 线程安全函数

如果一个函数在相同的时间点可以被多个线程安全的调用,则称该函数是线程安全的。

如果一个函数对多个线程来说是可重入的,就说这个函数是线程安全的。

最典型的场景是:使用了静态变量的函数就是线程不安全的函数。因为多个线程调用时,都可以修改该静态变量。

解决方法是: 将静态变量替换为调用者传入,各个线程自己管理自己的变量。