线程简介
本文简单说明了linux操作系统中线程的概念,以及简单的操作。
线程的概念
进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap或者进程间通信机制。有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了用场。
每个线程都会执行相同的程序代码,共享同一数据区域和堆。同时,每个线程都有自己的栈,用来存储本地变量和函数调用链接信息。线程之间可以通过共享的全局变量进行通讯,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
但有些资源是每个线程各有一份的:
- 线程id
- 上下文,包括各种寄存器的值、程序计数器和栈指针
- 栈空间
- errno变量
- 信号屏蔽字
- 调度优先级
线程控制
线程的创建
程序开始启动时,它是以单进程中的单个控制线程启动的。在创建多个线程之前,程序的行为与传统的进程并没有什么区别。新增的线程可以调用pthread_create
函数创建。
#include <pthread.h>
int pthread_create(pthread_t thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
当pthread_create
成功返回时,新创建线程的线程 ID 会被存入thread
指向的内存单元。attr
用于定制不同的线程属性,这里我们设为NULL
,创建一个具有默认属性的线程。新创建的线程会从start_toutine
函数的地址开始运行,将函数的参数存入arg
指针指向的地址,如果参数数量大于1,那么需要把参数放入结构体中,然后传入结构体的指针。
我们写一个打印线程ID的简单小程序:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
/* print thread information */
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
/* thread function */
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main(void)
{
int err;
// create a new thread
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err) {
fprintf(stderr, "can't create thread: %s\n", strerror(err));
exit(1);
}
printids("main thread:");
sleep(1);
return 0;
}
编译运行结果如下:
$ gcc 35-1.c -lpthread -o thread-1
$ ./thread-1
main thread: pid 45 tid 1461106432 (0x5716b700)
new thread: pid 45 tid 1452906240 (0x56999700)
从上面的输出结果可以看到,每个线程都有一个线程ID,进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属进程上下文中才有意义。
线程终止
如果进程中的任意线程调用了exit
、_exit
、_Exit
,那么整个进程就会终止。在不终止整个进程控制的情况下,单个线程可以通过3种方式退出:
- 线程简单的从启动例程中返回
- 线程可以调用
pthread_cancel
,终止同一进程中的另一个线程 - 线程调用
pthread_exit
终止自己
#include <pthread.h>
void pthread_exit(void *value_ptr);
需要注意,pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc
分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
调用pthread_join
的线程将会阻塞,直到线程thread
结束。我们可以根据value_ptr
中的内容,查看线程的退出状态。
pthread_create
和pthread_join
函数可以通过结构指针传递不止一个参数。但是这个结构所使用的内存在调用完成后必须仍然有效。
线程可以调用pthread_cancel
函数来请求取消同一进程中的其他线程
#include <pthread.h>
int pthread_cancel(pthread_t thread);
线程的实际终止和pthread_cancel
函数调用时异步的。函数成功得到返回值,仅仅代表成功发出请求,并不代表线程已经终止。
线程同步
对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
用pthread_mutex_init
函数初始化的Mutex可以用pthread_mutex_destroy
销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER
来初始化,相当于用pthread_mutex_init
初始化并且attr
参数为NULL
。Mutex的加锁和解锁操作可以用下列函数:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
一个线程可以调用pthread_mutex_lock
获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock
获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock
释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock
,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。以下为一个简单的双线程计数器代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char *argv[])
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, doit, NULL);
pthread_create(&tidB, NULL, doit, NULL);
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
for (i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val+1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
以上程序的运行结果为10000,不会出现多线程访问冲突的情况。假如将Mutux部分注释掉运行程序,结果可能会出现错误。