Unix线程基础

目录

1 线程标识

每个线程都有一个唯一标识:线程ID. 线程ID用 pthread_t 数据类型来表示,该结构体在不同的系统上有不同的实现,某些操作系统将其实现为 unsigned long ,某些操作系统将其实现为结构体。所有可移植代码中不可以对其直接比较,必须使用一个函数来对两个线程ID进行比较。

1
2
//返回值:若相等,则为非0;否则,返回0
int pthread_equl(pthread_t t1, pthread_t t2);

不同的实现方式带来的影响就是,不能使用一种可移植的方式来打印该数据类型的值。
获取当前线程:

1
2
3
4
5
//Unix
pthread_t pthread_self(void);

//Windows
HANDLE GetCurrentThread(void);

2 线程创建

新增的线程可以通过调用pthread_create 函数来创建

1
2
3
4
5
#include <pthread.h>
//thread: 当成功时,指向新增线程的内存单元
//返回值:若成功,则返回0;否则 返回错误编号
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

新创建的线程从 start_routine 函数的地址开始运行。该函数有一个 void* 类型的指针做为参数。如果函数需要多个参数,需要将参数做为结构体,然后将结构体地址做为参数传入。 创建线程时并不能保证新增线程优先于其它线程先运行,这依赖于操作系统的线程实现与调度算法。

3 线程终止

如果进程中的任意线程调用了 exit\ _Exit\ _exit, 那么整个进程就会终止。
单个线程可以通过3种方式退出,因此可以在不终止进程的情况下,控制线程:

  1. 线程可以简单地从启动全程中返回,返回值是线程的退出码

  2. 线程可以被同一进程中其他的线程取消

  3. 线程调用 pthread_exit

1
void pthread_create(void* rval_ptr);

rval_ptr是一个无类型指针。进程中的其它线程可以通过调用 pthread_join 函数来访问这个指针

1
2
//return: 0 is OK
int pthread_join(pthread tid, void** rval_ptr);

调用线程将一直阻塞,直到指定的线程被终止。如果线程是取取消的 ,rval_ptr 指定的内存单元被设置这PTHREAD_CANCELED. 如果是简单返回,则rval_ptr 就包含返回码。 线程可以通过调用pthread_cancal来请求取消同一进程中的其他线程

1
2
//return: 0 is OK
int pthread_cancel(pthread_t tid);

线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数一样。这样的函数称为/线程清理处理程序(thread cleanup handler)/。一个线程可以建立多个清理程序。
处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。

1
2
void pthread_cleanup_push(void (*rtn)(void *), void* arg);
void pthread_cleanup_pop(int execute);

清理函数rtn由 pthread_cleanup_pop 函数调度。调用时只有一个参数arg。
当线程执行以下动作时,清理函数被调用:

  1. 调用pthread_exit

  2. 响应取消请求

  3. 用非零execute参数调用pthread_cleanup_pop

如果 execute 参数设置为 0,清理函数将不被调用。
这些函数有一个限制,由于他们可以实现为宏,所有必须在与线程相同的作用域中以匹配对的形式使用。pthread_cleanup_push的宏定义中可以包含字符 { ,这种情况下,在 pthread_cleanup_pop 的宏定义中要有对应的匹配字符 } ,否则可能将无法编译。 实例 ::

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
void* on_threadexit(void* arg)
{
printf("on_threadexit fun called with param: %s \n",(char*)arg);
return 0;
}

void* th_fun(void* arg)
{
pthread_cleanup_push(on_threadexit, "first call");
pthread_cleanup_push(on_threadexit, "second call");
pthread_cleanup_push(on_threadexit, "third call");

printf("th_fun called\n");

pthread_cleanup_pop(1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);

return 0;
}

int main(void)
{
pthread_t nth;

int err = pthread_create(&nth, 0, th_fun, 0);
if (err != 0)
printf("thread create error");

err = pthread_join(nth, 0);
if (err != 0)
printf("can not jion thread nth\n");

printf("main fun end\n");
return 0;
}

运行程序会得到:

1
2
3
4
th_fun called
on_threadexit fun called with param: third call
on_threadexit fun called with param: first call
main fun end

可以看出,最后注册的清理程序最先执行,当 clean_cleanup_pop 的参数为 0 时,该清理程序不执行。
线程操作与进程操作有很多相似之处,下表总结对比了一些相似函数:

