linuxC++多线程程?


典型的UNIX系统都支持一个进程创建多个线程(thread)。在Linux进程基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实现方式有异于其它的UNIX系统,但Linux的多线程在逻辑和使用上与真正的多线程并没有差别。
1. 多进程
我们先来看一下什么是多线程。在Linux从程序到进程中,我们看到了一个程序在内存中的表示。这个程序的整个运行过程中,只有一个控制权的存在。当函数被调用的时候,该函数获得控制权,成为激活(active)函数,然后运行该函数中的指令。与此同时,其它的函数处于离场状态,并不运行。如下图所示:
Linux从程序到进程
我们看到,各个方块之间由箭头连接。各个函数就像是连在一根线上一样,计算机像一条流水线一样执行各个函数中定义的操作。这样的一个程序叫做单线程程序。
多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果。如下图所示,就是一个多线程的流程:
main()到func3()再到main()构成一个线程,此外func1()和func2()构成另外两个线程。操作系统一般都有一些系统调用来让你将一个函数运行成为一个新的线程。
回忆我们在Linux从程序到进程中提到的stack的功能和用途。由于stack中只有最下方的stack
frame可以被读取,所以我们只能有该frame对应的单一函数处于激活状态。为了实现多线程,我们必须绕开stack带给我们的限制。为此,当我们创建一个新的线程的时候,我们为这个线程创建一个新的stack。当该stack执行到全部弹出的时候,该线程完成任务并结束。所以,多线程的进程在内存中会有多个stack,相互之间以一定的空白区域隔开,以备stack的增长。每个线程随时可以使用自己stack中最下方的stack
frame中的参数和变量,并与其它线程共享内存中的Text,heap和global data区域。在上面的例子中,我们将在进程空间中有三个stack。
(要注意的是,对于多线程来说,由于同一个进程空间中存在多个stack,任何一个空白区域被填满都会导致stack overflow的问题。)
2. 并发
多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题。比如说,我们有一个多线程火车售票系统,用全局变量i存储剩余的票数。多个线程不断地卖票(i = i - 1),直到剩余票数为0。所以每个都需要执行如下操作:
/*mu is a global mutex*/while (1) {
/*infinite loop*/
if (i != 0) i = i -1
else {
printf("no more tickets");
exit();
}
}
如果只有一个线程执行上面的程序的时候(相当于一个窗口售票),则没有问题。但如果多个线程都执行上面的程序(相当于多个窗口售票), 我们就会出现问题。我们会看到,其根本原因在于同时发生的各个线程都可以对i读取和写入。
我们这里的if结构会给CPU两个指令, 一个是判断是否有剩余的票(i != 0), 一个是卖票 (i = i -1)。某个线程会先判断是否有票(比如说此时i为1),但两个指令之间存在一个时间窗口,其它线程可能在此时间窗口内执行卖票操作(i = i
-1),导致该线程卖票的条件不再成立。但该线程由于已经执行过了判断指令,所以无从知道i发生了变化,所以继续执行卖票指令,以至于卖出不存在的票 (i成为负数)。对于一个真实的售票系统来说,这将成为一个严重的错误 (售出了过多的票,火车爆满)。
在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清除哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件(race
condition),在这样的状况下,计算机的结果很难预知。我们应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分隔的一个原子操作(atomic operation),而其它任务不能插入到原子操作中。
3. 多进程同步(synchronization)
对于多线程程序来说,同步是指在一定的时间内只允许某一个线程访问某个资源 。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。
1) mutex
mutex是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。mutex一般被设置成全局变量。打开的mutex可以由某个线程获得。一旦获得,这个mutex会锁上,此后只有该线程有权打开。其它想要获得mutex的线程,会等待直到mutex再次打开的时候。我们可以将mutex想像成为一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上。其它人只能在mutex外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。
上面的问题很容易使用mutex的问题解决,每个进程的程序可以改为:
/*mu is a global mutex*/while (1) {
/*infinite loop*/
mutex_lock(mu);
/*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/
if (i != 0) i = i - 1;
else {
printf("no more tickets");
exit();
}
mutex_unlock(mu);
/*release mutex, make it unblocked*/
}
第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作。
需要注意的时候,如果存在某个进程依然使用原先的程序 (即不尝试获得mu,而直接修改i),mutex不能阻止该程序修改i,mutex就失去了保护资源的意义。所以,mutex机制需要程序员自己来写出完善的程序来实现mutex的功能。我们下面讲的其它机制也是如此。
2) condition variable
condition variable是另一种常用的变量。它也常常被保存为全局变量,并和mutex合作。
假设我们有这样一个状况: 我们有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,我们就通知相应的十个工人一起去喝啤酒。我们如何实现呢?我们可以让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险(其他工人会在该工人装修好房子和检查之间完成工作)。
/*mu: global mutex, cond: global codition variable, num: global int*/mutex_lock(mu)
num = num + 1;
/*worker build the room*/
if (num <= 10) {
/*worker is within the first 10 to finish*/
cond_wait(mu, cond);
/*wait*/
printf("drink beer");
}
else if (num = 11) {
/*workder is the 11th to finish*/
cond_broadcast(mu, cond);
/*inform the other 9 to wake up*/
}
mutex_unlock(mu);
通常, condition variable除了要和mutex配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。
我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu,并恢复线程的运行,我们会执行下一句prinft("drink beer")
(我们以此来代表喝啤酒)。此后直到mutex_unlock()就构成了另一个mutex结构。
那么如何让前面十个调用cond_wait()的线程得到通知呢?我们注意到if还有另一种可能,也就是修建好第11个房间的人负责调用cond_broadcast()。它会给所有调用cond_wait()的进程放送通知,以便让那些进程恢复运行。
Condition variable特别适用于多个线程等待某个条件的发生。如果不使用Condition variable,那么每个进程就需要不断尝试获得mutex并检查条件是否发生,这样大大浪费了系统的资源。
3) reader-writer lock
Reader-writer lock与mutex非常相似。r、RW lock有三种状态: 共享读取锁(shared-read), 互斥写入锁(exclusive-write lock), 打开(unlock)。后两种状态与之前的mutex两种状态完全相同。
一个unlock的RW lock可以被某个进程获取R锁或者W锁。
如果被一个进程获得R锁,RW lock可以被其它进程继续获得R锁,而不必等待该进程释放R锁。但是,如果此时有其它进程想要获得W锁,它必须等到所有持有共享读取锁的进程释放掉各自的R锁。
如果一个锁被一个进程获得W锁,那么其它进程,无论是想要获取R锁还是W锁,都必须等待该进程释放W锁。
这样,多个进程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。
我们需要同步并发系统,这为程序员编程带来了难度。但是多线程系统可以很好的解决许多IO瓶颈的问题。比如我们监听网络端口。如果我们只有一个线程,那么我们必须监听,接收请求,处理,回复,再监听。如果我们使用多线程系统,则可以让多个线程监听。当我们的某个线程进行处理的时候,我们还可以有其他的线程继续监听,这样,就大大提高了系统的利用率。在数据越来越大,服务器读写操作越来越多的今天,这具有相当的意义。多线程还可以更有效地利用多CPU的环境。
(就像做饭一样,不断切换去处理不同的菜。)
本文中所使用的程序采用伪C的写法。不同的语言有不同的函数名(比如mutex_lock)。这里关注的是逻辑上的概念,而不是具体的实现和语言规范。
总结:
multiple threads, multiple stacks
race condition
mutex, condition variable, RW lock

