概念

现代系统允许一个进程里同时运行多个线程,线程由内核自动调度。每个线程都有它自己的线程上下文,包括一个唯一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。

共享本进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。

Posix线程(Pthreads)是在C程序中处理线程的标准接口。

线程上下文模型

线程库

linux下提供了多种方式来处理线程同步,最常用的互斥锁、条件变量、信号量和读写锁。

pthread线程库

线程基本函数,linux下man pthread.h请查看。

头文件pthread.h, gcc链接是参数: -lpthread

1.创建线程:
int pthrad_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);

  • tid: 输出参数,保存返回的线程ID(与linux系统中的线程ID不一样,这个ID应该理解为一个地址),用无符号长整型表示;
  • attr: 输入参数,线程的相关属性,如线程优先级、初始栈大小、是否为守护进程等,一般置为NULL,表示使用默认属性;
  • func: 输入参数,一个函数指针(void *job(void *arg);),线程执行的函数;
  • arg: 输入参数,函数的参数,如果有多个参数须将其封装为一个结构体;
    返回值:成功返回0,失败返回errno值(正数);

2.退出线程:
void pthread_exit(void *status);

  • status: 输入状态,退出状态

3.等待线程退出:
int pthread_join(pthread_t tid, void **status);

  • tid: 输入参数,指定等待的线程ID;
  • status: 输出参数,一个二级指针,保存退出值,可为NULL; 返回值:成功返回0,失败返回errno值;
  • 功能:会阻塞直到线程tid终止,将线程例程返回的通用(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有资源。

4.获取当前线程ID :
pthread_t pthread_self(void);

5.分离线程 :
int pthread_detach(pthread_t tid);

  • tid:输入参数,指定的线程ID;
  • 返回值:成功返回0,失败返回errno值;

结合线程&分离线程

在任何时间节点上,线程是可结合的或者是分离的。

  • 结合线程:能够被其他线程回收和杀死,在他被回收之前内存资源不释放的;
  • 分离线程:不能被其他线程回收或杀死,他的内存资源在它终止时由系统自动释放;

通信(同步)方式

互斥锁

互斥锁是同一时刻只允许一个线程执行一个关键部分代码。

1.静态初始化:static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

2.动态初始化:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);:

  • mutex:输出参数,互斥变量;
  • 返回值:成功返回0,失败返回errno值;
  • attr:输入参数,锁属性,NULL值为默认属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
    • PTHREAD_MUTEX_TIMED_NP: 这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
    • PTHREAD_MUTEX_RECURSIVE_NP: 嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
    • PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
    • PTHREAD_MUTEX_ADAPTIVE_NP: 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

3.阻塞加锁:
int pthread_mutex_lock(pthread_mutex *mutex);

4.非阻塞加锁:
int pthread_mutex_trylock( pthread_mutex_t *mutex);
不同于阻塞加锁,已经被占用时返回EBUSY而不是挂起等待。

5.解锁(要求,锁是lock状态,并且由加锁线程解锁):
int pthread_mutex_unlock(pthread_mutex *mutex);

6.销毁锁(要求,锁是unlock状态,否则返回EBUSY):
int pthread_mutex_destroy(pthread_mutex *mutex);

条件变量

条件变量是利用线程间共享全局变量进行的一种机制。条件变量上基本操作有:触发条件(当条件变量为true时);等待条件,挂起线程直到其他线程出发条件。

1.静态初始化、动态初始化(和互斥锁相似):
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;:静态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);:动态初始化

2.无条件等待:
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

3.计时等待:
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

无条件等待、或计时等待都必须和互斥锁配合以防止多个线程同时请求:

  • mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP);
  • 在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。
  • 在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

4.激发条件,激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个):
int pthread_cond_signal(pthread_cond_t *cond);

5.激活所有等待线程:
int pthread_cond_broadcast(pthread_cond_t *cond);

6.销毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond);

  • 只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY

说明:

  1. pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
  2. 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)
  3. 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁

为什么pthread_cond_wait需要的互斥锁不在函数内部定义,而要使用户定义的呢?

  • pthread_cond_wait 和 pthread_cond_timewait 函数为什么需要互斥锁?因为:条件变量是线程同步的一种方法,这两个函数又是等待信号的函数,函数内部一定有须要同步保护的数据。
  • 使用用户定义的互斥锁而不在函数内部定义的原因是:无法确定会有多少用户使用条件变量,所以每个互斥锁都须要动态定义,而且管理大量互斥锁的开销太大,使用用户定义的即灵活又方便,符合UNIX哲学的编程风格(随便推荐阅读《UNIX编程哲学》这本好书!)。
  • 大神推测pthread_cond_wait函数的内部结构时
