总结面试1.多线程与资源同步

一.进程线程基本概念和常见问题

进程是计算机中实际运行的一个程序。每个进程都有自己的独立空间和上下文堆栈,进程为线程分配资源,线程才是进程中执行的基本单位。

1.主线程推出,支线程也将退出吗?

window系统是这样,Linux不会,主线程退出,子线程会变成僵尸线程。

2.某个线程崩溃,会导致进程推出吗?

每个线程都是独立执行的单位,都有自己的上下文堆栈,一个线程的崩溃不会影响其他线程。但是通常之情况下,线程的崩溃会导致进程的退出,进程退出会导致其他线程退。

二.线程的基本操作

1.线程的创建。

在Linux平台上,使用pthread_create创建

int pthread_create(pthread_t *thread, const pthread_attr_t* attr, void*(*start_routine)(void*),void*arg)

thread是创建成功后分配给它的线程编号,atrr是线程属性,一般指定为0,start_routine是线程运行的函数,arg是传入函数的值。

2.获取线程的ID

虽然可以通过thread知道线程的ID,但是一般情况下我们需要知道当前线程的ID,获取当前线程ID的方法是:

pthread_t pthread_self(void)

在Linux系统中,我们可以使用pstack查看线程的使用情况,它会显示堆栈。

3.等待线程的结束

在linux下等待线程结束的函数为

int pthread_join(pthread_t thread, void** retval);

void pthread_exit(void* val_ptr);

pthread_join函数会挂起当前线程等待目标线程退出,直到被等待的线程退出后才会被唤醒继续执行,retval可以获取线程的退出码.

在c++11中提供了join方法等待线程结束,但是前提要求是线程也处于运行状态,如果线程不存在就会报错,c++11提供了joinable方法来判断是否可以join,如果不可以就会返回false.

4.c++将对象实例指针作为线程函数的参数

LINUX的线程函数是固定,它的形式是

void*(*start_routine)(void*)

如果c++把这个函数封装为类的一个函数,会怎么样?

class Thread{
public:
    Thread();
    ~Thread();
    void* threadFunc(void*);

}

c++编译器会把它看作void* threadFunc(Thread* this, void*);

我们在外使用pthread_create就没办法调用它了。

因此我们不能把线程函数作为类的实例函数,但是我们可以把它用作类的静态函数。

但是如果使用c++提供的线程对象,就没有问题了,但是我们必须显式的把线程函数所属的类对象实例指针作为构造函数传递给std::thread。

class Thread{
public:
    Thread();
    ~Thread();
    void fun()
    {
        //线程对象的构造函数第一个参数必须是线程函数地址。
        //第二个参数必须显式的传入对象实例指针(this)
        thread_ptr = new(std::thread(&Thread::threadFunc,this, 100, 200));
        
    }

    //线程函数作为类的内部函数
    void ThreadFunc(int a, int b)
    {
        std::cout << a << " " << b << std::endl;    
    }
private:
    std::shared_ptr thread_ptr;//智能指针

}

为什么这样,而不是

thread_ptr = new(std::thread(&Thread::threadFunc,100, 200));

根据上面的解释好好思考就知道。

如果我们把类的静态函数作为线程函数,那么我们就可以在Linux的线程创建函数中使用它了,但是问题是,使用静态函数,我们就无法调用类的其他成员函数,怎么办呢?

我们可以把this指针作为静态函数的参数,然后转换回来。

class Thread{
    Thread();
    ~Thread();
    static threadFunc(void* ptr)
    {
        Thread* pthread = static_cast(ptr);
        ......

    }


}

最后,我们还可以用bind方法把类的实例方法作为线程函数

class Thread{

    void* thread(void* arg){

    }

    Thread(){
        std::thread(std::bind(&Thread::thread, this));

    }

}

c++还有原子操作,这个我在其他页面说过了。

5.Linux进程服务

1.Linux互斥体

pthread_mutex_t类型表示互斥体对象

有两种初始化方法

(1).静态初始化

pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZAR;

(2).动态初始化

int pthread_mutex_init(pthread_mutex_t* restrict metux, const pthread_mutexattr_t* restrict attr);

int pthread_mutex_init(mymutex,MULL)//设置为空表示默认属性,它可以用带属性的锁初始化

 如果互斥量动态创建,就需要用到动态初始化

2.销毁一个互斥体

int pthread_mutex_destroy(pthread_mutex_t* mutex);

无需销毁静态初始化对象,不能销毁一个已经被加锁的互斥体对象

3.加锁解锁

int pthread_mutex_lock(pthread_mutex_t* mutex);

int pthread_mutex_unlock(pthread_mutex_t* mutex);

