多线程和异步

xiaoxiao2021-02-28  82

秒杀多线程第一篇 多线程笔试面试题汇总

原文地址:http://blog.csdn.net/morewindows/article/details/7392749

第一题:线程的基本概念、线程的基本状态及状态之间的关系?

线程,有时称为轻量级进程,是CPU使用的基本单元;它由线程ID、程序计数器、寄存器集合和堆栈组成。它与属于同一进程的其他线程共享其代码段、数据段和其他操作系统资源(如打开文件和信号)。

线程有四种状态:新生状态、可运行状态、被阻塞状态、死亡状态。状态之间的转换如下图所示:

 

第二题:线程与进程的区别?

1、 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。 2、 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。 3、 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,出了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。 4、 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了。 5、 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在。

 

第三题:多线程有几种实现方法,都是什么?

  1. 继承 Thread 类   2. 实现 Runnable 接口再 new Thread(YourRunnableOjbect) 

第四题:多线程同步和互斥有几种实现方法,都是什么? 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

 

第五题:多线程同步和互斥有何异同,在什么情况下分别使用他们?举例说明。

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步(下文统称为同步)。

 

 

 

二.选择题

第一题(百度笔试题):

以下多线程对int型变量x的操作,哪几个不需要进行同步:  A. x=y;      B. x++;    C. ++x;    D. x=1;

 

第二题(阿里巴巴笔试题)

多线程中栈与堆是公有的还是私有的

A:栈公有, 堆私有

B:栈公有,堆公有

C:栈私有, 堆公有

D:栈私有,堆私有

 

三.综合题

第一题(台湾某杀毒软件公司面试题):

在Windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。

 

第二题:

一个全局变量tally,两个线程并发执行(代码段都是ThreadProc),问两个线程都结束后,tally取值范围。

inttally = 0;//glable

voidThreadProc()

{

       for(inti = 1; i <= 50; i++)

              tally += 1;

}

 

第三题(某培训机构的练习题):

子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100 次,如此循环50次,试写出代码。

 

第四题(迅雷笔试题):

编写一个程序,开启3个线程,这3个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC….依次递推。

 

第五题(Google面试题)

有四个线程1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推.........现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:

A:1 2 3 4 1 2....

B:2 3 4 1 2 3....

C:3 4 1 2 3 4....

D:4 1 2 3 4 1....

请设计程序。

 

下面的第六题与第七题也是在考研中或是程序员和软件设计师认证考试中的热门试题。

第六题

生产者消费者问题

这是一个非常经典的多线程题目,题目大意如下:有一个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,所有生产者和消费者都是异步方式运行的,但它们必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经装满产品且尚未被取走的缓冲区中投放产品。

 

第七题

读者写者问题

这也是一个非常经典的多线程题目,题目大意如下:有一个写者很多读者,多个读者可以同时读文件,但写者在写文件时不允许有读者在读文件,同样有读者读时写者也不能写。

 

多线程相关题目就列举到此,如果各位有多线程方面的笔试面试题,欢迎提供给我,我将及时补上。谢谢大家。

 

下一篇《多线程第一次亲密接触 CreateThread与_beginthreadex本质区别》将从源代码的层次上讲解创建多线程的二个函数CreateThread与_beginthreadex到底有什么区别,让你明明白白的完成与多线程第一次亲密接触。

 

什么是异步?

异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程。

异步和多线程

相同点:避免调用线程阻塞,从而提高软件的可响应性。

不同点:

异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。C#5.0 .NET4.5 以后关键字Async和Await的使用,使得异步编程变得异常简单。

多线程中的处理程序依然是顺序执行,但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。 5555555555555555555555555555

    线程同步是指同一进程中的多个线程互相协调工作从而达到一致性。之所以需要线程同步,是因为多个线程同时对一个数据对象进行修改操作时,可能会对数据造成破坏,下面是多个线程同时修改同一数据造成破坏的例子:

