线程概念
我们都知道,进程拥有独立的地址空间,进程之间共享数据需要通过进程间通信机制来实现。然而进程是单一执行流的,有些时候需要在同一个进程中执行多个控制流程,这时候线程就派上了用场。
比如实现一个图形界面的下载软件,一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件,等待和处理从多个网络主机发来的数据,这些任务都需要一个“等待-处理”的循环,可以用多线程实现,一个线程专门 负责与用户交互,另外几个线程每个线程负责和一个网络主机通信。
以前学过都信号信号捕捉也是一个进程中多个执行流的例子,但是线程却更加灵活。信号处理函数的控制流程只是在信号递达时产生,在处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。由于同一进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一 个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工工作目录
- 用户id和组id
但是也有其私有的部分:
- 线程id
- 硬件上下文,包括各种寄存器的值、程序计数器和栈指针
- 栈空间
- errno变量
- 信号屏蔽字
- 调度优先级
由于同一进程创建的线程都运行在当前的地址空间,所以自然可以直接看到公共的资源,那么线程之间的通信也就不难,唯一需要考虑的也就剩下了同步与互斥问题。后面再说。在Linux底下,可以认为,线程就是轻量级的进程,它也没有专有的数据结构用来描述线程,用的还是PCB,在这里,是用进程来模拟线程。那么在操作系统看来,我调度的都是进程,在用户层面上,通过库将进程模拟实现为线程,才呈现在我们的眼前。
线程的创建
第一个参数是一个输出型参数,用来保存线程id,第二个参数用来定制线程的相关属性,不过一般我们不关心它,设置为NULL即可。第三个参数是一个函数指针,指向我们要线程执行的代码体。这个函数有一个void * 型的参数,是为了让它能够接受任意类型。如果想给这个函数传递参数,那么就可以通过第四个参数arg来传参了,底层创建线程时,会自动将这个参数传递给线程调用的函数。如果创建成功返回0,否则返回错误码。
正所谓有创建,就有销毁:
线程的销毁
线程的销毁也就是线程的终止,之前也有学过,一个进程终止的方式有8种,5种正常终止,3种异常终止。现在说的是线程的终止,有三种:
(只终止线程而不终止进程)
- 从启动例程返回(当然主线程要是return了进程也就over了)
- 同进程中的一个线程取消另一个线程,pthread_cancel函数
- 线程终止自己:pthread_exit函数
进程有等待,需要回收,线程也一样,不然也会造成类似与僵尸进程那样的问题。
线程的等待
第一个参数直接是传需要等待的线程,第二个参数是该线程的返回值。线程等待这个函数是阻塞的,这就意味着如果调用pthread_join函数的时候,该线程还在运行,那么当前线程就会HANG住,也就是卡在那里死等,知道你退出为止,才回收掉它。不免浪费了许多时间。有时候我们不想这样,于是有了线程分离。
线程的分离
在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。
默认情况下,线程被创建成可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。
直接传参线程id,有主线程或者该线程自己调用都可以。
栗子:
运行结果:#include <stdio.h> #include <unistd.h> #include <pthread.h> void* pthread_run(void* arg) { printf("i am new pthread\n"); sleep(1); pthread_exit((void*)1); } int main() { pthread_t id; pthread_create(&id, NULL, pthread_run, NULL); printf("i am main pthread\n"); sleep(3); if (pthread_join(id, NULL) == 0) printf("join success\n"); return 0; }
detach之后在join:
#include <stdio.h> #include <unistd.h> #include <pthread.h> void* pthread_run(void* arg) { // pthread_detach(pthread_self()); printf("i am new pthread\n"); sleep(1); pthread_exit((void*)1); } int main() { pthread_t id; pthread_create(&id, NULL, pthread_run, NULL); pthread_detach(id); printf("i am main pthread\n"); sleep(3); void* ret; if (pthread_join(id, &ret) == 0) printf("join success: exit code: %d\n", *(int*)ret); else printf("join failed: exit code: %d\n", *(int*)ret); return 0; }
运行结果:
等待错误,退出码依然拿到了,但是不可靠,要看具体实现。总之,分离之后,无需再等。
线程的同步与互斥
多个线程访问同一份资源,有可能导致冲突,自然引入了同步与互斥问题。
比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:
1. 从内存读变量值到寄存器
2. 寄存器的值加1
3. 将寄存器的值写回内存
假设两个线程在多处理器平台上同时执行这三条指令,则可能由于时序问题导致得到错误的结果,但逻辑本身是正确的。对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,MutualExclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
栗子:
没有引入互斥机制:
#include <stdio.h> #include <unistd.h> #include <pthread.h> int g_val = 0; void* pthread_run(void* arg) { // pthread_detach(pthread_self()); printf("i am new pthread\n"); int i = 0; while (i++ < 5000) { int tmp = g_val; printf("id: %lu, g_val = %d\n", pthread_self(), g_val); g_val = tmp + 1; } pthread_exit((void*)1); } int main() { pthread_t id1, id2; pthread_create(&id1, NULL, pthread_run, NULL); pthread_create(&id2, NULL, pthread_run, NULL); pthread_join(id1, NULL); pthread_join(id2, NULL); printf("g_val = %d\n", g_val); return 0; }
输出结果:
引入同步机制后:
lock/unlock伪代码:
死锁
死锁:http://blog.csdn.net/qq_33724710/article/details/51960443
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。保证了原子性。
一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此 就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,有没有可能死锁的问题将会变得复杂和难以判断。
写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。
比如一个程序中用用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock 调用,以免死锁。
条件变量
1. 相关函数
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(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 *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);2. 说明
条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
条件变量要和互斥量相联结,以避免出现条件竞争——一个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件。pthread_cond_init 使用 cond_attr 指定的属性初始化条件变量 cond,当 cond_attr 为 NULL 时,使用缺省的属性。LinuxThreads 实现条件变量不支持属性,因此 cond_attr 参数实际被忽略。
pthread_cond_t 类型的变量也可以用 PTHREAD_COND_INITIALIZER 常量进行静态初始化。
pthread_cond_signal 使在条件变量上等待的线程中的一个线程重新开始。如果没有等待的线程,则什么也不做。如果有多个线程在等待该条件,只有一个能重启动,但不能指定哪一个。
pthread_cond_broadcast 重启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。
pthread_cond_wait 自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。
互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。例子:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <iostream> #include <queue> using namespace std; queue<int> q; static pthread_mutex_t con_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t pro_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t need_product=PTHREAD_COND_INITIALIZER; void *consumer(void *_val) { while (1) { pthread_mutex_lock(&lock); while (q.empty()) { pthread_cond_wait(&need_product, &lock); } pthread_mutex_lock(&con_lock); int tmp = q.front(); q.pop(); pthread_mutex_unlock(&con_lock); printf("comsum success, val is : %d\n", tmp); pthread_mutex_unlock(&lock); sleep(8); } return NULL; } void *product(void *_val) { while (1) { sleep(rand()%2); pthread_mutex_lock(&lock); int tmp = rand() % 1000; pthread_mutex_lock(&pro_lock); q.push(tmp); pthread_mutex_unlock(&pro_lock); printf("call consumer! product success, val is : %d\n", tmp); pthread_mutex_unlock(&lock); pthread_cond_signal(&need_product); sleep(1); } } int main() { pthread_t t_product, t_product2; pthread_t t_consumer, t_consumer2; pthread_create(&t_product, NULL, product, NULL); pthread_create(&t_product2, NULL, product, NULL); pthread_create(&t_consumer, NULL, consumer, NULL); pthread_create(&t_consumer2, NULL, consumer, NULL); pthread_join(t_product, NULL); pthread_join(t_consumer, NULL); pthread_join(t_product2, NULL); pthread_join(t_consumer2, NULL); return 0; }
运行结果:
POSIX信号量
运行结果:#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <semaphore.h> #define CAP 128 int ring[128]; sem_t sem_space; sem_t sem_data; pthread_mutex_t pro_lock; pthread_mutex_t con_lock; void* product(void* arg) { int index = 0; while (1) { sem_wait(&sem_space); int val = rand() % 100; pthread_mutex_lock(&pro_lock); ring[index++] = val; pthread_mutex_unlock(&pro_lock); index %= CAP; printf("product done ... , %d, %lu\n", val, pthread_self()); sem_post(&sem_data); sleep(1); } } void* consume(void* arg) { int index = 0; while (1) { sem_wait(&sem_data); pthread_mutex_lock(&con_lock); int val = ring[index++]; pthread_mutex_unlock(&con_lock); index %= CAP; printf("consume done...%d, %lu\n", val, pthread_self()); sem_post(&sem_space); sleep(1); } } int main() { pthread_t pro, pro2; pthread_t con, con2; sem_init(&sem_space, 0, CAP); sem_init(&sem_data, 0, 0); pthread_mutex_init(&pro_lock, NULL); pthread_mutex_init(&con_lock, NULL); pthread_create(&pro, NULL, product, NULL); pthread_create(&con, NULL, consume, NULL); pthread_create(&pro2, NULL, product, NULL); pthread_create(&con2, NULL, consume, NULL); pthread_join(pro, NULL); pthread_join(con, NULL); pthread_join(pro2, NULL); pthread_join(con2, NULL); return 0; }