1
2
3
4
5
6
7
8
9
10
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
{
if(没有条件信号)
{
1)pthread_mutex_unlock (mutex); // 因为用户在函数外面已经加锁了(这是使用约定),但是在没有信号的情况下为了让其他线程也能等待cond,必须解锁。
2) 阻塞当前线程,等待条件信号(当然应该是类似于中断触发的方式等待,而不是软件轮询的方式等待)... 有信号就继续执行后面。
3) pthread_mutex_lock (mutex); // 因为用户在函数外面要解锁(这也是使用约定),所以要与1呼应加锁,保证用户感觉依然是自己加锁、自己解锁。
}
...
}

作为有态度的技术人揣测咱们也不能轻易的相信,探究pthread_cond_wait函数的内部结构过程:

1.从http://ftp.gnu.org/gnu/glibc/ 下载最新的glibc-2.29.tar.gz(本人下载),在nptl目录下的pthread_cond_wait.c文件中

2.函数实现,整体过程跟大神讲的差不多,不同点在于加锁和解锁在循环外面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static __always_inline int
__pthread_cond_wait_common (pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime)
{
...
// 解锁
err = __pthread_mutex_unlock_usercnt (mutex, 0);
...
do
{
while (1)
{
...
}

}
while (!atomic_compare_exchange_weak_acquire (cond->__data.__g_signals + g, &signals, signals - 2));
...
// 加锁
err = __pthread_mutex_cond_lock (mutex);

信号量

如同进程一样,线程也可以通过信号量来实现通信。

头文件semaphore.h, 记得man查看详情。

1.初始化
int sem_init (sem_t *sem , int pshared, unsigned int value);

  • sem: 指定要初始化的信号量;
  • pshared: 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
  • value: 信号量 sem 的初始值。

2.信号量值加1
int sem_post(sem_t *sem);

3.信号量值减1
int sem_wait(sem_t *sem);

4.销毁信号量
int sem_destroy(sem_t *sem);

读写锁

读锁:一个读锁占用的临界区后来的读锁也可以进入,但是如果是写锁,则阻塞。在写锁阻塞后,后面再来读锁也会阻塞。这样避免读锁长期占用资源,防止写锁饥俄。
写锁:写锁占用了临界区,其他的线程(读写锁)都会阻塞。

1.初始化

1
2
3
4
5
// 动态初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

// 静态初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

2.读写锁

1
2
3
4
5
6
7
8
9
10
11
12
13
// 加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

// 试图加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

// 计时加锁
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);

3.销毁锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

并发小示例

要求:用两个线程分别输出数组的奇数下标和偶数下标的内容;

实现(忽略捕获异常):

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
48
49
50
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/syscall.h>

#define gettid() syscall(__NR_gettid)

int len = 5;
int arr[5] = {1,2,3,4,5};
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void printOddNumber(int * arr);
void printEvenNumber(int * arr);


int main(){

pthread_t tid1, tid2;
errno = pthread_create(&tid1 , NULL, printOddNumber, arr);
errno = pthread_create(&tid2, NULL, printEvenNumber, arr);

errno = pthread_join(tid1, NULL);
errno = pthread_join(tid2, NULL);

pthread_mutex_destroy(&mutex);

printf("-------leave thread_main (pid: %d, tid: %ld) -------\n", getpid(), gettid());
}

void printOddNumber(int* arr){
pthread_mutex_lock(&mutex);
printf("-------thread odd ( pid: %d , tid: %ld ) \n", getpid(), gettid());
int i = 0;
for(i = 1; i < len ; ){
printf("odd number : %d \n",arr[i]);
i+=2;
}
pthread_mutex_unlock(&mutex);
}
void printEvenNumber(int* arr){
pthread_mutex_lock(&mutex);
printf("-------thread even ( pid: %d , tid: %ld ) \n", getpid(), gettid());
int i = 0;
for(i = 0 ; i < len ; ){
printf("even number : %d \n" , arr[i]);
i+=2;
}
pthread_mutex_unlock(&mutex);
}

编译:
gcc -o pthread_array pthread-array.c -lpthread
编译时warn可忽略。。。

输出:

1
2
3
4
5
6
7
8
9
10
./pthread_array

-------thread odd ( pid: 23672 , tid: 23673 )
odd number : 2
odd number : 4
-------thread even ( pid: 23672 , tid: 23674 )
even number : 1
even number : 3
even number : 5
-------leave thread_main (pid: 23672, tid: 23672) -------

一个进程23672,三个线程(算main线程),即一个程序至少有一个进程,一个进程至少有一个线程 。

参考