int pthread_mutex_trylock(pthread_mutex_t* mutex);

 4.带属性的锁

带属性的锁对象是pthread_mutexattr_t

1.API

int pthread_mutexattr_init(pthread_mutexattr_t* attr)//初始化

int pthread_mutexattr_settype(pthread_mutexattr_t* attr);//设置属性

int pthread_mutexattr_gettype(pthread_mutexattr_t* attr);//获取属性

int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);//删除锁

2.属性取值

它的属性一般取如下

(1),PTHREAD_MUTEX_NORMAL:普通锁

(2).PTHREAD_MUTEX_ERRORCHECK:检错锁

设置这个属性后,如果重复调用,就会返回一个错误码

(3).PTHREAD_MUTEX_RECURSIVE:可重入锁

运行多个线程对同一把锁加锁,每次增加一个线程对锁lock一次,引用计数就会增加1,unlock一次就减少1

5.信号量

根据资源给定信号量,然后让其他线程前来消费

Linux给定的一系列信号量API如下:

#include

(1).int sem_init(sem_t* sem, int pshare, unsigned int value);

初始化信号量。pshare代表不同的进程是否共享,取值为0不共享,value代表信号量大小

(2).int sem_destroy(sem_t* sem);

消费信号量

(3).int sem_post(sem_t* sem);

信号量+1

(4).int sem_wait(sem_t* sem);

信号量-1,如果信号量为0,则将线程阻塞再这里。

(5).int sem_trywait(sem_t* sem);

不阻塞版本的sem_wait,如果信号量为0,线程则立刻返回,如果大于0,则信号-1

(6).int sem_timewait(sem_t* sem, const timespec* abs_timeout);

如果信号量大于0,则信号量-1,线程运行,如果信号量等于0,则等待设置的abs_timeout时间,再等待的时间里如果没有信号量,则线程退出。

6.条件变量

1.为什么需要条件变量

我们有了锁还有什么不满足的吗?

这是因为我们如果进入临界需要判断某种条件满足,然后进行操作,这个过程之前需要加锁,这个过程之后解锁,那么就显得效率低下。如果我们加锁进入临界区不满足,就要让出锁,岂不是白白浪费了一次获取锁的机会,无缘无故加了一次锁缺什么也没做。

如果我们可以做到这样:我们加锁进入临界区,然后等待条件满足,如果条件满足,则开始运行,如果不满足则进入睡眠,档条件满足的时候,就被唤醒。

这就是条件变量。

2.条件变量要与互斥量一起使用

为什么要一起使用?

因为如果不一起使用,就如下面一段代码

pthread_mytext_lock(&m)

while(条件)

        pthread_mutex_unlock(&m)

        条件唤醒

        pthread_metux_lock(&mutex);

end

如果正好运行到条件唤醒前面一行,一个线程突然杀出,cpu切换,这个线程唤醒了某个条件,则另一个线程完美错过,那么它将会永远阻塞再这里。所以这个步骤必须是一个原子性的。

3.条件变量的使用

(1).初始化和销毁

(1).int pthread_cond_init(pthread_mutex_t* cond,const pthread_condattr_t* attr);

初始化

静态初始化pthread_cond_t cond = PTTHREAD_COND_INITIALIZER

(2).int pthread_cond_destroy(pthread_mutex_t* cond);

销毁

(2).等待条件被唤醒

(1).int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);

等待条件被唤醒,如果条件没有被唤醒,则一直沉睡

(2).int pthread_cond_timewait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);

这是上面的函数的等待时间版本,时间到了还没有满足条件,则直接跳出

等待的线程可以被唤

(1).int pthread_cond_signal(pthread_cond_t* cond)

一次唤醒一个线程,如果有多个线程等待,可以认为是随机唤醒。

(2).int pthread_cond_broadcast(pthread_cond_t* cond);

唤醒所有等待的线程

我忘了说明前面的返回值了,默认都是返回0代表调用成功,返回负数代表失败,一般还会返回错误码。

3.虚假唤醒

#include
#include
#include
#include
#include
#include

using std::list;
using std::cout;
using std::endl;

class Task;
pthread_mutex_t mutex;
list tasks;
pthread_cond_t cond;

class Task{
public:
        Task(int taskID){ this->taskID = taskID;}

        void doTask(){
                pthread_t pid = pthread_self();
                cout << "task id = "<6.读写锁 
  

其实读取数据在很多情况下是安全的,如果使用各种安全措施,会造成资源浪费,毕竟看一下又不会少一块肉,不会修改值。

1.初始化和销毁

读写锁的类型是pthread_rwlock_t

(1).int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t* attr);

静态初始化:pthread_rwlock_t rd = PTHREAD_RWLOCK_INITIALIZER

(2).int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);

