{"id":403531,"date":"2024-06-29T17:10:54","date_gmt":"2024-06-29T17:10:54","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=403531"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=403531","title":{"rendered":"<span>Multithreading in Photon<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><strong>What this article is about\u00a0<\/strong><\/p>\n<p>In this article, we will talk about multithreading in the backend.\u00a0<\/p>\n<ul>\n<li>\n<p>how it is implemented\u00a0<\/p>\n<\/li>\n<li>\n<p>how is it used\u00a0<\/p>\n<\/li>\n<li>\n<p>what can be done\u00a0<\/p>\n<\/li>\n<li>\n<p>what we invented ourselves\u00a0<\/p>\n<\/li>\n<\/ul>\n<p>All these questions are relevant only if you develop something for the server side &#8212; modify the Server SDK code, write your own plugin, or even start some server application from scratch.\u00a0<\/p>\n<h4>What is Photon?\u00a0<\/h4>\n<p><em>Photon <\/em>or <em>Photon Engine <\/em>is a well-known solution for implementing multiplayer games. Using one of their client libraries, developers (or even a single developer) implements data exchange between players. The client library establishes a connection to the backend which can be the <em>Photon Cloud <\/em>or the developer\u2019s own servers.<\/p>\n<h4>How does Photon solve the issue of multithreading?\u00a0<\/h4>\n<p>The photon server application accepts requests from multiple client connections at the same time. I will call such connections <strong><em>peers<\/em><\/strong>. These requests form queues. One for each peer. If the peers are connected to the same room, their queues are merged into one &#8212; the room queue.\u00a0<\/p>\n<p>There are up to several thousand such rooms, and their request queues are processed in parallel.\u00a0<\/p>\n<p>As a basis for the implementation of task queues in Photon, the Retlang library was used, which was developed on the basis of the Jetlang library.\u00a0<\/p>\n<h4>Why don&#8217;t we use Task and async\/await<\/h4>\n<p>It\u2019s because of<strong> <\/strong>the following considerations:\u00a0<\/p>\n<ol>\n<li>\n<p>Photon Server development started before the appearance of these features<\/p>\n<\/li>\n<li>\n<p>The number of tasks that are performed by fibers is huge &#8212; tens of thousands per second. Therefore, there was no point in adding another abstraction, which, as it seems to me, also causes<strong> <\/strong>GC (Garbage Collector). The fiber abstraction is much more subtle, so to speak.\u00a0<\/p>\n<\/li>\n<li>\n<p>For sure, there is a <em>TaskScheduler <\/em>that does the same thing as fibers and I would have learned about it in the comments, but in general, I did not want to reinvent the wheel.\u00a0<\/p>\n<\/li>\n<\/ol>\n<h4>What is a Fiber?\u00a0<\/h4>\n<p>A fiber is a class that implements a command queue. The commands are queued and executed <strong>one after the other <\/strong>&#8212; FIFO. We can say that the template multiple writers &#8212; single reader is implemented here. Once again, I want to draw attention to the fact that the commands are executed in the order in which they were received, i.e. one after the other. This is the basis for the security of data access in a multithreaded environment.\u00a0<\/p>\n<p>Although in <em>Photon <\/em>we use only one fiber type, namely <em>PoolFiber<\/em>, the library provides five types. All of them implement the <em>IFiber <\/em>interface. Here is a short description of each.\u00a0<\/p>\n<ul>\n<li>\n<p><strong><em>ThreadFiber <\/em><\/strong>&#8212; an <strong>IFiber <\/strong>backed by a dedicated thread. Use for frequent or performance sensitive operations.\u00a0<\/p>\n<\/li>\n<li>\n<p><strong><em>PoolFiber <\/em><\/strong>&#8212; an <strong>IFiber <\/strong>backed by the .NET thread pool. Note<strong>:<\/strong> execution is still sequential and only executes on one pool thread at a time. Use for infrequent, less performance-sensitive executions, or when one desires to not raise the thread count.\u00a0<\/p>\n<\/li>\n<li>\n<p><strong><em>FormFiber<\/em><\/strong>\/<strong><em>DispatchFiber <\/em><\/strong>&#8212; an <strong>IFiber <\/strong>backed by a <strong>WinForms<\/strong>\/<strong>WPF <\/strong>message pump. The <strong>FormFiber<\/strong>\/<strong>DispatchFiber <\/strong>entirely removes the need to call Invoke or BeginInvoke to communicate with a window from a different thread.\u00a0<\/p>\n<\/li>\n<li>\n<p><strong><em>StubFiber <\/em><\/strong>&#8212; useful for deterministic testing. Fine grain control is given over execution to make <strong>testing races simple<\/strong>. Executes all actions on the caller thread\u00a0<\/p>\n<\/li>\n<\/ul>\n<h4>About PoolFiber\u00a0<\/h4>\n<p>Let\u2019s talk about tasks execution in PoolFiber. Even though it uses a thread pool, the tasks in it are still executed sequentially and only one thread is used at a time. It works like this:\u00a0<\/p>\n<ol>\n<li>\n<p>We enqueue a task in the fiber and it starts to be executed. To do this, the <em>ThreadPool.QueueUserWorkItem <\/em>is called. And at some point, one thread is selected from the pool and it performs this task.\u00a0<\/p>\n<\/li>\n<li>\n<p>If while the first task was running, we set several more tasks, then at the end of the first task, all the new ones are taken from the queue and the <em>ThreadPool.QueueUserWorkItem <\/em>is called again, so that all these tasks are sent for execution. A new thread from the pool will be selected for them. And when it finishes, if there are tasks in the queue, everything repeats from the beginning.\u00a0<\/p>\n<\/li>\n<\/ol>\n<p>That is, each time a new batch of tasks is executed by a new thread from the pool, but <strong><em>only <\/em><\/strong>ONE at a time. Therefore, if all the tasks for working with the game room are placed in its fiber, you can safely access the room data from them (tasks). If the object is accessed from tasks running in different fibers, synchronization is required.\u00a0<\/p>\n<h4>Why PoolFiber\u00a0<\/h4>\n<p><em>Photon <\/em>uses <em>PoolFiber <\/em>everywhere. First of all, just because it does not create additional threads and anyone who needs it can have their own fiber. By the way, we modified it a little and now it can&#8217;t be stopped. I.e. <em>PoolFiber.Stop <\/em>will not stop the execution of the current tasks. It was important for us.\u00a0<\/p>\n<p>You can set tasks in the fiber from any thread. All this is thread-safe. A task that is currently being executed can also enqueue new tasks in the fiber in which it is being executed.\u00a0<\/p>\n<p>There are three ways to set a task in fiber:\u00a0<\/p>\n<ol>\n<li>\n<p>put the task in the queue\u00a0<\/p>\n<\/li>\n<li>\n<p>put a task in a queue that will be executed after a certain interval\u00a0<\/p>\n<\/li>\n<li>\n<p>put a task in a queue that will be executed regularly.\u00a0<\/p>\n<\/li>\n<\/ol>\n<p>It looks something like this:\u00a0<\/p>\n<pre><code>\/\/ equeue task\u00a0  fiber.Enqueue(()=>{some action code;});\u00a0  \/\/ schedule a task to be executed in 10 seconds\u00a0  var scheduledAction = fiber.Schedule(()=>{some action code;}, 10_000); ...\u00a0  \/\/ stop the timer\u00a0  scheduledAction.Dispose()\u00a0  \/\/ schedule a task to be executed in 10 seconds and repeat every 5 seconds var scheduledAction = fiber.Schedule(()=>{some action code;}, 10_000, 5_000); ...\u00a0  \/\/ stop the timer\u00a0  scheduledAction.Dispose()\u00a0<\/code><\/pre>\n<p>For tasks that run at some interval, it is important to keep the reference to the object<strong> <\/strong>returned by <em>fiber.Schedule<\/em>. This is the only way to stop the execution of such a task.\u00a0<\/p>\n<h4>Executors<\/h4>\n<p>Now about the executors. These are the classes that actually execute the tasks. They implement the Execute(Action a) and Execute(List&lt;Action> a) methods. <em>PoolFiber <\/em>uses the second one. That is, the tasks fall into the executor in a batch. What happens to them next depends on the executor. At first, we used the <em>DefaultExecutor <\/em>class. All it does is:\u00a0<\/p>\n<pre><code>public void Execute(List&lt;Action> toExecute)\u00a0  {\u00a0     foreach (var action in toExecute)\u00a0     {\u00a0        Execute(action);\u00a0     }\u00a0     }\u00a0  public void Execute(Action toExecute)\u00a0  {\u00a0     if (_running)\u00a0    {\u00a0      toExecute();\u00a0    }\u00a0  }\u00a0<\/code><\/pre>\n<h4>What else did we invent ourselves\u00a0<\/h4>\n<p><strong>BeforeAfterExecutor\u00a0<\/strong><\/p>\n<p>Later, we added another executor to solve our logging problems. It is called <em>BeforeAfterExecutor<\/em>. It &#171;wraps&#187; the executor passed to it. If nothing is passed, <em>FailSafeBatchExecutor <\/em>is created. A special feature of <em>BeforeAfterExecutor <\/em>is the ability to perform an action before executing the task list and another action after executing the task list. The constructor looks like this:\u00a0<\/p>\n<p>public BeforeAfterExecutor(Action beforeExecute, Action afterExecute, IExecutor executor = null)\u00a0<\/p>\n<p>What is it used for? The fiber and the executor have the same owner. When creating an executor, two actions are passed to it. The first one adds key\/value pairs to the thread context, and the second one removes them, thereby performing the cleaner function. The pairs added to the thread context are added by the logging system to the messages and we can see some meta data of the object that left the message.\u00a0<\/p>\n<p>Example:\u00a0<\/p>\n<p><code>var beforeAction = ()=>\u00a0<\/code><\/p>\n<p><code>{\u00a0<\/code><\/p>\n<p><code>  log4net.ThreadContext.Properties[\"Meta1\"] = \"value\";\u00a0<\/code><\/p>\n<p><code>};\u00a0<\/code><\/p>\n<p><code>var afterAction = () => ThreadContext.Properties.Clear();\u00a0<\/code><\/p>\n<p><code>\/\/we create an executor\u00a0<\/code><\/p>\n<p><code>var e = new BeforeAfterExecutor(beforeAction, afterAction);\u00a0<\/code><\/p>\n<p><code>\/\/we create PoolFiber\u00a0<\/code><\/p>\n<p><code>var fiber = new PoolFiber(e);\u00a0<\/code><\/p>\n<p>Now, if something is logged from a task that runs in <em>fiber<\/em>, log4net will add the <em>Meta1 <\/em>tag with the value <em>value<\/em>.\u00a0<\/p>\n<p><strong>ExtendedPoolFiber and ExtendedFailSafeExecutor\u00a0<\/strong><\/p>\n<p>There is another thing that was not in the original version of <em>retlang<\/em>, and that we developed later. This was preceded by the following story<strong>:<\/strong>There is <em>PoolFiber <\/em>(this is the one that runs on top of the .NET thread pool). In the task that this fiber executes, we needed to execute a HTTP request synchronously.\u00a0<\/p>\n<p>We did it in a simple way like this:\u00a0<\/p>\n<p>1. before executing the request, we create <em>sync event<\/em>;\u00a0<\/p>\n<p>2. the task that executes the request is sent to another fiber, and, upon completion, puts <em>sync event <\/em>in the signaled stage;\u00a0<\/p>\n<p>3.after that, we start to wait for <em>sync event<\/em>.\u00a0<\/p>\n<p>It was not the best solution in terms of scalability and began to give an unexpected failure. It turned out that the task that we put in another fiber in step two falls into the queue of the very thread that started to wait for <em>sync event<\/em>. Thus, we get a deadlock. Not always. But often enough to worry about it.\u00a0<\/p>\n<p>The solution was implemented in <em>ExtendedPoolFiber <\/em>and <em>ExtendedFailSafeExecutor<\/em>. We came up with the idea of putting the entire fiber on pause. In this state, it can accumulate new tasks in the queue, but does not execute them. In order to pause the fiber, the <em>Pause <\/em>method is called. As soon as it is called, the fiber (namely, the fiber executor) waits until the current task is completed and freezes. All other tasks will wait for the first of the two events:\u00a0<\/p>\n<ol>\n<li>\n<p>Call of method <em>Resume\u00a0<\/em><\/p>\n<\/li>\n<li>\n<p>Timeout (specified when calling the Pause method). In the <em>Resume <\/em>method, you can also set a task that will be executed before all the queued tasks.\u00a0<\/p>\n<\/li>\n<\/ol>\n<p>We use this trick when the plugin needs to load the room state using an HTTP request. In order for players to see the updated state of the room immediately, the room&#8217;s fiber is paused. When calling the <em>Resume <\/em>method, we pass it a task that applies the loaded state and all other tasks are already working with the updated state..\u00a0<\/p>\n<p>By the way, the need to put the fiber <em>on pause completely killed the ability to use _ThreadFiber <\/em>for the task queue of game rooms.\u00a0<\/p>\n<p><strong>IFiberAction\u00a0<\/strong><\/p>\n<p><em>IFiberAction <\/em>is an experiment to reduce the load on the GC. We can&#8217;t control the process of creating actions in .NET. Therefore, it was decided to replace the standard actions with instances of the class that implements the <em>IFiberAction <\/em>interface. It is assumed that instances of such classes are taken from the object pool and returned there immediately after completion. This reduces the load on the GC.\u00a0<\/p>\n<p>The <em>IFiberAction <\/em>interface looks like this:\u00a0<\/p>\n<p><code>public interface IFiberAction\u00a0<\/code><\/p>\n<p><code>{\u00a0<\/code><\/p>\n<p><code>  void Execute()\u00a0<\/code><\/p>\n<p><code>  void Return()\u00a0<\/code><\/p>\n<p><code>}\u00a0<\/code><\/p>\n<p>The <em>Execute <\/em>method contains exactly what needs to be executed. The <em>Return <\/em>method is called after <em>Execute <\/em>when it is time to return the object to the pool.\u00a0<\/p>\n<p>Example:\u00a0<\/p>\n<p><code>public class PeerHandleRequestAction : IFiberAction\u00a0<\/code><\/p>\n<p><code>{\u00a0<\/code><\/p>\n<p><code>  public static readonly ObjectPool&lt;PeerHandleRequestAction> Pool = initialization; <\/code><\/p>\n<p><code>  public OperationRequest Request {get; set;}\u00a0<\/code><\/p>\n<p><code>  public PhotonPeer Peer {get; set;}\u00a0<\/code><\/p>\n<p><code>public void Execute()\u00a0<\/code><\/p>\n<p><code>{\u00a0<\/code><\/p>\n<p><code>  this.Peer.HandleRequest(this.Request);\u00a0<\/code><\/p>\n<p><code>}\u00a0<\/code><\/p>\n<p><code>  public void Return()\u00a0<\/code><\/p>\n<p><code>  {\u00a0<\/code><\/p>\n<p><code>    this.Peer = null;\u00a0<\/code><\/p>\n<p><code>    this.Request = null;\u00a0<\/code><\/p>\n<p><code>    Pool.Return(this);\u00a0<\/code><\/p>\n<p><code>  }\u00a0<\/code><\/p>\n<p><code>}\u00a0<\/code><\/p>\n<p><code>\/\/now we use it next way\u00a0<\/code><\/p>\n<p><code>var action = PeerHandleRequestAction.Pool.Get();\u00a0<\/code><\/p>\n<p><code>action.Peer = peer;\u00a0<\/code><\/p>\n<p><code>action.Request = request;\u00a0<\/code><\/p>\n<p><code>peer.Fiber.Enqueue(action);\u00a0<\/code><\/p>\n<h4>Conclusion\u00a0<\/h4>\n<p>In conclusion, I will briefly summarize: To ensure thread-safety in <em>Photon<\/em>, we use task queues, which in our case are represented by fibers. The main type of fiber that we use is <em>PoolFiber <\/em>and classes that extend it. <em>PoolFiber <\/em>implements a task queue on top of the standard .NET thread pool. Due to the small performance footprint<strong> <\/strong>of <em>PoolFiber<\/em>, everyone who needs it can have their own fiber. If you need to pause the task queue, use <em>ExtendedPoolFiber<\/em>.\u00a0<\/p>\n<p>The executors that implement the <em>IExecutor <\/em>interface directly perform tasks in fibers. <em>DefaultExecutor <\/em>is good for everyone, but in case of an exception, it loses the entire remainder of the tasks that were passed to it for execution. <em>FailSafeExecutor <\/em><strong>seems like a reasonable choice in this regard<\/strong>. If you need to perform some action before the executor executes a batch of tasks and after it, <em>BeforeAfterExecutor can be useful<\/em><\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/559314\/\"> https:\/\/habr.com\/ru\/articles\/559314\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><strong>What this article is about\u00a0<\/strong><\/p>\n<p>In this article, we will talk about multithreading in the backend.\u00a0<\/p>\n<ul>\n<li>\n<p>how it is implemented\u00a0<\/p>\n<\/li>\n<li>\n<p>how is it used\u00a0<\/p>\n<\/li>\n<li>\n<p>what can be done\u00a0<\/p>\n<\/li>\n<li>\n<p>what we invented ourselves\u00a0<\/p>\n<\/li>\n<\/ul>\n<p>All these questions are relevant only if you develop something for the server side &#8212; modify the Server SDK code, write your own plugin, or even start some server application from scratch.\u00a0<\/p>\n<h4>What is Photon?\u00a0<\/h4>\n<p><em>Photon <\/em>or <em>Photon Engine <\/em>is a well-known solution for implementing multiplayer games. Using one of their client libraries, developers (or even a single developer) implements data exchange between players. The client library establishes a connection to the backend which can be the <em>Photon Cloud <\/em>or the developer\u2019s own servers.<\/p>\n<h4>How does Photon solve the issue of multithreading?\u00a0<\/h4>\n<p>The photon server application accepts requests from multiple client connections at the same time. I will call such connections <strong><em>peers<\/em><\/strong>. These requests form queues. One for each peer. If the peers are connected to the same room, their queues are merged into one &#8212; the room queue.\u00a0<\/p>\n<p>There are up to several thousand such rooms, and their request queues are processed in parallel.\u00a0<\/p>\n<p>As a basis for the implementation of task queues in Photon, the Retlang library was used, which was developed on the basis of the Jetlang library.\u00a0<\/p>\n<h4>Why don&#8217;t we use Task and async\/await<\/h4>\n<p>It\u2019s because of<strong> <\/strong>the following considerations:\u00a0<\/p>\n<ol>\n<li>\n<p>Photon Server development started before the appearance of these features<\/p>\n<\/li>\n<li>\n<p>The number of tasks that are performed by fibers is huge &#8212; tens of thousands per second. Therefore, there was no point in adding another abstraction, which, as it seems to me, also causes<strong> <\/strong>GC (Garbage Collector). The fiber abstraction is much more subtle, so to speak.\u00a0<\/p>\n<\/li>\n<li>\n<p>For sure, there is a <em>TaskScheduler <\/em>that does the same thing as fibers and I would have learned about it in the comments, but in general, I did not want to reinvent the wheel.\u00a0<\/p>\n<\/li>\n<\/ol>\n<h4>What is a Fiber?\u00a0<\/h4>\n<p>A fiber is a class that implements a command queue. The commands are queued and executed <strong>one after the other <\/strong>&#8212; FIFO. We can say that the template multiple writers &#8212; single reader is implemented here. Once again, I want to draw attention to the fact that the commands are executed in the order in which they were received, i.e. one after the other. This is the basis for the security of data access in a multithreaded environment.\u00a0<\/p>\n<p>Although in <em>Photon <\/em>we use only one fiber type, namely <em>PoolFiber<\/em>, the library provides five types. All of them implement the <em>IFiber <\/em>interface. Here is a short description of each.\u00a0<\/p>\n<ul>\n<li>\n<p><strong><em>ThreadFiber <\/em><\/strong>&#8212; an <strong>IFiber <\/strong>backed by a dedicated thread. Use for frequent or performance sensitive operations.\u00a0<\/p>\n<\/li>\n<li>\n<p><strong><em>PoolFiber <\/em><\/strong>&#8212; an <strong>IFiber <\/strong>backed by the .NET thread pool. Note<strong>:<\/strong> execution is still sequential and only executes on one pool thread at a time. Use for infrequent, less performance-sensitive executions, or when one desires to not raise the thread count.\u00a0<\/p>\n<\/li>\n<li>\n<p><strong><em>FormFiber<\/em><\/strong>\/<strong><em>DispatchFiber <\/em><\/strong>&#8212; an <strong>IFiber <\/strong>backed by a <strong>WinForms<\/strong>\/<strong>WPF <\/strong>message pump. The <strong>FormFiber<\/strong>\/<strong>DispatchFiber <\/strong>entirely removes the need to call Invoke or BeginInvoke to communicate with a window from a different thread.\u00a0<\/p>\n<\/li>\n<li>\n<p><strong><em>StubFiber <\/em><\/strong>&#8212; useful for deterministic testing. Fine grain control is given over execution to make <strong>testing races simple<\/strong>. Executes all actions on the caller thread\u00a0<\/p>\n<\/li>\n<\/ul>\n<h4>About PoolFiber\u00a0<\/h4>\n<p>Let\u2019s talk about tasks execution in PoolFiber. Even though it uses a thread pool, the tasks in it are still executed sequentially and only one thread is used at a time. It works like this:\u00a0<\/p>\n<ol>\n<li>\n<p>We enqueue a task in the fiber and it starts to be executed. To do this, the <em>ThreadPool.QueueUserWorkItem <\/em>is called. And at some point, one thread is selected from the pool and it performs this task.\u00a0<\/p>\n<\/li>\n<li>\n<p>If while the first task was running, we set several more tasks, then at the end of the first task, all the new ones are taken from the queue and the <em>ThreadPool.QueueUserWorkItem <\/em>is called again, so that all these tasks are sent for execution. A new thread from the pool will be selected for them. And when it finishes, if there are tasks in the queue, everything repeats from the beginning.\u00a0<\/p>\n<\/li>\n<\/ol>\n<p>That is, each time a new batch of tasks is executed by a new thread from the pool, but <strong><em>only <\/em><\/strong>ONE at a time. Therefore, if all the tasks for working with the game room are placed in its fiber, you can safely access the room data from them (tasks). If the object is accessed from tasks running in different fibers, synchronization is required.\u00a0<\/p>\n<h4>Why PoolFiber\u00a0<\/h4>\n<p><em>Photon <\/em>uses <em>PoolFiber <\/em>everywhere. First of all, just because it does not create additional threads and anyone who needs it can have their own fiber. By the way, we modified it a little and now it can&#8217;t be stopped. I.e. <em>PoolFiber.Stop <\/em>will not stop the execution of the current tasks. It was important for us.\u00a0<\/p>\n<p>You can set tasks in the fiber from any thread. All this is thread-safe. A task that is currently being executed can also enqueue new tasks in the fiber in which it is being executed.\u00a0<\/p>\n<p>There are three ways to set a task in fiber:\u00a0<\/p>\n<ol>\n<li>\n<p>put the task in the queue\u00a0<\/p>\n<\/li>\n<li>\n<p>put a task in a queue that will be executed after a certain interval\u00a0<\/p>\n<\/li>\n<li>\n<p>put a task in a queue that will be executed regularly.\u00a0<\/p>\n<\/li>\n<\/ol>\n<p>It looks something like this:\u00a0<\/p>\n<pre><code>\/\/ equeue task\u00a0  fiber.Enqueue(()=>{some action code;});\u00a0  \/\/ schedule a task to be executed in 10 seconds\u00a0  var scheduledAction = fiber.Schedule(()=>{some action code;}, 10_000); ...\u00a0  \/\/ stop the timer\u00a0  scheduledAction.Dispose()\u00a0  \/\/ schedule a task to be executed in 10 seconds and repeat every 5 seconds var scheduledAction = fiber.Schedule(()=>{some action code;}, 10_000, 5_000); ...\u00a0  \/\/ stop the timer\u00a0  scheduledAction.Dispose()\u00a0<\/code><\/pre>\n<p>For tasks that run at some interval, it is important to keep the reference to the object<strong> <\/strong>returned by <em>fiber.Schedule<\/em>. This is the only way to stop the execution of such a task.\u00a0<\/p>\n<h4>Executors<\/h4>\n<p>Now about the executors. These are the classes that actually execute the tasks. They implement the Execute(Action a) and Execute(List&lt;Action> a) methods. <em>PoolFiber <\/em>uses the second one. That is, the tasks fall into the executor in a batch. What happens to them next depends on the executor. At first, we used the <em>DefaultExecutor <\/em>class. All it does is:\u00a0<\/p>\n<pre><code>public void Execute(List&lt;Action> toExecute)\u00a0  {\u00a0     foreach (var action in toExecute)\u00a0     {\u00a0        Execute(action);\u00a0     }\u00a0     }\u00a0  public void Execute(Action toExecute)\u00a0  {\u00a0     if (_running)\u00a0    {\u00a0      toExecute();\u00a0    }\u00a0  }\u00a0<\/code><\/pre>\n<h4>What else did we invent ourselves\u00a0<\/h4>\n<p><strong>BeforeAfterExecutor\u00a0<\/strong><\/p>\n<p>Later, we added another executor to solve our logging problems. It is called <em>BeforeAfterExecutor<\/em>. It &#171;wraps&#187; the executor passed to it. If nothing is passed, <em>FailSafeBatchExecutor <\/em>is created. A special feature of <em>BeforeAfterExecutor <\/em>is the ability to perform an action before executing the task list and another action after executing the task list. The constructor looks like this:\u00a0<\/p>\n<p>public BeforeAfterExecutor(Action beforeExecute, Action afterExecute, IExecutor executor = null)\u00a0<\/p>\n<p>What is it used for? The fiber and the executor have the same owner. When creating an executor, two actions are passed to it. The first one adds key\/value pairs to the thread context, and the second one removes them, thereby performing the cleaner function. The pairs added to the thread context are added by the logging system to the messages and we can see some meta data of the object that left the message.\u00a0<\/p>\n<p>Example:\u00a0<\/p>\n<p><code>var beforeAction = ()=>\u00a0<\/code><\/p>\n<p><code>{\u00a0<\/code><\/p>\n<p><code>  log4net.ThreadContext.Properties[\"Meta1\"] = \"value\";\u00a0<\/code><\/p>\n<p><code>};\u00a0<\/code><\/p>\n<p><code>var afterAction = () => ThreadContext.Properties.Clear();\u00a0<\/code><\/p>\n<p><code>\/\/we create an executor\u00a0<\/code><\/p>\n<p><code>var e = new BeforeAfterExecutor(beforeAction, afterAction);\u00a0<\/code><\/p>\n<p><code>\/\/we create PoolFiber\u00a0<\/code><\/p>\n<p><code>var fiber = new PoolFiber(e);\u00a0<\/code><\/p>\n<p>Now, if something is logged from a task that runs in <em>fiber<\/em>, log4net will add the <em>Meta1 <\/em>tag with the value <em>value<\/em>.\u00a0<\/p>\n<p><strong>ExtendedPoolFiber and ExtendedFailSafeExecutor\u00a0<\/strong><\/p>\n<p>There is another thing that was not in the original version of <em>retlang<\/em>, and that we developed later. This was preceded by the following story<strong>:<\/strong>There is <em>PoolFiber <\/em>(this is the one that runs on top of the .NET thread pool). In the task that this fiber executes, we needed to execute a HTTP request synchronously.\u00a0<\/p>\n<p>We did it in a simple way like this:\u00a0<\/p>\n<p>1. before executing the request, we create <em>sync event<\/em>;\u00a0<\/p>\n<p>2. the task that executes the request is sent to another fiber, and, upon completion, puts <em>sync event <\/em>in the signaled stage;\u00a0<\/p>\n<p>3.after that, we start to wait for <em>sync event<\/em>.\u00a0<\/p>\n<p>It was not the best solution in terms of scalability and began to give an unexpected failure. It turned out that the task that we put in another fiber in step two falls into the queue of the very thread that started to wait for <em>sync event<\/em>. Thus, we get a deadlock. Not always. But often enough to worry about it.\u00a0<\/p>\n<p>The solution was implemented in <em>ExtendedPoolFiber <\/em>and <em>ExtendedFailSafeExecutor<\/em>. We came up with the idea of putting the entire fiber on pause. In this state, it can accumulate new tasks in the queue, but does not execute them. In order to pause the fiber, the <em>Pause <\/em>method is called. As soon as it is called, the fiber (namely, the fiber executor) waits until the current task is completed and freezes. All other tasks will wait for the first of the two events:\u00a0<\/p>\n<ol>\n<li>\n<p>Call of method <em>Resume\u00a0<\/em><\/p>\n<\/li>\n<li>\n<p>Timeout (specified when calling the Pause method). In the <em>Resume <\/em>method, you can also set a task that will be executed before all the queued tasks.\u00a0<\/p>\n<\/li>\n<\/ol>\n<p>We use this trick when the plugin needs to load the room state using an HTTP request. In order for players to see the updated state of the room immediately, the room&#8217;s fiber is paused. When calling the <em>Resume <\/em>method, we pass it a task that applies the loaded state and all other tasks are already working with the updated state..\u00a0<\/p>\n<p>By the way, the need to put the fiber <em>on pause completely killed the ability to use _ThreadFiber <\/em>for the task queue of game rooms.\u00a0<\/p>\n<p><strong>IFiberAction\u00a0<\/strong><\/p>\n<p><em>IFiberAction <\/em>is an experiment to reduce the load on the GC. We can&#8217;t control the process of creating actions in .NET. Therefore, it was decided to replace the standard actions with instances of the class that implements the <em>IFiberAction <\/em>interface. It is assumed that instances of such classes<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-403531","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/403531","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=403531"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/403531\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=403531"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=403531"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=403531"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}