1. 多线程的优势
在多进程编程中,程序每处理一个任务,都需要创建一个进程进行处理,而每个进程在创建时都需要复制父进程的进程上下文,且有自己独立的地址空间,当只需要并发处理很小的任务时(如并发服务器处理客户端的请求),这种开销是很不划算的,且每个进程之间的变量并不共享,使得进程间的通信也很麻烦。
这时候多线程的优势就体现了出来,线程也有自己的上下文(thread context),但是只是一些如线程ID、栈、栈指针、程序计数器、通用寄存器之类的东西,所以创建线程的开销较小。这些一般都是在线程运行的函数里被指定,所以在每个线程中都是独立的。而所有线程依旧在这个进程内共享一片地址空间,包括它的代码、堆、共享库和打开的文件等。
2. 线程的创建
在下面的线程相关函数中都默认导入了<pthread.h>头文件,且在编译时链接了pthread库文件。
pthread即遵守Posix标准的多线程,它是C语言处理线程的一个标准接口。
Linux中通过pthread_create函数来创建线程,其定义为
int pthread_create(pthread_t* tidp, const pthread_attr_t* attr, (void*)(*start_run)(void*), void* arg);
创建线程函数无非做了两件事(对程序员来说,对操作系统来说做了很多),让操作系统在当前进程创建一个线程,将线程的相关信息写入第一个参数指向的pthread_t类型的线程类型中,这个类型在Linux内部的定义实际是unsigned long
int类型,代表线程ID;attr参数用于指定线程的一些属性;后两个参数则是做的另一件事——让创建后的线程去做什么事,start_run对应要做的事的函数指针,arg则是需要传入的参数,注意函数指针的参数和返回值都必须定义为void*类型。
3. 线程的退出
线程的退出中主要有两个函数配套使用,pthread_exit与pthread_join
void pthread_exit(void* retval);
pthread_exit在需要退出的线程中使用,使得线程显式地退出,当然线程也可以通过函数的return关键字隐式地退出。线程退出时允许携带一个返回值,这个值将被调用pthread_join的线程接收到。
在讲pthread_join函数前,需要讲一下Linux的线程状态。Linux线程状态分为joinable状态和unjoinable状态。如果是joinable状态的线程,则线程所占用的资源即使在线程return或pthread_exit时都不会释放,只有在其它线程中调用pthread_join函数才会释放。而unjoinable状态的线程的资源在线程退出时就会自动释放,不需要pthread_join函数进行手动释放。所以pthread_join操作的线程必须是joinable的。
至于线程创建时具体是什么状态可以通过pthread_create的attr属性进行指定,默认是joinable的。
更深入的讲,pthread_join执行的其实是线程的合并,将调用函数的线程与指定的线程进行合并,然后调用线程会等待退出线程的退出,退出后进行资源的释放,这一点从函数名的join中或许可以看出来。
int pthread_join(pthread_t* thread, void** retval);
pthread_join在等待线程退出的线程中使用,thread参数代表等待的线程的ID。需要声明的是这个函数执行后是阻塞的,目的是等待thread的退出,如果指定的线程已经退出了的话就会立刻返回。
int pthread_detach(pthread* thread);
该函数不会将调用线程与退出线程合并,也不会阻塞调用线程,只是在等待退出线程退出后将资源回收,效果与将线程设置为unjoinable相同。
4. 线程同步
4.1 互斥锁
在同一个进程中,不同线程的运行先后顺序对程序员来说是不确定的。在操作系统内部,内核在很短的时间内不断地在不同的线程上下文间进行切换。因此,当有多个线程操作一个临界资源时,需要程序员进行线程的同步。
这时就诞生了互斥锁(mutex),互斥锁允许同时有一个或多个线程拥有它(一般都是一个),当多个线程需要同时操作某临界资源时,它们需要先抢占互斥锁,抢到了互斥锁的线程就可以继续运行,没抢到的则被阻塞,直到有线程释放了互斥锁,则继续抢占。所以在线程抢占了互斥锁操作完临界资源后应该尽早释放互斥锁,避免其它线程阻塞过久。
互斥锁实现的方式有多种,程序员可以自己定义一个互斥锁,这里主讲pthread.h中提供的互斥锁接口。
互斥锁的主要操作函数有三个,首先定义一个互斥锁
pthread_mutex_t mutex;
pthread_mutex_init(pthread_mutex_t* mutex);
初始化一个互斥锁
pthread_mutex_lock(pthread_mutex_t* mutex);
抢占互斥锁,如果没有抢到则被阻塞
pthread_mutex_unlock(pthread_mutex_t* mutex);
释放互斥锁
4.2 条件变量
考虑这样一种场景,某一条线程需要临界资源满足某一种条件才对临界资源进行操作,如抢票系统要在票卖完后进行加票。则加票的线程需要一直进行互斥锁的抢占,抢占后只是检查票是否卖完了,但大部分时间是没卖完的,所以多了很多不必要的抢占。而在票真正卖完后,加票线程又可能需要抢占多轮才能抢到互斥锁,导致程序的延误。这些都是只用互斥锁的程序很难解决的,于是Linux提供了条件变量接口。
于是就产生了条件变量(condition variable),系统会将使用条件变量的线程阻塞,直到满足条件时才唤醒,唤醒后互斥锁将被当前线程锁定。
定义条件变量:pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* cond_attr);
初始化条件变量,cond_attr表示需要赋予条件变量的属性,在Linux man pages中有一段话
The LinuxThreads implementation supports no attributes for conditions, hence the cond_attr parameter is actually ignored.
所以一般置为NULL就好了。
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);
阻塞线程,等待条件变量响应后唤醒,唤醒后得到相应的互斥锁mutex(如果有多个阻塞线程需要抢占)。但是线程离开阻塞状态不一定是因为条件变量满足,也有可能遇到了中断信号或出现错误,这一点很重要。
int pthread_cond_timewait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
在pthread_cond_wait的基础上加入了阻塞时限。
int pthread_cond_destroy(pthread_cond_t* cond);
释放条件变量。
5. 线程池模板
最后提供一个线程池模板程序,我已在一个web server项目中成功使用。
由于线程的创建和销毁都需要消耗时间,在需要持续处理高并发任务时,通常线程在处理一个任务并不会直接退出,而是阻塞或者接着执行下一个任务,这就是线程池。
本线程池模板使用任务队列的架构,即线程池将所有接收到的任务放到一个任务队列中,然后所有线程领取任务队列中的任务进行执行。当然任务的领取需要处理线程的同步问题。
源码:
头文件
/*
* FILE: threadpool.h
* Copyright (C) Lunar Eclipse
* Copyright (C) Railgun
*/
#ifndef THREADPOOL_H
#define THREADPOOL_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdint.h>
#include "debug.h"
#define THREAD_NUM 8
typedef struct task_s {
void (*func)(void*);
//task function pointer
void *arg;
//function arguments
struct task_s* next;
//points to the next task in the task queue
} task_t;
typedef struct {
pthread_mutex_t lock; //mutex
pthread_cond_t cond; //condition variable
pthread_t* threads; //thread_t type array
task_t* head;
int thread_count; //thread number in the threadpool
int queue_size;
//task number in the task queue
int shutdown;
/*indicate if the threadpool is shutdown. Shutdown fall into two categories[immediate_shutdown, graceful_shutdown], immediate_shutdown means the threadpool has to shutdown no matter if there are tasks or not, graceful_shutdown will wait until all tasks are executed. */
int started; //number of threads started
} threadpool_t;
typedef enum {
tp_invalid = -1,
tp_lock_fail = -2,
tp_already_shutdown = -3,
tp_cond_broadcast = -4,
tp_thread_fail = -5,
} threadpool_error_t;
threadpool_t* threadpool_init(int thread_num);
int threadpool_add(threadpool_t* pool, void (*func)(void*), void* arg);
int threadpool_destory(threadpool_t* pool, int graceful);
#ifdef __cplusplus
}
#endif
#endif
源文件
/*
* FILE: threadpool.c
* Copyright (C) Lunar Eclipse
* Copyright (C) Railgun
*/
#include "threadpool.h"
typedef enum {
immediate_shutdown = 1,
graceful_shutdown = 2
} threadpool_st_t;
static int threadpool_free(threadpool_t* pool);
static void* threadpool_worker(void* arg);
threadpool_t* threadpool_init(int thread_num) {
if (thread_num <= 0) {
LOG_ERR("the arg of the threadpool_init must greater than 0");
return NULL;
}
threadpool_t* pool;
if ((pool = (threadpool_t*)malloc(sizeof(threadpool_t))) == NULL) {
goto ERR;
}
pool->thread_count = 0;
pool->queue_size = 0;
pool->shutdown = 0;
pool->started = 0;
pool->threads = (pthread_t*)malloc(sizeof(pthread_t) * thread_num);
pool->head = (task_t*)malloc(sizeof(task_t)); //dummy head
if ((pool->threads == NULL)
(pool->head == NULL)) {
goto ERR;
}
pool->head->func = NULL;
pool->head->arg = NULL;
pool->head->next = NULL;
if (pthread_mutex_init(&(pool->lock), NULL) != 0) {
goto ERR;
}
if (pthread_cond_init(&(pool->cond), NULL) != 0) {
pthread_mutex_destroy(&(pool->lock));
goto ERR;
}
int i;
for (i = 0; i < thread_num; i++) {
if (pthread_create(&(pool->threads[i]), NULL, threadpool_worker, (void*)pool) != 0) {
threadpool_destroy(pool, 0);
return NULL;
}
//output the thread id as an 8-bit hexadecimal number
LOG_INFO("thread: %08x started", (uint32_t)pool->threads[i]);
pool->thread_count++;
pool->started++;
}
return pool;
ERR:
if (pool) {
threadpool_free(pool);
}
return NULL;
}
//add a task to the threadpool, not thread
int threadpool_add(threadpool_t* pool, void (*func)(void*), void* arg) {
int res, err = 0;
if (pool == NULL
func == NULL) {
LOG_ERR("pool = NULL or func = NULL");
return -1;
}
if (pthread_mutex_lock(&(pool->lock)) != 0) {
LOG_ERR("pthread_mutex_lock");
return -1;
}
if (pool->shutdown) {
err = tp_already_shutdown;
goto OUT;
}
//TODO: use a memory pool
task_t* task = (task_t*)malloc(sizeof(task_t));
if (task == NULL) {
LOG_ERR("malloc task fail");
goto OUT;
}
//TODO: use a memory pool
task->func = func;
task->arg = arg;
task->next = pool->head->next;
pool->head->next = task;
pool->queue_size++;
res = pthread_cond_signal(&(pool->cond));
CHECK(res == 0, "pthread_cond_signal");
OUT:
if (pthread_mutex_unlock(&pool->lock) != 0) {
LOG_ERR("pthread_mutex_unlock");
return -1;
}
return err;
}
//free all threads in the threadpool
int threadpool_free(threadpool_t* pool) {
if (pool == NULL
pool->started > 0) {
return -1;
}
if (pool->threads) {
free(pool->threads);
}
task_t* node;
while (pool->head->next) {
node = pool->head->next;
pool->head->next = pool->head->next->next;
free(node);
}
return 0;
}
//destroy the threadpool
int threadpool_destroy(threadpool_t* pool, int graceful) {
int err = 0;
if (pool == NULL) {
LOG_ERR("pool is NULL");
return tp_invalid; //tp_invalid is in enum
}
if (pthread_mutex_lock(&(pool->lock)) != 0) {
return tp_lock_fail;
}
do {
if (pool->shutdown) {
err = tp_already_shutdown;
break;
}
pool->shutdown = (graceful) ? graceful_shutdown : immediate_shutdown;
if (pthread_cond_broadcast(&(pool->cond)) != 0) {
err = tp_cond_broadcast;
break;
}
if (pthread_mutex_unlock(&(pool->lock)) != 0) {
err = tp_lock_fail;
break;
}
int i;
for (i = 0; i < pool->thread_count; i++) {
if (pthread_join(pool->threads[i], NULL) != 0) {
err = tp_thread_fail;
}
LOG_INFO("thread %08x exit", (uint32_t)pool->threads[i]);
}
} while (0);
if (!err) {
pthread_mutex_destroy(&(pool->lock));
pthread_cond_destroy(&(pool->cond));
threadpool_free(pool);
}
return err;
}
static void* threadpool_worker(void* arg) {
if (arg == NULL) {
LOG_ERR("arg should be type threadpool_t");
return NULL;
}
threadpool_t* pool = (threadpool_t*)arg;
task_t* task;
while (1) {
pthread_mutex_lock(&(pool->lock));
//wait on condition variable, check for fake wakeups
while ((pool->queue_size == 0) && !(pool->shutdown)) {
pthread_cond_wait(&(pool->cond), &(pool->lock));
}
if (pool->shutdown == immediate_shutdown) {
break;
}
else if ((pool->shutdown == graceful_shutdown) && pool->queue_size == 0) {
break;
}
task = pool->head->next;
if (task == NULL) {
pthread_mutex_unlock(&(pool->lock));
continue;
}
pool->head->next = task->next;
pool->queue_size--;
pthread_mutex_unlock(&(pool->lock));
(*(task->func))(task->arg);
//TODO: memory pool
free(task);
}
pool->started--;
pthread_mutex_unlock(&(pool->lock));
pthread_exit(NULL);
return NULL;
}
参考资料
1. Linux man-pages
2. Linux中pthread_join和pthread_detach详解
3. Debian Linux man-pages