1 #include <thread> 2 #include <iostream> 3 4 void Fun_1(unsigned int &counter); 5 void Fun_2(unsigned int &counter); 6 7 int main() 8 { 9 unsigned int counter = 0; 10 std::thread thrd_1(Fun_1, counter); 11 std::thread thrd_2(Fun_2, counter); 12 thrd_1.join(); 13 thrd_2.join(); 14 system("pause"); 15 return 0; 16 } 17 18 void Fun_1(unsigned int &counter) 19 { 20 while (true) 21 { 22 ++counter; 23 if (counter < 1000) 24 { 25 std::cout << "Function 1 counting " << counter << "...\n"; 26 } 27 else 28 { 29 break; 30 } 31 } 32 } 33 34 void Fun_2(unsigned int &counter) 35 { 36 while (true) 37 { 38 ++counter; 39 if (counter < 1000) 40 { 41 std::cout << "Function 2 counting " << counter << "...\n"; 42 } 43 else 44 { 45 break; 46 } 47 } 48 }

      运行结果如图所示:

      显然输出的结果存在问题,变量并没有按顺序递增,所以线程同步是很重要的。在这里记录三种线程同步的方式:

  ①使用C++标准库的thread、mutex头文件:

1 #include <thread> 2 #include <mutex> 3 #include <iostream> 4 5 void Fun_1(); 6 void Fun_2(); 7 8 unsigned int counter = 0; 9 std::mutex mtx; 10 11 int main() 12 { 13 std::thread thrd_1(Fun_1); 14 std::thread thrd_2(Fun_2); 15 thrd_1.join(); 16 thrd_2.join(); 17 system("pause"); 18 return 0; 19 } 20 21 void Fun_1() 22 { 23 while (true) 24 { 25 std::lock_guard<std::mutex> mtx_locker(mtx); 26 ++counter; 27 if (counter < 1000) 28 { 29 std::cout << "Function 1 counting " << counter << "...\n"; 30 } 31 else 32 { 33 break; 34 } 35 } 36 } 37 38 void Fun_2() 39 { 40 while (true) 41 { 42 std::lock_guard<std::mutex> mtx_locker(mtx); 43 ++counter; 44 if (counter < 1000) 45 { 46 std::cout << "Function 2 counting " << counter << "...\n"; 47 } 48 else 49 { 50 break; 51 } 52 } 53 }

  这段代码与前面一段代码唯一的区别就是在两个线程关联的函数中加了一句 std::lock_guard<std::mutex> mtx_locker(mtx); 在C++中,通过构造std::mutex的实例来创建互斥元,可通过调用其成员函数lock()和unlock()来实现加锁和解锁,然后这是不推荐的做法,因为这要求程序员在离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。作为替代,标准库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法(资源获取即初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁。代码运行结果如下图所示,可见得到了正确的结果。

 

 

  ②使用windows API的临界区对象:

1 //header.h 2 #ifndef CRTC_SEC_H 3 #define CRTC_SEC_H 4 5 #include "windows.h" 6 7 class RAII_CrtcSec 8 { 9 private: 10 CRITICAL_SECTION crtc_sec; 11 public: 12 RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); } 13 ~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); } 14 RAII_CrtcSec(const RAII_CrtcSec &) = delete; 15 RAII_CrtcSec & operator=(const RAII_CrtcSec &) = delete; 16 // 17 void Lock() { ::EnterCriticalSection(&crtc_sec); } 18 void Unlock() { ::LeaveCriticalSection(&crtc_sec); } 19 }; 20 21 #endif 1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 #include "header.h" 5 6 DWORD WINAPI Fun_1(LPVOID p); 7 DWORD WINAPI Fun_2(LPVOID p); 8 9 unsigned int counter = 0; 10 RAII_CrtcSec cs; 11 12 int main() 13 { 14 HANDLE h1, h2; 15 h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0); 16 std::cout << "Thread 1 started...\n"; 17 h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 cs.Lock(); 31 ++counter; 32 if (counter < 1000) 33 { 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 cs.Unlock(); 36 } 37 else 38 { 39 cs.Unlock(); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 cs.Lock(); 51 ++counter; 52 if (counter < 1000) 53 { 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 cs.Unlock(); 56 } 57 else 58 { 59 cs.Unlock(); 60 break; 61 } 62 } 63 return 0; 64 }

  上面的代码使用了windows提供的API中的临界区对象来实现线程同步。临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占该资源,不允许其他线程访问该资源。在该线程访问完资源后,其他线程才能对资源进行访问。Windows API提供了临界区对象的结构体CRITICAL_SECTION,对该对象的使用可总结为如下几步:

  1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量。

  2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。

  3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。

  4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。

  该段代码的运行结果如下图所示:

 

 

  ③使用Windows API的事件对象:

1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_event; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_event = CreateEvent(nullptr, true, false, nullptr); 14 SetEvent(h_event); 15 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 16 std::cout << "Thread 1 started...\n"; 17 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_event, INFINITE); 31 ResetEvent(h_event); 32 if (counter < 1000) 33 { 34 ++counter; 35 std::cout << "Thread 1 counting " << counter << "...\n"; 36 SetEvent(h_event); 37 } 38 else 39 { 40 SetEvent(h_event); 41 break; 42 } 43 } 44 return 0; 45 } 46 47 DWORD WINAPI Fun_2(LPVOID p) 48 { 49 while (true) 50 { 51 WaitForSingleObject(h_event, INFINITE); 52 ResetEvent(h_event); 53 if (counter < 1000) 54 { 55 ++counter; 56 std::cout << "Thread 2 counting " << counter << "...\n"; 57 SetEvent(h_event); 58 } 59 else 60 { 61 SetEvent(h_event); 62 break; 63 } 64 } 65 return 0; 66 }

  事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程的同步。使用事件对象的步骤可概括如下:

  1.创建事件对象,函数原型为:

1 HANDLE WINAPI CreateEvent( 2 _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, 3 _In_ BOOL bManualReset, 4 _In_ BOOL bInitialState, 5 _In_opt_ LPCTSTR lpName 6 );

  如果该函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:

  -lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL则表示该程序使用的是默认安全属性。

  -bManualReset:表示所创建的事件对象是人工重置还是自动重置。若设为true,则表示使用人工重置,在调用线程获得事件对象所有权后用户要显式地调用ResetEvent()将事件对象设置为无信号状态。

  -bInitialState:表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态,则线程可以使用事件对象。

  -lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。

  2.若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为有信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。

  3.线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:

1 DWORD WINAPI WaitForSingleObject( 2 _In_ HANDLE hHandle, 3 _In_ DWORD dwMilliseconds 4 );

  该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,直到用户所指定的事件到达。

   该代码的运行结果如下图所示:

  

 

  ④使用Windows API的互斥对象:

1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_mutex; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_mutex = CreateMutex(nullptr, false, nullptr); 14 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 15 std::cout << "Thread 1 started...\n"; 16 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 17 std::cout << "Thread 2 started...\n"; 18 CloseHandle(h1); 19 CloseHandle(h2); 20 // 21 //CloseHandle(h_mutex); 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_mutex, INFINITE); 31 if (counter < 1000) 32 { 33 ++counter; 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 ReleaseMutex(h_mutex); 36 } 37 else 38 { 39 ReleaseMutex(h_mutex); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 WaitForSingleObject(h_mutex, INFINITE); 51 if (counter < 1000) 52 { 53 ++counter; 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 ReleaseMutex(h_mutex); 56 } 57 else 58 { 59 ReleaseMutex(h_mutex); 60 break; 61 } 62 } 63 return 0; 64 }

  互斥对象的使用方法和c++标准库的mutex类似,互斥对象使用完后应记得释放。

转载请注明原文地址: https://www.6miu.com/read-43361.html

最新回复(0)