2.请求读锁的接口

(1).int pthread_rwlock_rdlock(pthread_rwlock_t* rwlcok);

(2).int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);

(3).int pthread_rwlock_timedrdlock(pthread_rwlock_t* rwlock,const struct timespec* abstime);

3.请求写锁的接口

(1).int pthread_rwlock_wrlock(pthread_rwlock_t* rwlcok);

(2).int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);

(3).int pthread_rwlock_timedwrlock(pthread_rwlock_t* rwlock,const struct timespec* abstime);

读锁用于共享模式也就是读锁形式下可以获取多个读锁,如果是写模式,则会阻塞

写锁用于独占模式,被写锁占据,则其他进程都被阻塞。

4.释放锁

int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

5.读写锁的属性

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr, int pref);

int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t*attr, int* pref);

第二个参数pref设定了属性,取值自己查一查man

6.对读写锁初始化和销毁的函数

(1).int  pthread_rwlockattr_init(pthread_rwlockattr_t*attr);

(2).int pthread_rwlockattr_destroy(pthread_rwlockattr_t*attr);

创建一个读写锁,获取读锁的可能性比获取写锁的可能性大很多。除非延长读锁的时间或者设置写锁优先。

7.c++多线程实现消息线程池和队列系统

关于c++的多线程这里不做介绍了,可以看看其他文章;

比如线程库,互斥量metux,操作互斥量的封装lock_guard,unique_lock等等

条件变量condition_variable

这里实现一个线程队列池,实现多线程处理多任务

#include 
#include
#include
#include
#include
#include
#include

using namespace std;

//任务 
class Task{
public:
	void DoTask(){
		cout << "执行任务" << endl;
	}
	
	~Task()
	{
		cout << "任务执行结束" << endl;
	}
	
};

//线程队列 
class PthreadList{
public:
	
	PthreadList(int num = 5)
	{
		if(num <= 0) 
			num = 5;
		runable = true;
		for(int i{0}; i < num; i++)
		{
			shared_ptr ptr;
			ptr.reset(new thread(bind(PthreadList::threadFunc, this)));
			m_threads.push_back(ptr);
		}
	}
	
	
	~PthreadList()
	{
		removeAllTasks(); 
	}

	//线程函数 
	void threadFunc()
	{
		shared_ptr ptr_task;
		while(true)
		{
			{
			unique_lock guard(m_mutexList);//上锁
			//条件变量存在的意义,当条件满足的时候被唤醒 
			while(_taskList.empty()) //检查任务是否为空
			 {
			 	//如果不允许执行,则跳出 
			 	if(!runable)
				 	break;
				//等待条件满足,满足则唤醒 ,不满足则在此阻塞 
				cv.wait(guard); 
				 
			 }
			 
			 	if(!runable)
				 	break;
				
				//获取任务 
				ptr_task = _taskList.front();
				_taskList.pop_front();
			}
			//执行任务
			if(ptr_task == NULL)
				continue;
			
			ptr_task->DoTask(); 
			ptr_task.reset();
		}	
	}
	
	//等到所有线程结束 
	void stop(){
		runable = false;
		cv.notify_all();
		for(auto& ptr : m_threads)
			if(ptr->joinable())
				ptr->join();
	}
	
	//添加任务到队列 
	void addTask(Task* task)
	{
		if(task == NULL)
			return;
		shared_ptr ptr;
		ptr.reset(task);
		
		{
			lock_guard m(m_mutexList);
			_taskList.push_back(ptr);
		}
		cv.notify_one();
		 
	}
	
	void removeAllTasks()
	{
		lock_guard guard(m_mutexList);
		for(auto& iter : _taskList)
			iter.reset();
		_taskList.clear();
	}
	
private:
	list> _taskList;
	mutex m_mutexList;
	condition_variable cv;
	bool runable{false};
	vector> m_threads; 
	
};

int main(int argc, char** argv) {

	PthreadList plist(5);
	Task* ptr_task;
	for(int i = 0; i < 10; i++)
		plist.addTask(new Task());
	
	plist.stop();
	


	return 0;
}

为什么要一起使用?

可以思考一下:

pthread_mutex_lock(&mutex);

while(条件不满足):

        pthread_mutet_unlock(&mutex);

        条件等待

        pthread_mutex_lock(&mutex);

end

如果线程运行到解锁,cpu切换,另一个线程处理了某个条件,然后发送信号;信号就会被之前的线程错过,条件会一直等待下去。

所以,加锁和条件要是一个步骤,具有原子性。

你可能感兴趣的:(面试,c++)