Table 1: 进程与线程操作函数对比表
进程 线程 描述
fork pthread_create 创建新的控制流
exit pthread_exit 从现有的控制流中退出
waitpid pthread_jion 从控制流是得到退出状态
atexit pthread_cleanup_push 注册控制流退出时调用的函数
getpid pthread_selft 获取控制流的ID
abort pthread_cancel 请求控制流非正常退出

4 线程同步

当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量其他线程都不会读取或修改,或者多个线程都只访问只读变量,就不会有一致性问题。但是,当一个线程可以修改变量,其他线程也可以读取或修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量时,不会访问到无效值。 为解决这个问题,我们需要使用同步锁,同一时间只允许一个线程访问该变量。

4.1 互斥量

可以使用 pthread 的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex) 从本质上来说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放互斥变量(解锁)。在释放互斥变量时,如果该锁上有一个或多个线程处于阻塞状态,则会有一个线程根据优先级对互斥变量加锁,其他线程依次等待。这样保证很次只有一个线程访问变量。
互斥变量使用 pthread_mutex_t 数据类型表示。互斥变量使用前必须初始化,可以把它设置为 PTHREAD_MUTEX_INITALIZER (只适用于静态分配的互斥量),也可以通过 pthread_mutex_init 函数进行初始化。如果动态分配了互斥量,在释放前需要调用 pthread_mutex_destroy.

1
2
3
int pthread_mutex_init (pthread_mutex_t * mutex,
const pthread_mutexattr_t * attr);
int pthread_mutex_destroy (pthread_mutex_t * mutex);

要使用默认属性初始化互斥量,只需要把attr参数设置为NULL。
对互斥变量加锁,需要调用 pthread_mutex_lock. 如果互斥变量已经上锁,调用线程将阻塞直到互斥变量解锁。互斥变量解锁需要调用 pthread_mutex_unlock.

1
2
3
4
int pthread_mutex_lock (pthread_mutex_t * mutex);
int pthread_mutex_timedlock(pthread_mutex_t * mutex,
const struct timespec *abstime);
int pthread_mutex_unlock (pthread_mutex_t * mutex);

如果线程不希望被阻塞,可以使用 pthread_mutex_trylock 尝试对互斥量进行加锁。如果调用 pthread_mutex_trylock 时互斥量处于未锁住状态,那么调用后互斥量将会锁住,线程不会阻塞直接返回0,否则 pthread_mutex_trylock 就会失败,不能锁住互斥量,并返回 EBUSY.
函数 pthread_mutex_timedlock 与 pthread_mutex_lock 基本是等价的,但是在达到超时时间值时,pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码 ETIMEDOUT。

4.2 读写锁

读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量有两个状态,加锁或不加锁,而且一次只有一个线程或以对其进行加锁。读写锁可以有3种状态:读模式下加锁、写模式下加锁、不加锁。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁的线程都会被阻塞。

当读写锁是读加锁状态时,所有试图以图模式对其进行加锁的线程都可以获得访问权,但是任何希望以写模式对此锁进行加锁的线程都会被阻塞,直到所有的线程都释放它们的读锁为止。

读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为一次只有一个线程可以在写模式下拥有这个锁。

读写锁也叫共享互斥锁(shared-exclusive lock).当读写锁是读模式锁住时,就可以说成是共享模式锁住的。当它是写模式锁住时,就可以说它是互斥模式锁住的。
与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。 读写锁以 pthread_wrlock_t 数据类型表示。有两种方法进行初始化,PTHREAD_RWLOCK_INITIALIZER 和 pthread_rwlock_init() .

1
2
3
int pthread_rwlock_init(pthread_rwlock_t *lock,
const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *lock);

在释放读写锁占用的内存之前,需要调用 pthead_rwlock_destroy 做清理工作。如果 pthread_rwlock_init 为读写锁分配了资源, pthread_rwlock_destroy 将释放这些资源。如果在调用 pthread_rwlock_destroy 之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。
要在读模式下锁定读写锁,需要调用 pthread_rwlock_rdlock.
要在写模式下锁定读写锁,需要调用 pthread_rwlock_wrlock.
不管在何种模式下释放读写锁,都可以调用 pthread_rwlock_unlock 进行解锁。

