多线程编程
线程和进程的比较:
- 它是一种非常"节俭"的多任务操作方式优点。
Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立
众多的数据表来维护它的代码段、堆栈段和数据段,这是一种“昂贵”的多
任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右。 - 线程间方便的通信机制。
对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通
过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
总结:两者各有优劣,
线程执行开销小,而不利于资源的管理和保护,而进程则相反。
多线程的优点:
不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。 这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(timeconsuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。 操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。 一个既长又复杂的进程可以考虑分为多个线程,成
为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
消息机制
可以再查找一些资料,查查消息机制
多核与超线程
线程
概念
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
• 线程是进程中的一个实体,是CPU调度和分配的基本单位
线程共享资源
除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_IGN、 SIG_DFL或者自定义的信号处理
函数)
当前工作目录
用户id和组id
线程独享资源
线程id
• 上下文,包括各种寄存器的值、程序计数器和栈指针
• 栈空间
• errno变量
• 信号屏蔽字
• 调度优先级
线程与进程的区别
可归纳如下四点:
- 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
- 在多线程OS中,进程不是一个可执行的实体。
线程库
◼ 在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项。
创建线程
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
返回值:成功返回0,失败返回错误号。
thread: 线程标识符
attr: 线程属性设置,没有特殊设定,设置为NULL
start_routine:线程函数起始地址
arg: 传递给start_routine的参数
说明一下上述的restrict关键字
关键字restrict只用于限定指针;
该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。
练习:
创建线程的例子
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
//◼ thread_t类型是一个地址值,属于同一进程的多个线程调用getpid(2)可以得到相同的进程号,而调用pthread_self(3)得到的线程号各不相同。
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid(); /*得到进程ID*/
tid = pthread_self(); /*得到线程ID*/
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err != 0)
{
//◼ 由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。
fprintf(stderr, "can't create thread: %s\n", strerror(err));
exit(1);
//◼ **如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,从main函数return也相当于调用exit。**
}
printids("main thread:");
sleep(1);
return 0;
}
终止线程
return\pthread_cancel\pthread_exit
◼ 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
◼ 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
◼ 线程可以调用pthread_exit终止自己。
return | pthread_cancel | pthread_exit | pthread_join | |
---|---|---|---|---|
返回值 | 从线程函数return。 | 成功返回0,失败返回错误编号 | 无 | |
这种方法对主线程不适用,从main函数return相当于调用exit。 | 一个线程去杀掉别的线程 | 自杀 | 等待线程退出 | |
区别 | 不等待线程终止,仅仅提出请求 | 将线程挂起等待,等到对应的线程来到再将其终止 |
线程退出
#include <pthread.h>
void pthread_exit( void *retval )
retval: pthread_exit调用者线程的返回值,可由其他函数和pthread_join来检测获取。
注意, pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
等待线程退出
#include <pthread.h>
int pthread_join( pthread_t th, void ** value_ptr)
th: 等待线程的标识符
value_ptr : 用户定义指针,用来存储被等待线程的返回值,
调用该函数的线程将挂起等待,直到id为thread的线程终止。
线程以调用不同的终止方法,pthread_join里value_ptr是有不同的状态
◼如果thread线程通过return返回, value_ptr所指向的单元里存放的是thread线程函数的返回值。
◼如果thread线程被别的线程调用pthread_cancel异常终止掉, value_ptr所指向的单元里存放的是常数
PTHREAD_CANCELED。
#define PTHREAD_CANCELED ((void *) -1)
◼如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
◼如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
return | pthread_cancel | pthread_exit | NULL | |
---|---|---|---|---|
存放value_ptr的状态 | 存放的是返回thread线程函数的返回值 | 存放常数 | 存放的是传给pthread_exit的参数 | 不感兴趣就写NULL |
例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void* thr_fn1(void* arg)
{
printf("thread 1 returning\n");
return (void*)1;
}
void f(void)
{
printf("thread 2 exiting\n");
pthread_exit((void*)2);
}
void* thr_fn2(void* arg)
{
f();
while (1)
{
;
}
}
void* thr_fn3(void* arg)
{
while (1)
{
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t pthreadid;
void* tret;
pthread_create(&pthreadid, NULL, thr_fn1, NULL);
pthread_join(pthreadid, &tret);
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&pthreadid, NULL, thr_fn2, NULL);
pthread_join(pthreadid, &tret);
printf("thread 2 exit code %d\n", (int)tret);
pthread_create(&pthreadid, NULL, thr_fn3, NULL);
sleep(3);
pthread_cancel(pthreadid);
pthread_join(pthreadid, &tret);
printf("thread 3 exit code %d\n", (int)tret);
return 0;
}
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。
那么能不能线程一旦终止就立马回收其资源,不进行等待获取状态呢?
detach状态
线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。
注意:如果一个线程已经调用detach,就不能再调用pthread_join,否则返回EINVAL。。对于一个没有调用detach的线程来说可以调用pthread_join或pthread_detach来把该线程设置成detach状态。也就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值: 成功返回0,失败返回错误号。
线程间同步
线程同步要解决什么问题?
解决访问冲突的问题。
互斥锁
怎么解决?
引入互斥锁。
互斥锁的功能:获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
插入:什么是原则操作呢?
例子:i = i+1,这不是一个原子操作,有取值,赋值,(具体完了再补充完整)每次都是分开操作,如果有多个线程进来操作的话就会导致冲突,因为分时操作,同一个时间段内每个线程都能分配到一定的时间来执行,所以很容易被其他线程执行导致冲突。
互斥锁解决步骤
初始化pthread_mutex_init——>上锁pthread_mutex_lock——>判断pthread_mutex_trylock——>解锁pthread_mutex_unlock——>销毁pthread_mutex_destroy
mutex的初始化和销毁
mutex用pthread_mutex_t类型的变量表示
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
如果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);
返回值:成功返回0,失败返回错误号。
pthread_mutex_lock得挂起等待
如果一个线程先调用pthread_mutex_lock获得一个锁Mutex后,又有一个线程调用pthread_mutex_lock它,则必须等待先前调用的那个锁释放之后才能使用该锁。(当前线程会挂起的等待,知道先前锁调用pthread_mutex_unlock释放后才能获得该锁Mutex并继续执行)。
pthread_mutex_trylock,不用挂起等待
如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
锁的例子:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 10
int counter; /* incremented by threads */
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);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void* doit(void* vptr)
{
int i, val;
/*
* Each thread fetches, prints, and increments the counter NLOOP times.
* The value of the counter should increase monotonically.
*/
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;
}
该程序会等待线程1执行完后再执行线程2,如果不加锁,则会两个线程交替执行。运行如下,加锁后
不加锁
分析mutex的实现
lock:
if(mutex > 0)
{
mutex = 0;
return 0;
} else
挂起等待;
goto lock;
unlock:
mutex = 1;
唤醒等待Mutex的线程;
return 0;
死锁
如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了。(就是自己锁了两次,等不到第一次解锁,而使得自己一直处于挂起等待的状态。)
避免死锁的原则
所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。
举例说明:比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。
或者可以调用pthread_mutex_trylock代替pthread_mutex_lock避免死锁。
条件变量(Condition Variable)
条件不成立,就阻塞等待。
线程间的同步的另外一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。
在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。
条件变量的初始化和销毁
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
**pthread_cond_t cond = PTHREAD_COND_INITIALIZER;**
返回值:成功返回0,失败返回错误号
pthread_cond_init函数初始化一个Condition Variable,attr参数为NULL则表示缺省属性
pthread_cond_destroy函数销毁一个Condition Variable。
条件变量的操作
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//阻塞等待 唤醒
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
//阻塞等待 唤醒,比pthread_cond_wait多加一个超时时间,超时则返回ETIMEDOUT
int pthread_cond_broadcast(pthread_cond_t *cond);
//唤醒在条件变量上等待的所有线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒在条件变量上等待的另一个线程
返回值:成功返回0,失败返回错误号。
使用方法:
一个条件变量总是和一个mutex搭配使用,一个线程调用pthread_cond_wait在一个条件变量上阻塞等待。
pthread_cond_wait 做一下三步骤:
- 释放Mutex
- 阻塞等待
- 当被唤醒时,重新获得Mutex并返回
条件变量示例代码:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg
{
int num;
struct msg *next;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
{
pthread_cond_wait(&has_product, &lock);//等待调用pthread_cond_siganl的唤醒
}
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);//唤醒等待pthread_cond_wait的另一个线程
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
Posix信号量(同步原语)
信号量(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。
信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
Posix信号量分为有名信号量和无名信号量(也叫基于内存的信号量)
使用方法:
信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。
Posix有名信号量
使用范围:进程和线程都适用。
sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
由sem_open来创建一个新的信号量或打开一个已存在的信号量。
返回:若成功则为指向信号量的指针,若出错则为SEM_FAILED
主要看第二个参数 oflag的取值,第三、四个参数可以没有
oflag : 可以是0、 O_CREAT()或O_CREAT|O_EXCL。
指定为O_CREAT() 时,只有当所需的信号量尚未存在时才初始化它。但是如果所需的信号量已经存在也不会出错。
指定为O_CREAT|O_EXCL时如果存在则会报错。
mode参数:指定权限位
value参数:指定信号量的初始值。该初始值不能超过SEM_VALUE_MAX(这个常值必须至少为32767) .二值信号量的初始值通常为1,计数信号量的初始值则往往大于1。
2) sem_close来关闭该信号量。
3) 使用sem_unlink删除信号量:
int sem_unlink(const char *name);
返回:成功返回0,出错返回-1
4) 获取信号量的当前值:
int sem_getvalue(sem_t *sem,int *valp);
返回:成功返回0,出错返回-1
sem_getvalue在由valp指向的整数中返回所指定信号量的当前值。如果
信号量当前已上锁,那么返回值或为0,或为某个负数,绝对值即为等待
等待该信号量解锁的线程数
5)信号量的等待: (P操作,也称为递减down 或 上锁lock)
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
返回:成功返回0,出错返回-1
sem_wait函数测试所指定信号量的值,如果该值大于0,就将它的值减1
并立即返回;如果该值等于0,调用线程就被投入睡眠中,直到该值变为
大于0,这时再将它减1,函数随后返回。“测试并减1” 操作必须是原
子的。(>0 -1, =0 睡眠)
sem_wait和sem_trywait的差别是:当所指定信号量的值已经是
0时,后者并不将调用的进程投入睡眠。相反,它返回一个EAGAIN错误。
如果被某个信号中断, sem_wait就可能过早的返回,返回的错误为
EINTR。
5)信号量挂出: (V操作,也称为递增up 或解锁unlock)
int sem_post(sem_t *sem);
返回:成功返回0,出错返回-1 将所指定的信号量值加1
采用Posix信号量实现生产者-消费者问题
对生产者-消费者问题进行扩展,把共享缓冲区用作一个环绕缓冲区,即生产者填写最后一项后回头来填写第一项,消费者也这么操作。此时需要维持三个条件:
(1)当缓冲区为空时,消费者不能试图从其中去除一个条目
(2)当缓冲区填满时,生产者不能试图往其中放置一个条目
(3)共享变量可能描述缓冲区的当前状态(下标、计数和链表指针),因此生产者和消费者的所有缓冲区操作都必须保护起来,以避免竞争。给出使用信号量的方案展示三种不同类型的信号量:
(1)定义mutex二元信号量保护两个临界区。
(2)定义nempty的计数信号量统计共享缓冲区中的空槽位数。
(3)定义nstored的计数信号量统计共享缓冲区中已填写的槽位数。
Posix基于内存的信号量
Posix有名信号量创建时候是用一个name参数标识,它通常指代文件系
统中的某个文件。
基于内存的信号量是由应用程序分配信号量的内存空间,即分配一个sem_t数据类型的内存空间,然后由系统初始化它们的值。
操作函数如下:
#include <semaphore.h>
//初始化内存信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
pshared:pthread=0时,进程内 线程之间共享,否则,进程间共享,此时该信号量必须存放在某种类型的共享内存区中。
value:该信号量的初始值
//摧毁信号量
int sem_destroy(sem_t *sem);
信号量(semaphore)
信号量(semaphore)和mutex类似,表示可用资源的数量,和mutex不同的是这个数量可以大于1。
semaphore变量的类型为sem_t
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem_init()初始化一个semaphore变量, value参数表示可用资源的数量,
//pshared参数为0表示信号量用于同一进程的线程间同步
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
信号量示例代码:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;
void *producer(void *arg)
{
int p = 0;
while (1)
{
sem_wait(&blank_number);
queue[p] = rand() % 1000 + 1;
printf("Produce %d\n", queue[p]);
sem_post(&product_number);
p = (p+1)%NUM;
sleep(rand()%5);
}
}
void *consumer(void *arg)
{
int c = 0;
while (1)
{
sem_wait(&product_number);
printf("Consume %d\n", queue[c]);
queue[c] = 0;
sem_post(&blank_number);
c = (c+1)%NUM;
sleep(rand()%5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 0, 0);
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}