1.1 线程的概念
概念:线程是进程内部的一条执行序列或执行路径,一个进程可以包含多条线程。线程是进行资源调度和分配的基本单位 。(1)每个进程至少有一条执行路径,所以一个进程至少有一个线程。(2)每个进程都有一个主线程。
1.2 线程的实现方式
在操作系统中,线程的实现有以下三种方式:(1)用户级线程:由线程库中的代码进行管理,处理 ,销毁。用户自己创建的多线程,即多个处理路径,无法使用多处理器的资源,在内核眼里就只是一条路径。(2)内核级线程:由内核直接创建、直接管理、直接调度,直接结束。开销大,可以利用处理器的资源。(3)组合级线程:内核空间允许其使用多处理器的资源。比如用户创建多个线程,内核可以创建两个线程来处理这些线程,以达到可以有效使用处理器资源的目的。
1.3Linux 中线程的实现
Linux 实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的
task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。
1.4 进程与线程的区别
(1)进程是资源分配的最小单位,线程是 CPU 调度的最小单位。(2)进程有自己的独立地址空间,线程共享进程中的地址空间。(3)进程的创建消耗资源大,线程的创建相对较小。(4)进程的切换开销大,线程的切换开销相对较小。
2.1 线程库中的接口介绍
#include <pthread.h>
/*
pthread_create()用于创建线程
thread: 接收创建的线程的 ID
attr: 指定线程的属性//一般传NULL
start_routine:指定线程函数
arg: 给线程函数传递的参数
成功返回 0, 失败返回错误码
*/
int pthread_create(pthread_t * thread, const pthread_attr_t *attr,void *(*start_routine) ( void *),void *arg);
/*
pthread_exit()退出线程
retval:指定退出信息
*/
int pthread_exit( void *retval);
/*
pthread_join()等待 thread 指定的线程退出,线程未退出时,该方法阻塞
retval:接收 thread 线程退出时,指定的退出信息
*/
int pthread_join(pthread_t thread, void **retval);
2.2 多线程代码
如下简单写一个多线程代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
void* pthread_fun(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("fun run\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
int res = pthread_create(&tid,NULL,pthread_fun,NULL);
assert(res == 0);
for(int i = 0; i < 10; i++)
{
printf("main run\n");
sleep(1);
}
exit(0);
}
运行结果(注意编译链接需要带上库 -lpthread):如果子线程循环10次,主线程循环5次呢?代码及运行结果如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
void* pthread_fun(void* arg)
{
for(int i = 0; i < 10; i++)
{
printf("fun run\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
int res = pthread_create(&tid,NULL,pthread_fun,NULL);
assert(res == 0);
for(int i = 0; i < 5; i++)
{
printf("main run\n");
sleep(1);
}
exit(0);
}
运行结果:发现主线程结束后,子线程并没有打印完也紧跟着结束了。所以,主线程不会因为其他线程的结束而结束,但是其它线程的结束会因为主线程的结束而结束。这是因为主线程结束后会退出进程,所以进程里的其他线程都会终止结束。所以为了正常运行程序,一般我们都会让主线程等待其他线程结束后再结束。代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
void* pthread_fun(void* arg)
{
for(int i = 0; i < 10; i++)
{
printf("fun run\n");
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
int res = pthread_create(&tid,NULL,pthread_fun,NULL);
assert(res == 0);
for(int i = 0; i < 5; i++)
{
printf("main run\n");
sleep(1);
}
char* s = NULL;
pthread_join(tid,(void**)&s);
exit(0);
}
运行结果:其他线程没有结束的话,主线程会在pthread_join()处阻塞。所以主线程会在其他线程结束之后再结束,程序正常退出。
2.3 线程并发运行
示例代码 1:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
void* pthread_fun(void* arg)
{
int index = *(int*)arg;
int i = 0;
for(; i < 5; i++)
{
printf("index = %d\n",index);
sleep(1);
}
}
int main()
{
pthread_t id[5];
int i = 0;
for(; i < 5; i++)
{
pthread_create(&id[i],NULL,pthread_fun,(void*)&i);
}
for(i = 0; i < 5; i++)
{
pthread_join(id[i],NULL);
}
exit(0);
}
运行结果1:运行结果2:为什么会产生这种情况呢?线程并发问题。这是因为我们向pthread_fun传入i的地址。首先来说说为什么会出现多个线程拿到同一个i的值。线程创建在计算机中需要很多个步骤,我们进入for循环传入i的地址后就去进行下一个for循环,创建的线程还没有从地址中获取打印i的值,主函数就继续创建后面的线程了,导致多个线程并发,拿到同一个i值,而且不是创建该线程的时候i的值。注意到打印第一个运行结果都是打印0,这是因为主函数第一个for循环已经结束了,后面一个for循环将i又置为0,而这些线程在主函数第一个for循环执行的时候,都没有回获取i的值打印,直到下一个for循环,这些线程才获取i值打印,所以打印出来 都是0。
示例代码 2:多线程并发访问同一块内存的问题
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
int g = 0;
void* pthread_fun(void* arg)
{
for(int i = 0; i < 1000; i++)
{
printf("g = %d\n",++g);
}
pthread_exit(NULL);
}
int main()
{
pthread_t id[5];
for(int i = 0; i < 5; i++)
{
pthread_create(&id[i],NULL,pthread_fun,NULL);
}
for(int j = 0; j < 5; j++)
{
char* s = NULL;
pthread_join(id[j],(void**)&s);
}
exit(0);
}
三次运行结果:
运行结果最后可能是5000,也可能是4900多,这是怎么回事呢?看一下本人的虚拟机设置,处理器数量2个,每个处理器2个内核。原因就是linux的线程是内核级线程。程序中对g++并不是原子操作,对g++,计算机需要 很多次操作 ,比如将内存中的g读取到寄存器中,再从寄存器中读走进行++,再回头进行写入等等一系列操作。可能一个线程拿到了内存中的g,还没来得及++再写回去,另一个线程被分配到另一个处理器上,读取了相同值的g进行++。所以我们得到的值有时候会比5000要小。解决方法有:(1)将处理器设置为单核处理器;(2)进行线程同步。

我要回帖

更多关于 C++多线程 的文章

 

随机推荐