1
2
3
4
5
6
7
8
9
10
11
int pthread_rwlock_rdlock(pthread_rwlock_t *lock);
int pthread_rwlock_wrlock(pthread_rwlock_t *lock);
int pthread_rwlock_unlock(pthread_rwlock_t *lock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *);
int pthread_rwlock_trywrlock(pthread_rwlock_t *);

int pthread_rwlock_timedrdlock(pthread_rwlock_t *lock,
const struct timespec *abstime);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *lock,
const struct timespec *abstime);

有些实现可能会对共享模式下获取读写锁的次数进行限制,所以要检查 pthread_rwlock_rdlock 的返回值。

4.3 条件变量

条件变量是线程可用的另一种同步机制。条件变量与互斥变量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的,线程在改变条件状态之前必须首先锁住互斥量。
条件变量以 pthread_cond_t 数据类型表示。可以用两种方法进行初始化: PTHREAD_COND_INITIALIZER 和 pthread_cond_init

1
2
3
int pthread_cond_init (pthread_cond_t * cond,
const pthread_condattr_t * attr);
int pthread_cond_destroy (pthread_cond_t * cond);

4.4 自旋锁

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁适用于锁被持有的时间短且线程不希望在重新调试上花太多成本的情况下。 自旋锁一般用在底层,用于实现其它的锁。

4.5 屏障

屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都达到一个点,然后从该点继续执行。前面我们已经看到一种屏障 pthread_jion ,它允许线程A等待线程B,直到线程B退出。
线程屏障的基本函数:

1
2
3
int pthread_barrier_init (pthread_barrier_t * barrier, const pthread_barrierattr_t * attr, unsigned int count);
int pthread_barrier_destroy (pthread_barrier_t * barrier);
int pthread_barrier_wait (pthread_barrier_t * barrier);

在屏障初始化时,可以使用count参数,打指定在允许解开屏障时,必须达到屏障的线程数目。使用 pthread_barrier_wait 的线程在屏障计数未满足条件时,会进入休眠状态,如果该线程是最后一个调用 pthread_barrier_wait 的线程,就满足了屏障计数,线有的线程都被唤醒。
对于返回 PTHREAD_BARRIER_SERIAL_THREAD 的线程,可以作为主线程,它可以工作在其他所有线程已经完成的工作结果上。
下面给出屏障的示例:

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
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <iostream>
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;

pthread_barrier_t bar;

void print_time()
{
timeval tv;
gettimeofday(&tv,NULL);
printf("%u",tv.tv_sec);
}

void* th_fun(void* arg)
{
int* tm = (int*)arg;
print_time();
printf(" %d sleep Start\n",*tm); //print when thread start

sleep(*tm);

print_time();
printf(" %d sleep End and barrier wait\n", *tm); //print when barrier work
pthread_barrier_wait(&bar);

print_time();
printf(" %d barrier End\n", *tm); //print when barrier end

return 0;
}

int main(void)
{
const int barCount = 4;
pthread_t* ths = new pthread_t[barCount];

int barErr = pthread_barrier_init(&bar, NULL, barCount+1);

int err = 0;

for (int i = 0; i < barCount ; i++)
{
int* p = new int((barCount - i + 1) * 1);
err = pthread_create(&ths[i], 0, th_fun, p);
if (err != 0)
printf("thread %d create error\n",i);
}

pthread_barrier_wait(&bar);

if (barErr == 0)
pthread_barrier_destroy(&bar);

printf("main fun end\n");
return 0;
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
1464142316  5 sleep Start
1464142316 4 sleep Start
1464142316 3 sleep Start
1464142316 2 sleep Start
1464142318 2 sleep End and barrier wait
1464142319 3 sleep End and barrier wait
1464142320 4 sleep End and barrier wait
1464142321 5 sleep End and barrier wait
1464142321 4 barrier End
1464142321 3 barrier End
1464142321 2 barrier End
1464142321 5 barrier End

我们在屏障初始化时,count 参数加了 1 ,这是因为我们将主线程也作为候选线程。 因为我们使用了主线程做为最后工作的线程,所有我们不需要使用 pthread_barrier_wait 函数中的返回值 PTHREAD_BARRIER_SERIAL_THREAD 来决定哪个线程执行最终的工作。